HERON

Гайд 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 и языки.