Перейти к содержанию

Общий layout, хедер и футер

Страницы статей уже имеют самостоятельные маршруты, но их HTML-оболочка повторяется. Выделим Base.astro, который будет отвечать за название вкладки, описание, canonical, хедер, основной контейнер и футер. Урок показывает, как менять общую часть библиотеки в одном месте, не перемешивая её с содержанием Markdown.

Компонент layout принимает title, необязательное description и canonicalPath. Значения описаны интерфейсом Props. Название статьи передаётся как данные, а не вставляется в строку HTML вручную. При обычном отображении Astro экранирует текстовые значения. Не стоит обходить это поведение лишь для того, чтобы разрешить случайную разметку в названии материала.

Общая оболочка без копирования документа

slot обозначает место содержимого конкретной страницы. Layout не знает, сколько заголовков и абзацев находится в уроке. Статья передаёт туда H1 и Content, главная — короткое представление библиотеки. Такой договор отделяет общий каркас от материала, но не требует превращать каждый абзац в отдельный компонент.

Canonical строится через new URL относительно Astro.site. Это даёт абсолютный адрес на выбранном домене. По умолчанию используется путь текущей страницы; позднее для старого адреса мы передадим explicit canonicalPath. Признак нельзя назначать наугад: он должен описывать публичный адрес именно этого материала, а не главную страницу всей библиотеки.

Общие стили импортируются внутри layout. Их область должна включать Markdown-содержимое, поэтому в этом уроке используется отдельный файл site.css. Изолированный стиль компонента полезен для конкретного виджета, но может не охватить элементы, которые приходят через slot и рендер Markdown. При появлении сложной темы стоит явно различать глобальную типографику и стили отдельных компонентов.

Хедер содержит обычные ссылки. В основном контейнере есть идентификатор для ссылки пропуска. Футер стоит после содержимого и прижимается вниз благодаря flex-структуре всего документа: основной контейнер занимает доступное пространство. Его не делаем fixed, иначе он мог бы перекрыть конец длинной статьи. Поведение короткой и длинной страницы имеет общий договор без ручного расчёта высоты.

Сейчас хедер содержит ссылку на главную, а главная предлагает открыть первый материал по уже существующему адресу. Каталог будет создан следующим этапом; тогда общий layout получит соответствующий пункт. Такой порядок сохраняет рабочие маршруты каждого полного снимка. Пока можно рассмотреть хедер и футер на главной и статьях, не считая готовой всю будущую структуру библиотеки.

Ожидаемый результат — одинаковая общая оболочка у главной страницы и пяти статей. Изменение надписи в футере должно отразиться во всех этих документах после новой сборки. До сборки исходный файл является инструкцией генератору, а не мгновенным обновлением файлов, уже лежащих на хостинге. Это особенно важно при объяснении, почему редактирование Markdown ещё не означает публикацию.

Для будущего просмотра сравните короткую главную и более длинную статью. Футер должен занимать нижнюю часть короткой страницы, но уходить после текста длинной. Затем рассмотрите title, описание и canonical в исходном HTML. Проверка внешнего вида не заменяет проверку метаданных: одинаковый красивый хедер может сопровождать ошибочный адрес страницы.

Полный файл src/layouts/Base.astro текущего этапа:

---
import '../styles/site.css';
interface Props { title: string; description?: string; canonicalPath?: string; }
const { title, description = 'Учебная библиотека Полка', canonicalPath = Astro.url.pathname } = Astro.props;
const canonical = new URL(canonicalPath, Astro.site);
---
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>{title} · Полка</title><meta name="description" content={description}><link rel="canonical" href={canonical}></head><body><a href="#main">Перейти к содержанию</a><header><a href="/">Полка</a> <nav aria-label="Главная навигация"><a href="/">Главная</a></nav></header><main id="main"><slot /></main><footer>Полка · учебная библиотека</footer></body></html>

Полный файл src/pages/articles/[id].astro текущего этапа:

