Read in:
Русский

Компоненты шаблонов

Собирайте свои шаблоны из компонентов. Компонент — это Jet-блок block в отдельном файле в _layouts/, который страница вызывает через {{ yield name(param="value") }}. Имя блока задавайте через @lid, CSS-классы — по BEM через @did, а импорт оставьте trip2g: {{ import }} не нужен. CSS компонентов собирайте через yield_blocks("_style_"). Краткая версия этой страницы — список #Лучшие практики ниже.

Если вы ещё не делали свой шаблон, начните с Шаблонов. Все функции и конструкции — в Справочнике функций Jet.

Что такое компонент

Компонент — один файл в _layouts/, в котором определены:

  • HTML-блок с именем по файлу: {{ block @lid(...) }}
  • при необходимости CSS-блок {{ block _style_@lid() }} и JS-блок {{ block _js_@lid() }}

Страница вызывает HTML-блок через yield. trip2g находит файл, где определён блок, импортирует его и собирает CSS всех компонентов страницы в один тег <style>.

Файлы и имена блоков

_layouts/
├── page.html
└── components/
    ├── button.html
    └── card.html

Компонентом может быть любой .html-файл в _layouts/, в любой папке. components/ — соглашение, а не требование. Блоки ищутся по всем файлам шаблонов сайта, поэтому страница из одной папки может вызвать блок из другой.

Перед разбором файла trip2g подставляет вместо двух плейсхолдеров имена из пути файла относительно _layouts/:

Файл @lid (имена блоков) @did (CSS-классы)
button.html button button
components/card.html components_card components-card
site/components/card.html site_components_card site-components-card
my-theme/card.html my_theme_card my-theme-card

Страница вызывает components/card.html так: {{ yield components_card(...) }}. Полное описание плейсхолдеров, включая @@lid для буквального @lid, — в yield_blocks.

@lid заменяет на _ каждый символ, которого не может быть в имени Jet (/, -, ., пробел), и ставит _ перед цифрой в начале: из 2col/card.html получится _2col_card. @did заменяет только / на -. Поэтому имена папок и файлов начинайте с буквы и не используйте в них . и пробелы: @did их сохраняет, а класс вроде 2col-card или v1.2-card CSS в таком виде не выберет.

Вызов компонента

{{ yield components_button(label="Все посты", url="/blog") }}
  • Передавайте параметры по имени. Позиционный аргумент, yield components_button("Все посты"), игнорируется: параметр остаётся со значением по умолчанию, а без него выводит false.

  • Задавайте значение по умолчанию каждому параметру в определении: {{ block @lid(label="", url="", featured=false) }}. Тогда у непереданного параметра известное значение.

  • Вложенное содержимое передаётся через content после вызова. Компонент выводит его через {{ yield content }}, а если вызывающий ничего не передал — ничего не выводит:

    {{ yield components_card(title="Документация", url="/docs") content }}
      <p>Всё здесь станет телом карточки.</p>
    {{ end }}
    
  • content — зарезервированное слово. С параметром content файл компонента не загрузится.

  • Значение после вызова становится . внутри блока: {{ yield components_list() items }}.

Автоимпорт

Чтобы использовать компонент, странице не нужен {{ import }}. Все компоненты, которые она вызывает через {{ yield имя(...) }}, находятся и подключаются автоматически — вместе с компонентами, которые вызывают они сами: карточка, внутри которой кнопка, подтянет и файл кнопки. Достаточно вызвать блок по имени.

Поиск идёт по исходнику шаблона, а не по результату рендера, поэтому компонент подключится, даже если его yield стоит внутри if, ложного на этом запросе. Как это устроено внутри загрузчика, описано для разработчиков trip2g в dev/layouts (раздел «Автоимпорт компонентов»); автору шаблонов это знать не нужно.

Совпадение имён. Если два файла определяют блок с одним именем, загрузка не падает:

  • Между файлами компонентов побеждает файл, чей путь идёт раньше по алфавиту: a/card.html побеждает b/card.html. Страница, которая вызывает yield_blocks, показывает в превью шаблона предупреждение block "card" defined in both /a/card and /b/card. На странице без yield_blocks предупреждения нет. С именами через @lid такое совпадение невозможно: у двух файлов не бывает одного пути.
  • Блок, определённый в самой странице, побеждает одноимённый блок компонента. Помните, что {{ block }} в странице ещё и выводится там, где определён.

Когда явный {{ import }} всё же нужен. Автоимпорт работает для той страницы, которая рендерится, в том числе для страницы, которая начинается с {{ extends }}. В другие шаблоны, которые она подключает, он не заглядывает:

