🔍 Начните печатать, чтобы искать по книге или перейти к нужной странице по номеру
Удобно листать не только прокруткой, но и клавишами‑стрелками:
Эта книга — пошаговая инструкция по вёрстке сайтов на языках ХТМЛ и ЦСС. Вы узнаете не только как сверстать сайт, но и как опубликовать его в интернете, настроить красивый шаринг в соцсети и подключить системы аналитики.
Отдельный раздел книги посвящён работе с верстальщиком: как ставить задачи, получать предсказуемый результат и правильно принимать и внедрять вёрстку.
Знакомство с ХТМЛ и ЦСС
Модули
Расстановка
Страницы
Спецэффекты
Контроль качества
Публикация
Как работать с верстальщиком
Эта книга — пошаговая инструкция по вёрстке сайтов на языках ХТМЛ и ЦСС. Вы узнаете не только как сверстать сайт, но и как опубликовать его в интернете, настроить красивый шаринг в соцсети и подключить системы аналитики.
Отдельный раздел книги посвящён работе с верстальщиком: как ставить задачи, получать предсказуемый результат и правильно принимать и внедрять вёрстку.
Знакомство с ХТМЛ и ЦСС
Модули
Расстановка
Страницы
Спецэффекты
Контроль качества
Публикация
Как работать с верстальщиком
Добавим ещё ряд и соберём всё вместе:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Портфолио Романа</title>
<link rel="stylesheet" href="reset.css">
</head>
<body>
<div class="layoutContainer">
<section class="projects">
<div class="project sizeL">
<a href="/projects/maximilyahov-identity/">
<div class="project-media">
<img src="/projects/maximilyahov-identity/files/cover.jpg">
</div>
<div class="project-caption">
<u>Максим Ильяхов</u> десять лет учит интернет писать. Теперь у него есть фирстиль
</div>
</a>
</div>
<div class="project sizeS">
<a href="/projects/worldchess-font/">
<div class="project-media">
<img src="/projects/worldchess-font/files/cover.jpg">
</div>
<div class="project-caption">
<u>Шрифт для цен</u> Ворлд Чесс
</div>
</a>
</div>
<div class="project sizeM">
<a href="/projects/worldchess-2/">
<div class="project-media">
<img src="/projects/worldchess-2/files/cover.jpg">
</div>
<div class="project-caption">
<u>Вторая версия главной страницы</u> Ворлд Чесса
</div>
</a>
</div>
<div class="project sizeS">
<a href="/projects/worldchess-broadcasts/">
<div class="project-media">
<img src="/projects/worldchess-broadcasts/files/cover.jpg">
</div>
<div class="project-caption">
<u>Ворлд Чесс</u> ведёт трансляции шахматных турниров
</div>
</a>
</div>
<div class="project sizeM">
<a href="/projects/vashrepetitor-logo/">
<div class="project-media">
<img src="/projects/vashrepetitor-logo/files/cover.jpg">
</div>
<div class="project-caption">
<u>Логотип</u> «Вашего репетитора» — сервиса подбора репетиторов
</div>
</a>
</div>
<div class="project sizeM">
<a href="/projects/mailganer/">
<div class="project-media">
<img src="/projects/mailganer/files/cover.jpg">
</div>
<div class="project-caption">
<u>Сайт Мейлганера</u> — кросс-канальной платформы маркетинга
</div>
</a>
</div>
</section>
</div>
<script>
const MOBILE_LAYOUT = ['L', 'S', 'S']
document
.querySelectorAll('.project')
.forEach((project, index) => {
const layoutClass = MOBILE_LAYOUT[index % MOBILE_LAYOUT.length]
const mobileClass = `mobileSize${layoutClass}`
project
.classList
.add(mobileClass)
})
</script>
</body>
</html>
:root {
--textColor: #000;
--linkColor: #00aff0;
--keyColor: #ff5e00;
}
body {
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
color: var(--textColor);
}
a,
a u {
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: .175em;
text-decoration-color: color-mix(in srgb, currentColor, transparent 80%);
text-decoration-skip: none;
text-decoration-skip-ink: none;
color: var(--linkColor);
}
a:has(u) {
text-decoration: none;
}
a:hover,
a:hover u {
color: var(--keyColor);
text-decoration-color: currentColor;
}
.projects {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-auto-rows: max-content;
column-gap: 20px;
row-gap: 20px;
}
.project {
padding: 10px 10px 20px;
border-radius: 4px;
background: #f3f5f4;
min-height: 33vh;
overflow: hidden;
}
.project a,
.project a:hover {
color: var(--textColor);
}
.project-media {
margin: -10px -10px 0;
aspect-ratio: 1.91 / 1;
}
.project-caption {
margin-top: .5em;
font-size: 12px;
line-height: 1.2;
}
.project.sizeS {
grid-column: auto / span 1;
}
.project.sizeM {
grid-column: auto / span 2;
}
.project.sizeL {
grid-column: auto / span 4;
}
@media (width <= 768px) {
.projects {
grid-template-columns: 1fr 1fr;
grid-auto-flow: dense;
}
.project.mobileSizeS,
.project.mobileSizeM {
grid-column: auto / span 1;
}
.project.sizeL,
.project.mobileSizeL {
grid-column: auto / span 2;
}
}Добавим ещё ряд и соберём всё вместе:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Портфолио Романа</title>
<link rel="stylesheet" href="reset.css">
</head>
<body>
<div class="layoutContainer">
<section class="projects">
<div class="project sizeL">
<a href="/projects/maximilyahov-identity/">
<div class="project-media">
<img src="/projects/maximilyahov-identity/files/cover.jpg">
</div>
<div class="project-caption">
<u>Максим Ильяхов</u> десять лет учит интернет писать. Теперь у него есть фирстиль
</div>
</a>
</div>
<div class="project sizeS">
<a href="/projects/worldchess-font/">
<div class="project-media">
<img src="/projects/worldchess-font/files/cover.jpg">
</div>
<div class="project-caption">
<u>Шрифт для цен</u> Ворлд Чесс
</div>
</a>
</div>
<div class="project sizeM">
<a href="/projects/worldchess-2/">
<div class="project-media">
<img src="/projects/worldchess-2/files/cover.jpg">
</div>
<div class="project-caption">
<u>Вторая версия главной страницы</u> Ворлд Чесса
</div>
</a>
</div>
<div class="project sizeS">
<a href="/projects/worldchess-broadcasts/">
<div class="project-media">
<img src="/projects/worldchess-broadcasts/files/cover.jpg">
</div>
<div class="project-caption">
<u>Ворлд Чесс</u> ведёт трансляции шахматных турниров
</div>
</a>
</div>
<div class="project sizeM">
<a href="/projects/vashrepetitor-logo/">
<div class="project-media">
<img src="/projects/vashrepetitor-logo/files/cover.jpg">
</div>
<div class="project-caption">
<u>Логотип</u> «Вашего репетитора» — сервиса подбора репетиторов
</div>
</a>
</div>
<div class="project sizeM">
<a href="/projects/mailganer/">
<div class="project-media">
<img src="/projects/mailganer/files/cover.jpg">
</div>
<div class="project-caption">
<u>Сайт Мейлганера</u> — кросс-канальной платформы маркетинга
</div>
</a>
</div>
</section>
</div>
<script>
const MOBILE_LAYOUT = ['L', 'S', 'S']
document
.querySelectorAll('.project')
.forEach((project, index) => {
const layoutClass = MOBILE_LAYOUT[index % MOBILE_LAYOUT.length]
const mobileClass = `mobileSize${layoutClass}`
project
.classList
.add(mobileClass)
})
</script>
</body>
</html>
:root {
--textColor: #000;
--linkColor: #00aff0;
--keyColor: #ff5e00;
}
body {
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
color: var(--textColor);
}
a,
a u {
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: .175em;
text-decoration-color: color-mix(in srgb, currentColor, transparent 80%);
text-decoration-skip: none;
text-decoration-skip-ink: none;
color: var(--linkColor);
}
a:has(u) {
text-decoration: none;
}
a:hover,
a:hover u {
color: var(--keyColor);
text-decoration-color: currentColor;
}
.projects {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-auto-rows: max-content;
column-gap: 20px;
row-gap: 20px;
}
.project {
padding: 10px 10px 20px;
border-radius: 4px;
background: #f3f5f4;
min-height: 33vh;
overflow: hidden;
}
.project a,
.project a:hover {
color: var(--textColor);
}
.project-media {
margin: -10px -10px 0;
aspect-ratio: 1.91 / 1;
}
.project-caption {
margin-top: .5em;
font-size: 12px;
line-height: 1.2;
}
.project.sizeS {
grid-column: auto / span 1;
}
.project.sizeM {
grid-column: auto / span 2;
}
.project.sizeL {
grid-column: auto / span 4;
}
@media (width <= 768px) {
.projects {
grid-template-columns: 1fr 1fr;
grid-auto-flow: dense;
}
.project.mobileSizeS,
.project.mobileSizeM {
grid-column: auto / span 1;
}
.project.sizeL,
.project.mobileSizeL {
grid-column: auto / span 2;
}
}Раскладка как в Пинтересе — это частный случай плитки, в которой высота ячеек переменная. Карточки вписываются в колонки, а по рядам располагаются так, чтобы прижаться вплотную друг к другу.
К сожалению, в ЦСС пока нет хорошей поддержки этой раскладки. В светлом будущем она могла бы включаться с помощью grid-template-rows: masonry. Но пока это поддерживается только в последней версии Сафари и Файрфокса, если включить в настройках.
.projects {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-auto-rows: max-content;
grid-template-rows: masonry;
column-gap: 20px;
row-gap: 20px;
}
Пока поддержка в браузерах хромает, можно сымитировать такую раскладку вручную. Для этого нужно назначить проектам колонки, следя за их высотой. Не должно быть слишком высоких или слишком низких колонок, нижний край раскладки должен быть как можно ровнее. В Пинтересе за этим автоматически следит код, в нашем случае — мы сами.
<section class="projects">
<!-- Добавим колонки, по которым будем расставлять проекты: -->
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="project col-1"></div> <!-- Этот проект пойдёт в первую колонку -->
<div class="project col-2"></div> <!-- Этот проект пойдёт во вторую колонку -->
<div class="project col-3"></div> <!-- В третью колонку -->
<div class="project col-4"></div> <!-- В четвёртую колонку -->
</section>
<script>
const columns = document
.querySelectorAll('.projects > .col') // Находим колонки
columns.forEach((column, index) => {
const projects = document
.querySelectorAll(`.project.col-${index + 1}`)
column.append(...projects) // Перемещаем в них проекты с подходящим классом
})
</script>
.projects {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
column-gap: 20px;
}
.projects > .col { /* Теперь грид-контейнер содержит только четыре колонки, а проекты вложены в них */
display: grid;
grid-auto-rows: max-content;
row-gap: 20px;
}
.project {
padding: 10px 10px 20px;
background: #f3f5f4;
border-radius: 4px;
overflow: hidden;
}
.project a {
color: var(--textColor);
}
.project-media {
margin: -10px -10px 0;
}
.project-caption {
margin-top: .5em;
font-size: 14px;
line-height: 1.25;
}
@media (width <= 768px) {
.projects { /* На мобильных перестраиваем колонки по две в ряд */
grid-template-columns: 1fr 1fr;
}
}
И, конечно, есть третий вариант сделать такую раскладку — с помощью программирования и готовых библиотек:
Раскладка как в Пинтересе — это частный случай плитки, в которой высота ячеек переменная. Карточки вписываются в колонки, а по рядам располагаются так, чтобы прижаться вплотную друг к другу.
К сожалению, в ЦСС пока нет хорошей поддержки этой раскладки. В светлом будущем она могла бы включаться с помощью grid-template-rows: masonry. Но пока это поддерживается только в последней версии Сафари и Файрфокса, если включить в настройках.
.projects {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-auto-rows: max-content;
grid-template-rows: masonry;
column-gap: 20px;
row-gap: 20px;
}
Пока поддержка в браузерах хромает, можно сымитировать такую раскладку вручную. Для этого нужно назначить проектам колонки, следя за их высотой. Не должно быть слишком высоких или слишком низких колонок, нижний край раскладки должен быть как можно ровнее. В Пинтересе за этим автоматически следит код, в нашем случае — мы сами.
<section class="projects">
<!-- Добавим колонки, по которым будем расставлять проекты: -->
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="col"></div>
<div class="project col-1"></div> <!-- Этот проект пойдёт в первую колонку -->
<div class="project col-2"></div> <!-- Этот проект пойдёт во вторую колонку -->
<div class="project col-3"></div> <!-- В третью колонку -->
<div class="project col-4"></div> <!-- В четвёртую колонку -->
</section>
<script>
const columns = document
.querySelectorAll('.projects > .col') // Находим колонки
columns.forEach((column, index) => {
const projects = document
.querySelectorAll(`.project.col-${index + 1}`)
column.append(...projects) // Перемещаем в них проекты с подходящим классом
})
</script>
.projects {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
column-gap: 20px;
}
.projects > .col { /* Теперь грид-контейнер содержит только четыре колонки, а проекты вложены в них */
display: grid;
grid-auto-rows: max-content;
row-gap: 20px;
}
.project {
padding: 10px 10px 20px;
background: #f3f5f4;
border-radius: 4px;
overflow: hidden;
}
.project a {
color: var(--textColor);
}
.project-media {
margin: -10px -10px 0;
}
.project-caption {
margin-top: .5em;
font-size: 14px;
line-height: 1.25;
}
@media (width <= 768px) {
.projects { /* На мобильных перестраиваем колонки по две в ряд */
grid-template-columns: 1fr 1fr;
}
}
И, конечно, есть третий вариант сделать такую раскладку — с помощью программирования и готовых библиотек:
Задача промостраницы — продать товар или услугу, собрать контактную информацию посетителей. Наполнение зависит от того, что и кому продаём. Обычно это разные комбинации картинок продукта и текста о его преимуществах.
На промах обычно размещают призыв к действию (call to action) — модуль с кнопкой покупки, формой регистрации или телефоном для связи.
Разберём вёрстку промостраницы на примере бюро.
Задача промостраницы — продать товар или услугу, собрать контактную информацию посетителей. Наполнение зависит от того, что и кому продаём. Обычно это разные комбинации картинок продукта и текста о его преимуществах.
На промах обычно размещают призыв к действию (call to action) — модуль с кнопкой покупки, формой регистрации или телефоном для связи.
Разберём вёрстку промостраницы на примере бюро.
Выразительную шапку страницы в западном веб‑дизайне называют «героем» (hero). Часто в её вёрстке используют aspect-ratio и абсолютное позиционирование, чтобы расставить модули вокруг и поверх текста.
Здесь и далее в коде сосредоточимся на раскладке, не вдаваясь в подробности стилизации внутренних модулей. Об этом достаточно сказано в прошлых главах.
<!-- Контейнер на всю ширину -->
<div class="hero">
<!-- Контентная область с ограниченной шириной -->
<div class="heroIn">
<div class="heroInfo">
<b>Дистанционный курс …</b>
<p>Перерождение классического курса …</p>
<p>
<span class="serif">24 февраля — 22 марта.</span>
<b class="heroExpire">Запись открыта до ВС 23 фев</b>
</p>
</div>
<h1 class="heroTitle">
<!-- Здесь br уместен -->
Переговоры <br/>
и отношения <br/>
с клиентами
<span class="heroTitleVersion">2.0</span>
</h1>
<img class="heroAuthor" src="./ilya.png" alt="Фото Ильи Синельникова">
<img class="heroGorilla" src="./gorilla.png" alt="…">
<img class="heroChuvak" src="./chuvak.png" alt="…">
</div>
</div>
.hero {
background: #FCBF32;
color: #000;
padding-top: 60px;
}
.heroIn {
max-width: 768px;
margin: 0 auto;
aspect-ratio: 16/9;
/* Контекст для позиционирования «абсолютных» модулей */
position: relative;
}
.heroInfo {
margin-bottom: 10px;
}
.heroExpire {
color: #fc5621;
}
.heroTitle {
font-size: 6vw;
text-transform: uppercase;
}
.heroTitleVersion {
display: block;
text-align: right;
color: #fc5621;
}
.heroAuthor {
position: absolute;
bottom: 0;
/* Трюк для центровки модуля: сдвиг 50% родителя + смещение -50% собственной ширины*/
left: 50%;
translate: -50% 0;
max-width: 50%;
}
.heroGorilla {
position: absolute;
top: 5%;
right: 3%;
max-width: 25%;
transform: rotate(-5deg);
}
.heroChuvak {
position: absolute;
bottom: 25%;
left: 7%;
max-width: 15%;
transform: rotate(-5deg);
}Выразительную шапку страницы в западном веб‑дизайне называют «героем» (hero). Часто в её вёрстке используют aspect-ratio и абсолютное позиционирование, чтобы расставить модули вокруг и поверх текста.
Здесь и далее в коде сосредоточимся на раскладке, не вдаваясь в подробности стилизации внутренних модулей. Об этом достаточно сказано в прошлых главах.
<!-- Контейнер на всю ширину -->
<div class="hero">
<!-- Контентная область с ограниченной шириной -->
<div class="heroIn">
<div class="heroInfo">
<b>Дистанционный курс …</b>
<p>Перерождение классического курса …</p>
<p>
<span class="serif">24 февраля — 22 марта.</span>
<b class="heroExpire">Запись открыта до ВС 23 фев</b>
</p>
</div>
<h1 class="heroTitle">
<!-- Здесь br уместен -->
Переговоры <br/>
и отношения <br/>
с клиентами
<span class="heroTitleVersion">2.0</span>
</h1>
<img class="heroAuthor" src="./ilya.png" alt="Фото Ильи Синельникова">
<img class="heroGorilla" src="./gorilla.png" alt="…">
<img class="heroChuvak" src="./chuvak.png" alt="…">
</div>
</div>
.hero {
background: #FCBF32;
color: #000;
padding-top: 60px;
}
.heroIn {
max-width: 768px;
margin: 0 auto;
aspect-ratio: 16/9;
/* Контекст для позиционирования «абсолютных» модулей */
position: relative;
}
.heroInfo {
margin-bottom: 10px;
}
.heroExpire {
color: #fc5621;
}
.heroTitle {
font-size: 6vw;
text-transform: uppercase;
}
.heroTitleVersion {
display: block;
text-align: right;
color: #fc5621;
}
.heroAuthor {
position: absolute;
bottom: 0;
/* Трюк для центровки модуля: сдвиг 50% родителя + смещение -50% собственной ширины*/
left: 50%;
translate: -50% 0;
max-width: 50%;
}
.heroGorilla {
position: absolute;
top: 5%;
right: 3%;
max-width: 25%;
transform: rotate(-5deg);
}
.heroChuvak {
position: absolute;
bottom: 25%;
left: 7%;
max-width: 15%;
transform: rotate(-5deg);
}Иногда нет времени или возможности верстать сложную типографику заголовка.
Можно сделать заголовок картинкой, но это плохо для поисковых роботов и программ чтения с экрана.
Трюк: разместите поверх картинки визуально скрытый «честный» заголовок. Поставьте текст как можно точнее по картинке, чтобы выделение текста выглядело адекватно.
Скройте текст, сделав его прозрачным.
Опасность. Используйте трюк исключительно для дублирования текста, содержащегося на картинке. Намеренное скрытие текста от пользователей называется гостингом (ghosting) и сурово наказывается поисковиками, вплоть до исключения сайта из поисковой выдачи.
<div class="fakeHeading">
<!-- По возможности используйте СВГ -->
<img class="fakeHeadingImg" src="./fake-heading.svg" alt="Красивый заголовок: переговоры и отношения с клиентами">
<h1 class="fakeHeadingHidden">
Переговоры <br>
и отношения <br>
с клиентами
</h1>
</div>
.fakeHeading {
max-width: 100%;
position: relative;
overflow: hidden; /* На всякий случай, чтобы текст настоящего заголовка не протёк за пределы модуля */
}
.fakeHeadingImg {
display: block;
width: 100%;
}
.fakeHeadingHidden {
position: absolute;
top: 0;
left: 0;
font-size: 5vw;
color: transparent;
/* Подгоняем текст по к картинке */
margin: 0;
text-transform: uppercase;
font-size: 12.2vw;
line-height: 1.05;
}
Иногда нет времени или возможности верстать сложную типографику заголовка.
Можно сделать заголовок картинкой, но это плохо для поисковых роботов и программ чтения с экрана.
Трюк: разместите поверх картинки визуально скрытый «честный» заголовок. Поставьте текст как можно точнее по картинке, чтобы выделение текста выглядело адекватно.
Скройте текст, сделав его прозрачным.
Опасность. Используйте трюк исключительно для дублирования текста, содержащегося на картинке. Намеренное скрытие текста от пользователей называется гостингом (ghosting) и сурово наказывается поисковиками, вплоть до исключения сайта из поисковой выдачи.
<div class="fakeHeading">
<!-- По возможности используйте СВГ -->
<img class="fakeHeadingImg" src="./fake-heading.svg" alt="Красивый заголовок: переговоры и отношения с клиентами">
<h1 class="fakeHeadingHidden">
Переговоры <br>
и отношения <br>
с клиентами
</h1>
</div>
.fakeHeading {
max-width: 100%;
position: relative;
overflow: hidden; /* На всякий случай, чтобы текст настоящего заголовка не протёк за пределы модуля */
}
.fakeHeadingImg {
display: block;
width: 100%;
}
.fakeHeadingHidden {
position: absolute;
top: 0;
left: 0;
font-size: 5vw;
color: transparent;
/* Подгоняем текст по к картинке */
margin: 0;
text-transform: uppercase;
font-size: 12.2vw;
line-height: 1.05;
}