Гайд 04 · для дизайнера и верстальщика
Тема и дизайн
Как устроена тема, как сделать свою и что видит шаблон. Для дизайнера и верстальщика — человека или ИИ-агента.
Тема решает, как выглядит сайт; контент решает, что на нём написано. Тема не знает ни одного текста страницы, контент не знает ни одного класса CSS. Встречаются они по именам: тип страницы выбирает шаблон, якорь секции — модуль.
Простыми словами
Тема — это одежда сайта, а страницы — человек. Один и тот же человек может носить разную одежду, и одежда никогда не решает, что он говорит. HERON держит их отдельно намеренно: тексты не попадают в тему, а в теме нет текстов.
Как они встречаются. У каждой страницы есть тип (главная, страница, статья…), и у темы на каждый тип есть шаблон. У каждой секции есть форма (список, шаги, таблица…), и у темы на каждую форму есть маленький модуль. Собирая страницу, HERON выбирает шаблон по типу и рисует каждую секцию модулем её формы.
Пример. Секцию ## Цены {#prices} с таблицей внутри рисует модуль таблицы из темы. Перейдите на другую тему со своим модулем таблицы — и те же цены появятся в новом оформлении, без правки страницы.
Эта страница нужна, если вы делаете или меняете тему. Её можно пропустить, если вы только пишете страницы: ваша тема их уже рисует.
Из чего состоит
theme/
├── theme.yaml паспорт: типы страниц, их секции, связи, картинки
├── base.html каркас: head, шапка, подвал, блок content
├── templates/ по шаблону на тип: home.html, page.html, service.html, 404.html…
├── modules/ по файлу на модуль: prose.html, steps.html, faq.html…
├── partials/ куски каркаса: header.html, footer.html, analytics.html
├── assets/ CSS, JS, шрифты — копируются в dist как есть
└── i18n/ЯЗЫК.yaml строки интерфейса: en.yaml, ru.yaml — «Read more» / «Читать далее»
heron new кладёт стартовую тему: типы home, page, 404 и модули breadcrumbs, cards, facts, faq, list, prose, steps. Её задача — чтобы сайт собирался с первой минуты, а не чтобы на ней жить. Своя тема начинается с её копии.
theme.yaml
name: main
version: "1.0"
requires: [contact.phone, organization.name] # что тема берёт из site.yaml
modules: [breadcrumbs, prose, facts, list, steps, faq, cards]
types:
home:
uses: [intro, about, services, faq]
service:
uses: [intro, quick-facts, what, "how:steps", faq]
jsonld: [Service]
page:
uses: [intro, "*"] # "*" — любые секции
links: # связи между страницами
- field: related
type: service
back: mentioned_in
images:
widths: [400, 800, 1200, 1600]
ratios: ["1:1", "16:9"]
formats: [avif, webp]
types.ТИП.uses— какие секции тип показывает. Это договор с контентом:heron page service roofположит заготовки ровно этих секций, а сводка «Не заполнено» после сборки назовёт пустые. Секция в файле, которой нет вuses, — строка в отчёте: так ловится опечатка в якоре."how:steps"— тема ждёт у секции формуsteps.requires— пути вsite.yaml, без которых тема не работает: не заполнены — ошибкаE011до рендера.links— поля связей. HERON найдёт страницы, проверит их и построит обратный список (back).typeв связи ограничивает, на страницы какого типа ведёт поле:relatedсtype: serviceне свяжет статьи между собой. Связь без ограничения — безtype:- {field: related, back: related_from}.images— ширины, пропорции, форматы нарезки иfallback_width— ширина запасного PNG или JPEG. Режется только то, что шаблоны попросили.
Каждый ключ подробно — theme.yaml в спецификации.
Шаблон типа
Тип service рисуется templates/service.html. Порядок блоков читается сверху вниз:
{% extends "base.html" %}
{% block content %}
{{ mod.breadcrumbs() }}
<h1>{{ page.h1 }}</h1>
{{ mod.facts('quick-facts') }}
{{ mod.prose('what') }}
{{ mod.steps('how') }}
{{ mod.faq('faq') }}
{% for p in page.related.get('related', []) %}
<a href="{{ p.url }}">{{ p.h1 }}</a>
{% endfor %}
{% endblock %}
mod.prose('what') берёт секцию с якорем what. Нет секции — нет ничего: ни заголовка, ни пустой рамки, условия вокруг не нужны. Поэтому в контенте порядок секций не важен, а у страницы может не хватать секций без вреда для вёрстки.
Шаблон, который не знает заранее, какие секции будут, выбирает модуль по форме — так сделан page.html стартовой темы:
{% for section in page.sections.values() %}
{% if section.kind == 'faq' %}{{ mod.faq(section.id) }}
{% elif section.kind == 'steps' %}{{ mod.steps(section.id) }}
{% else %}{{ mod.prose(section.id) }}{% endif %}
{% endfor %}
Пути и макросы
- Пути в
extends,includeиimportсчитаются от корня темы, а не от папки шаблона:{% extends "base.html" %},{% extends "templates/page.html" %},{% include "partials/header.html" %}. Написать{% extends "page.html" %}изtemplates/landing.html— ошибкаE008с подсказкой, где файл на самом деле. modдоступен в каждом шаблоне — импортировать модули не нужно. Свои макросы — другое дело:{% import %}вbase.htmlдочерним шаблонам не виден (так устроен Jinja2), импортируйте их в каждом шаблоне, где они нужны.
Модуль
Модуль — файл modules/ИМЯ.html, вызывается как mod.ИМЯ(якорь). Внутри доступны:
| Переменная | Что это |
|---|---|
section |
секция: id, title, kind, lead, body, note, callout, links |
items |
разобранные данные формы: шаги, пары вопрос-ответ, факты… |
html |
готовая вёрстка всего содержимого секции |
| всё, что видит шаблон | page, site, t, picture… |
<section id="{{ section.id }}" class="faq">
{% if section.title %}<h2>{{ section.title }}</h2>{% endif %}
{% for pair in items %}
<details><summary>{{ pair.q }}</summary>{{ pair.a|safe }}</details>
{% endfor %}
</section>
Что лежит в items у каждой из двенадцати форм — таблица форм секций, а справочник форм показывает, как каждую рисует тема этого сайта. Модули без аргумента (mod.breadcrumbs()) берут данные из page и site.
Что видит шаблон
| Имя | Что это |
|---|---|
page |
страница: h1, title, url, type, intro, sections, children, parent, breadcrumbs, translations, related, meta.ПОЛЕ |
site |
весь site.yaml, значения уже на языке страницы: site.site.name, site.contact.phone |
data |
содержимое data/*.yaml |
nav |
меню из site.yaml, уже страницами: item.url, item.nav_title |
t |
строки интерфейса текущего языка: t.read_more |
lang, languages, hreflang |
текущий язык, все языки, адреса переводов |
page_of(key) |
страница по ключу на текущем языке; главная — page_of('/') |
picture(src, ratio) |
готовый элемент picture с srcset, WebP и AVIF и размерами |
media_url(src, ratio, width) |
адрес одной нарезки |
jsonld() |
разметка JSON-LD страницы, вставить в head |
env |
окружение сборки: env.indexable, env.analytics |
Полная модель страницы и все переменные — сборка и рендеринг в спецификации.
Обращение к несуществующей переменной валит сборку с именем шаблона и строки, а не выводит пустоту: опечатка в теме ловится сразу.
Свои поля страницы
Тема может ждать во фронтматтере свои поля — topic, eyebrow, что угодно. Объявлять их нигде не нужно: HERON кладёт их в страницу как есть.
{% if page.meta.get('eyebrow') %}<p class="eyebrow">{{ page.meta.get('eyebrow') }}</p>{% endif %}
<span>{{ page.meta.get('topic', 'general') }}</span>
page.meta.get('поле', запасное) не падает, если у страницы поля нет. page.meta.topic упадёт — так ловятся опечатки в полях самого HERON. Все свои поля разом — page.meta.extra.
fields в theme.yaml — только для полей HERON, которые тема ждёт от новой версии. Свои поля туда не пишут: HERON их не знает и ответит E023.
Значения на языке страницы
Шаблон получает site.yaml уже на языке страницы — все блоки, на любой глубине: на русской странице site.contact.address — это address_ru, site.site.name — name_ru, site.organization.about — about_ru. Нет языкового ключа — берётся ключ без хвоста. Выбирать язык фильтром не нужно; ключи с хвостом при этом тоже доступны.
То же правило работает в подстановках разметки ({{ contact.address }} в блоке schema), так что адрес в подвале и в разметке не разойдутся.
Строки интерфейса
Тексты, которые принадлежат теме, а не странице, — в theme/i18n/ЯЗЫК.yaml:
# theme/i18n/en.yaml
read_more: Read more
faq: Questions and answers
# theme/i18n/ru.yaml
read_more: Читать далее
faq: Вопросы и ответы
В шаблоне — {{ t.read_more }}. Не хватает строки — сборка не падает: на странице останется имя ключа, в отчёте будет предупреждение. Зашитый в шаблон текст делает тему одноязычной; держите его в i18n.
Картинки в теме
{{ picture(page.meta.image, '16:9', alt=page.meta.image_alt) }}
HERON отдаёт нарезки под ширины и пропорции из theme.yaml, иллюстрации вписывает, фотографии кропает. Сам путь — из контента; тема решает только пропорцию и место.
Режется только то, что попросил шаблон. ratios в theme.yaml — пропорции, которые тема может попросить; в сборку попадают только те, что шаблоны действительно вызвали через picture() или media_url(). Карточка просит 16:9 — у её картинки в сборке будет только 16:9. Запасной файл в исходном формате (для браузеров без AVIF и WebP) — один на пропорцию, шириной images.fallback_width (по умолчанию 800). Нужна ширина больше — попросите её явно: media_url(src, '16:9', 1200) для og:image.
Подробно: картинки и файлы.
Как проверить тему
heron build— тема рендерится на каждой странице; ошибки шаблона называют файл и строку;- отчёт после сборки: секции, которых нет в
uses, формы не того вида, недостающие строкиi18n; heron check— тот же отчёт без сборки.
Источник
Эта страница повторяет docs/guide/04-theme.md из ветки prod. Репозиторий обновляется первым, поэтому там всегда самая свежая версия.
Следующий гайд: SEO и языки.