Случай Что происходит Что делать
Шаблон, подключённый через {{ include "partials/x" }}, вызывает компонент Рендер останавливается: unresolved block "components_card" Добавьте {{ import "components/card" }} в начало подключаемого шаблона или сделайте его компонентом и вызывайте через yield
Родительский шаблон, подключённый через {{ extends "base" }}, вызывает компонент Рендер останавливается: unresolved block … Добавьте {{ import "components/card" }} в начало родительского шаблона
Файл, который выполняют через exec("lib/x"), вызывает компонент Рендер останавливается: unresolved block … Добавьте {{ import "components/card" }} в начало этого файла

Явный {{ import }} вместе с автоимпортом того же файла работает, так что лишний импорт страницу не сломает.

yield блока, которого нет ни в одном файле, падает при рендере: unresolved block "name". Админ видит ошибку на странице и в /_system/renderlayout.

CSS компонента: BEM и yield_blocks

CSS компонента лежит в блоке _style_@lid, классы названы по BEM от @did:

{{ block _style_@lid() }}
.@did { display: block; }
.@did--featured { border-color: #0070f3; }
.@did__title { font-size: 1.25rem; }
{{ end }}
Часть BEM Вид Пример для components/card.html
Блок .@did .components-card
Элемент .@did__name .components-card__title
Модификатор .@did--name .components-card--featured

Страница выводит CSS один раз, в <head>:

<style>{{ yield_blocks("_style_") }}</style>

yield_blocks включает блоки _style_ только тех компонентов, до которых доходит эта страница: на странице без карточки нет CSS карточки. В блоке _style_ не должно быть параметров и yield: его вызывают без аргументов, а стилевой блок, который вызывает другой стилевой блок, выведет тот CSS дважды. JS-блоки работают так же: <script>{{ yield_blocks("_js_") }}</script>. Подробнее — yield_blocks и BEM-именование в шаблонах.

CSS попадает во встроенный тег <style>. Если сайт отдаёт заголовок Content-Security-Policy, в нём нужен style-src 'unsafe-inline', иначе браузер отбросит CSS компонентов.

Базовый слой

Общий CSS сайта — переменные темы, сброс стилей, типографику — держите в одном компоненте, который вызывает каждая страница, например components/base.html. Сделайте его заодно рамкой страницы, чтобы страница не могла его забыть:

{{ block _style_@lid() }}
:root { --text: #1a1a1a; --accent: #0070f3; }
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font: 16px/1.5 system-ui, sans-serif; color: var(--text); }
a { color: var(--accent); }
{{ end }}

{{ block @lid(title="") }}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>{{ title }}</title>
  <style>{{ yield_blocks("_style_") }}</style>
</head>
<body>
  {{ yield content }}
</body>
</html>
{{ end }}

Страница оборачивает своё содержимое в базовый компонент:

{{ yield components_base(title=note.Title()) content }}
  {{ note.HTMLString() }}
  {{ yield components_button(label="All posts", url="/blog") }}
{{ end }}

В базовом слое — только переменные в :root и селекторы тегов (body, a, h1), никаких классов. Компоненты читают переменные, .@did { color: var(--accent); }, поэтому тема меняется в одном файле.

Порядок стилей в <style>

yield_blocks("_style_") выводит блоки стилей в том порядке, в каком trip2g доходит до файлов компонентов:

  1. Сначала компоненты, которые вызывают другие компоненты: шапка, которую вызывает базовый компонент, кнопка внутри карточки.
  2. Потом компоненты, которые вызывает сама страница, — в порядке их первого yield на странице.

Страница, которая вызывает base (а он — header), а затем card (а она — button), получит CSS в порядке header, button, base, card. То есть CSS базового слоя не обязательно идёт первым.

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

Пример

Три файла: карточка, кнопка и страница со списком трёх новых публичных постов.

_layouts/
├── page.html
└── components/
    ├── button.html
    └── card.html

components/card.html:

{{ block _style_@lid() }}
.@did {
  display: block;
  padding: 16px;
  border: 1px solid #ddd;
  border-radius: 8px;
}
.@did--featured { border-color: #0070f3; }
.@did__title { margin: 0 0 8px; font-size: 1.25rem; }
{{ end }}

{{ block @lid(title="", url="", featured=false) }}
<a class="@did{{ if featured }} @did--featured{{ end }}" href="{{ url }}">
  <h3 class="@did__title">{{ title }}</h3>
  {{ yield content }}
</a>
{{ end }}

components/button.html:

{{ block _style_@lid() }}
.@did {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 6px;
  background: #0070f3;
  color: #fff;
}
{{ end }}

{{ block @lid(label="", url="") }}
<a class="@did" href="{{ url }}">{{ label }}</a>
{{ end }}

page.html, без {{ import }}:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>{{ note.Title() }}</title>
  <style>{{ yield_blocks("_style_") }}</style>
</head>
<body>
  {{ note.HTMLString() }}

  {{ blog := nvs.ByGlob("blog/*.md").Public() }}
  {{ sorted := blog.SortByMeta("date").Desc().SortBy("Title") }}
  {{ range i, post := sorted.Limit(3).All() }}
    {{ yield components_card(
      title=post.Title(),
      url=post.PermalinkEncoded(),
      featured=i == 0
    ) content }}
      {{ if summary := post.M().GetString("summary", ""); summary }}
        <p>{{ summary }}</p>
      {{ end }}
    {{ end }}
  {{ end }}

  {{ yield components_button(label="All posts", url="/blog") }}
</body>
</html>

В <style> попадает CSS только .components-card и .components-button. Самый новый пост получает components-card--featured, а у поста с summary во frontmatter под заголовком появляется абзац.

Этот пример рендерит тест в репозитории trip2g (internal/layoutloader/components_doc_example_test.go), поэтому он не устареет незаметно. Текст в шаблоне английский, потому что тест сверяет вывод дословно.

Лучшие практики

Следуйте им, когда пишете или меняете свой шаблон:

  1. Собирайте страницу из компонентов, не копируйте разметку. Если одна и та же разметка встретилась дважды — вынесите её в файл компонента и вызывайте через yield.
  2. Один компонент — один файл. В файле HTML-блок компонента и при необходимости его блоки _style_ и _js_. Больше ничего.
  3. Имена блоков — через @lid, классы — через @did, никогда вручную. Тогда имена следуют за путём файла, и два файла не пересекутся.
  4. Все классы — по BEM: .@did, .@did__element, .@did--modifier. Не стилизуйте голые теги и классы чужих компонентов.
  5. CSS компонента — в блоке _style_@lid, вывод — один раз через <style>{{ yield_blocks("_style_") }}</style> в <head>.
  6. На страницах полагайтесь на автоимпорт. {{ import }} пишите только в шаблоне, до которого дошли через include, extends или exec: туда автоимпорт не доходит (см. #Автоимпорт).
  7. CSS темы — в одном базовом компоненте, который вызывает каждая страница: только переменные в :root и селекторы тегов. На порядок стилей не полагайтесь (см. #Базовый слой).
  8. У каждого параметра блока — значение по умолчанию, аргументы — по имени.
  9. Необязательный поиск проверяйте в том же if, где используете: {{ if about := nvs.ByPermalink("/about"); about }}…{{ end }}. Поиск, который ничего не нашёл, возвращает nil, поэтому работают и if x, и x == nil.
  10. Текст экранирует шаблон. Вывод экранируется по умолчанию: заголовкам, строкам frontmatter и всему, что написал автор или посетитель, фильтр не нужен. Методы, которые возвращают HTML, выводятся как есть. | unsafe — только для разметки, которую собирает сам шаблон (см. #Экранирование).
  11. exec — для данных, компоненты — для разметки. Файл для exec отдаёт через return список или словарь, общий для нескольких шаблонов. В try/catch оборачивайте только ту часть, которая может законно упасть, и показывайте ошибку админам.
  12. Список других заметок — через запросы nvs с .Public() на любой странице, которую видят анонимные посетители: nvs.ByGlob("blog/*.md").Public(). Без .Public() в список попадут платные заметки, заметки только для вошедших и заметки под _. Сортируйте с дополнительным ключом: без сортировки порядок случайный.
  13. Контент — в заметках, не в шаблонах. Настройки и списки — во frontmatter, читаются через note.M(). Текст — в теле заметки, выводится через note.HTMLString() или note.PartialRenderer(). Структурированные данные, например график, — в блоке кода в заметке. В шаблоне — разметка, а не контент.

Экранирование

{{ value }} экранирует HTML и в тексте, и внутри атрибутов: {{ post.Title() }} выведет Q&A как Q&amp;A. Методы, которые возвращают готовый HTML сервера, выводятся как есть: {{ note.HTMLString() }}, TitleHTML, ContentHTML, asset(). | html из старых шаблонов не мешает: он экранирует один раз. | unsafe выводит обычную строку без экранирования; он для разметки, которую собирает сам шаблон, и никогда — для текста, который написал кто-то другой. Подробнее — Справочник функций Jet.

Смотрите также