---
import { getCollection, render } from 'astro:content';
import Base from '../../layouts/Base.astro';
export async function getStaticPaths() {
  return (await getCollection('lessons', ({ data }) => !data.draft)).map(entry => ({ params: { id: entry.id }, props: { entry } }));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
---
<Base title={entry.data.title} description={entry.data.description}><h1>{entry.data.title}</h1><Content /></Base>

Полный файл src/styles/site.css текущего этапа:

* { box-sizing: border-box; }
:root { color-scheme: light; --accent: #6d28d9; --paper: #fff; --ink: #20202a; }
body { margin: 0; color: var(--ink); background: #f7f6fb; font: 1rem/1.6 system-ui, sans-serif; }
header, main, footer { max-width: 72rem; margin-inline: auto; padding: 1.5rem; }
a { color: #5923b5; text-underline-offset: .18em; }
button { font: inherit; padding: .5rem .9rem; cursor: pointer; }
:focus-visible { outline: 3px solid #17693d; outline-offset: 4px; }
[hidden] { display: none !important; }
.cards { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); list-style: none; padding: 0; }
.card, aside { padding: 1.25rem; border: 1px solid #ddd8e8; border-radius: .7rem; background: white; }
.card h2 { margin-block-start: 0; }
.card-meta { color: #51495f; }
.notice { border-inline-start: .25rem solid var(--accent); padding-inline-start: 1rem; }
dialog { width: min(36rem, calc(100vw - 2rem)); border: 1px solid #b9afca; padding: 1.5rem; border-radius: .8rem; }
dialog::backdrop { background: rgb(20 15 30 / .5); }

body { min-height: 100dvh; display: flex; flex-direction: column; } main { width: 100%; flex: 1; }

Разбор состояния библиотеки

Layout является компонентом, поэтому он может принимать разные данные от разных страниц. Не нужно создавать отдельный почти одинаковый файл для каждой темы лишь ради другого H1. Границу варианта лучше выразить явным свойством, когда отличие действительно относится к общей оболочке. Но не перегружайте Base всей логикой каталога и поиска: список данных принадлежит конкретной странице или общему модулю, а не глобальному футеру.

У canonicalPath есть значение по умолчанию, однако это не заменяет адресный реестр. Временная страница генератора и сохранённый старый путь позднее будут различаться. Поэтому интерфейс layout уже предусматривает override. Такое раннее решение полезно, когда оно основано на известной задаче миграции. Не добавляйте десятки неизвестных свойств на всякий случай: они затрудняют чтение договора и не подтверждают будущую совместимость.

Ссылка пропуска находится до хедера, чтобы её можно было использовать в начале клавиатурного маршрута. Основной контейнер имеет устойчивый id main на каждой странице. Если вложенный компонент добавит второй такой ID, фрагмент станет неоднозначным. Общая оболочка поэтому должна владеть своими идентификаторами, а локальные компоненты — использовать отдельные имена. Визуально два одинаковых ID могут не выдавать проблему, но адресация уже будет неверной.

Для самостоятельного изменения добавьте в футер короткую ссылку на главную. Сначала меняется один исходник Base, затем будущая сборка обновляет все потребляющие документы. Не копируйте полученный HTML обратно в Markdown. Производные файлы служат выдаче, исходные шаблоны — дальнейшему редактированию. Смешение этих слоёв приводит к тому, что одно исправление исчезает при следующей генерации.

Типографика Markdown нуждается в достаточном внимании к заголовкам, абзацам, спискам и коду. Общая ширина 72rem подходит оболочке каталога, но длинному тексту статьи может потребоваться более узкая колонка. Этот урок показывает общий механизм, не окончательно выверенную тему для любой длины материала. При развитии продукта следует отдельно рассмотреть статью с длинным листингом и таблицей, а не только короткие абзацы учебных данных.

Наконец, метаданные должны отражать материал без рекламных повторов. Название вкладки получает суффикс Полка, а описание приходит от записи. Одинаковый общий текст description на всех статьях скрыл бы их различие. Layout сохраняет возможность индивидуальных значений, поэтому центральный шаблон и самостоятельность материалов не противоречат друг другу. Правильный договор объединяет общие элементы и оставляет конкретные данные на своей странице.

Внутри layout не повторяем H1: главный заголовок принадлежит конкретной странице. Это позволяет главной называть библиотеку, а статье называть изучаемое действие. Хедер с брендом остаётся общим ориентиром и не становится вторым основным заголовком. Если дизайн требует крупной надписи в шапке, её визуальный размер не обязан определять семантический уровень. Смысл документа и размер шрифта следует выбирать отдельно.

Условия для самостоятельного наблюдения

Это план будущего разбора, а не журнал выполненных проверок. Рассмотрите одинаковые исходные данные и сопоставьте действие с ожидаемым состоянием.

Условие Ожидаемое состояние и его граница
Короткая главная Основной контейнер занимает свободную высоту, футер остаётся после него.
Длинная статья Футер следует за текстом и не перекрывает нижнюю часть содержания.
Изменена надпись хедера Общий layout использует её для всех потребителей после новой сборки.
Публичный домен изменён Astro.site и абсолютные canonical требуют согласованной конфигурации.
Каталог ещё не создан Главная предлагает существующий первый материал; пункт каталога добавится вместе с маршрутом.
Markdown имеет H1 Повторный заголовок в теле нужно убрать, поскольку главный заголовок формирует страница.

Оглавление серии, курсы ProfessorWeb и полный архив исходников. Для этого урока выберите lesson-04.

Первичные источники: Layouts, Configuration.

Редакционный статус: локальный черновик. Установка, учебные программы, браузерные сценарии, тесты и публикация не выполнялись. Описанные эффекты ожидаемые.