Русский
Компоненты шаблонов
Собирайте свои шаблоны из компонентов. Компонент — это 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 доходит до файлов компонентов:
- Сначала компоненты, которые вызывают другие компоненты: шапка, которую вызывает базовый компонент, кнопка внутри карточки.
- Потом компоненты, которые вызывает сама страница, — в порядке их первого
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), поэтому он не устареет незаметно. Текст в шаблоне английский, потому что тест сверяет вывод дословно.
Лучшие практики
Следуйте им, когда пишете или меняете свой шаблон:
- Собирайте страницу из компонентов, не копируйте разметку. Если одна и та же разметка встретилась дважды — вынесите её в файл компонента и вызывайте через
yield. - Один компонент — один файл. В файле HTML-блок компонента и при необходимости его блоки
_style_и_js_. Больше ничего. - Имена блоков — через
@lid, классы — через@did, никогда вручную. Тогда имена следуют за путём файла, и два файла не пересекутся. - Все классы — по BEM:
.@did,.@did__element,.@did--modifier. Не стилизуйте голые теги и классы чужих компонентов. - CSS компонента — в блоке
_style_@lid, вывод — один раз через<style>{{ yield_blocks("_style_") }}</style>в<head>. - На страницах полагайтесь на автоимпорт.
{{ import }}пишите только в шаблоне, до которого дошли черезinclude,extendsилиexec: туда автоимпорт не доходит (см. #Автоимпорт). - CSS темы — в одном базовом компоненте, который вызывает каждая страница: только переменные в
:rootи селекторы тегов. На порядок стилей не полагайтесь (см. #Базовый слой). - У каждого параметра блока — значение по умолчанию, аргументы — по имени.
- Необязательный поиск проверяйте в том же
if, где используете:{{ if about := nvs.ByPermalink("/about"); about }}…{{ end }}. Поиск, который ничего не нашёл, возвращаетnil, поэтому работают иif x, иx == nil. - Текст экранирует шаблон. Вывод экранируется по умолчанию: заголовкам, строкам frontmatter и всему, что написал автор или посетитель, фильтр не нужен. Методы, которые возвращают HTML, выводятся как есть.
| unsafe— только для разметки, которую собирает сам шаблон (см. #Экранирование). exec— для данных, компоненты — для разметки. Файл дляexecотдаёт черезreturnсписок или словарь, общий для нескольких шаблонов. Вtry/catchоборачивайте только ту часть, которая может законно упасть, и показывайте ошибку админам.- Список других заметок — через запросы
nvsс.Public()на любой странице, которую видят анонимные посетители:nvs.ByGlob("blog/*.md").Public(). Без.Public()в список попадут платные заметки, заметки только для вошедших и заметки под_. Сортируйте с дополнительным ключом: без сортировки порядок случайный. - Контент — в заметках, не в шаблонах. Настройки и списки — во frontmatter, читаются через
note.M(). Текст — в теле заметки, выводится черезnote.HTMLString()илиnote.PartialRenderer(). Структурированные данные, например график, — в блоке кода в заметке. В шаблоне — разметка, а не контент.
Экранирование
{{ value }} экранирует HTML и в тексте, и внутри атрибутов: {{ post.Title() }} выведет Q&A как Q&A. Методы, которые возвращают готовый HTML сервера, выводятся как есть: {{ note.HTMLString() }}, TitleHTML, ContentHTML, asset(). | html из старых шаблонов не мешает: он экранирует один раз. | unsafe выводит обычную строку без экранирования; он для разметки, которую собирает сам шаблон, и никогда — для текста, который написал кто-то другой. Подробнее — Справочник функций Jet.
Смотрите также
- Шаблоны — как устроены шаблоны
- Справочник функций Jet — все функции, фильтры и конструкции
- yield_blocks — CSS и JS по страницам,
@lid/@did,asset() - BEM-именование в шаблонах
- Отладка Jet-шаблонов
- Превью лейаута — проверить шаблон без загрузки