Шаблоны: лучшие практики
Как организовать шаблоны в проекте с несколькими страницами.
Когда нужна структура
Один шаблон на страницу работает для простых сайтов. Когда появляются общие элементы — шапка, подвал, стили — стоит вынести их в общие блоки.
Паттерн: базовый layout + страницы
Структура:
_layouts/
└── my-theme/
├── blocks.html — общие блоки (шапка, подвал, обёртка)
├── page.html — шаблон обычной страницы
└── landing.html — шаблон лендинга
blocks.html — общие блоки
{{ block main_layout() }}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>{{ note.Title() }} — Мой сайт</title>
<link rel="stylesheet" href="{{ asset("style.css") }}">
</head>
<body>
{{ yield header() }}
<main>
{{ yield content }}
</main>
{{ yield footer() }}
</body>
</html>
{{ end }}
{{ block header() }}
<header>
<a href="/">Главная</a>
<nav>...</nav>
</header>
{{ end }}
{{ block footer() }}
<footer>
<p>2025 Мой сайт</p>
</footer>
{{ end }}
page.html — использует layout
{{ import "blocks" }}
{{ yield main_layout() content }}
<article class="prose">
<h1>{{ note.Title() }}</h1>
{{ note.HTMLString() | unsafe }}
</article>
{{ end }}
landing.html — другой контент в том же layout
{{ import "blocks" }}
{{ yield main_layout() content }}
<div class="hero">
{{ intro := note.PartialRenderer().Introduce() }}
{{ intro.ContentHTML | unsafe }}
</div>
<div class="features">
{{ range b := note.PartialRenderer().Sections(2) }}
<section>
<h2>{{ b.TitleHTML | unsafe }}</h2>
{{ b.ContentHTML | unsafe }}
</section>
{{ end }}
</div>
{{ end }}
Паттерн: общие блоки
Выносите повторяющиеся элементы в блоки:
{{ block card(title, content) }}
<div class="card">
<h3>{{ title }}</h3>
<div>{{ content }}</div>
</div>
{{ end }}
{{ block button(text, href, type="primary") }}
<a href="{{ href }}" class="btn btn-{{ type }}">{{ text }}</a>
{{ end }}
Использование:
{{ yield card(title="Быстрый старт", content="Описание...") }}
{{ yield button(text="Попробовать", href="/signup") }}
{{ yield button(text="Узнать больше", href="/docs", type="secondary") }}
Паттерн: условные блоки
Переопределяйте блоки для разных страниц:
{* В blocks.html — блок по умолчанию *}
{{ block sidebar() }}
<aside>Стандартный сайдбар</aside>
{{ end }}
{* В page.html — переопределение *}
{{ import "blocks" }}
{{ block sidebar() }}
<aside>Сайдбар для статей</aside>
{{ end }}
{{ yield main_layout() content }}
...
{{ end }}
Паттерн: include для фрагментов
Небольшие повторяющиеся фрагменты — через include:
_layouts/
└── my-theme/
├── blocks.html
├── page.html
└── partials/
├── social-links.html
└── newsletter-form.html
{* partials/social-links.html *}
<div class="social">
<a href="https://t.me/...">Telegram</a>
<a href="https://twitter.com/...">Twitter</a>
</div>
{* В любом шаблоне *}
{{ include "partials/social-links" }}
С передачей данных:
{{ include "partials/user-card" user }}
Организация стилей
Один CSS-файл на тему, подключается в blocks.html:
<link rel="stylesheet" href="{{ asset("style.css") }}">
Для сборки (Tailwind, PostCSS) — выходной файл:
<link rel="stylesheet" href="{{ asset("style.out.css") }}">
Чеклист перед созданием структуры
- Больше двух страниц с общей шапкой/подвалом?
- Есть повторяющиеся элементы (карточки, кнопки)?
- Планируются разные типы страниц (статьи, лендинги)?
Если да хотя бы на один — используйте структуру с blocks.html.
Если нет — один файл шаблона на страницу проще и понятнее.
Паттерн: sidebar из markdown-файла
Навигацию в сайдбаре удобно хранить в отдельном markdown-файле. Это позволяет редактировать меню без изменения шаблона.
Структура:
docs/
├── _sidebar.md — навигация для документации
├── onboarding.md
└── ...
thoughts/
├── _sidebar.md — навигация для статей
├── article-1.md
└── ...
Файл _sidebar.md:
---
title: "Sidebar"
---
### Раздел 1
- [Страница 1](/docs/page-1)
- [Страница 2](/docs/page-2)
### Раздел 2
- [Страница 3](/docs/page-3)
В шаблоне:
{{ sidebar := nvs.ByPath("/docs/_sidebar.md") }}
{{ if sidebar }}
{{ range i, section := sidebar.PartialRenderer().Sections(3) }}
<div>
<h3>{{ section.TitleHTML | unsafe }}</h3>
{{ section.ContentHTML | unsafe }}
</div>
{{ end }}
{{ else }}
<p>Создайте файл <code>docs/_sidebar.md</code> для навигации</p>
{{ end }}
Преимущества:
- Добавить страницу в меню — отредактировать markdown
- Порядок и группировка — в одном месте
- Подсказка при отсутствии файла — не нужно читать документацию
Подводные камни Jet: блоки с параметрами
Jet требует особого внимания при работе с блоками. Два правила, которые легко нарушить:
1. Параметры блока должны иметь значения по умолчанию
Если хотите вызывать блок с именованными параметрами — укажите дефолтные значения при объявлении:
{* Правильно — есть дефолты *}
{{ block cta_section(title="", subtitle="") }}
<h2>{{ title }}</h2>
<p>{{ subtitle }}</p>
{{ end }}
{{ yield cta_section(title="Заголовок", subtitle="Подзаголовок") }}
{* Неправильно — без дефолтов *}
{{ block cta_section(title, subtitle) }}
<h2>{{ title }}</h2>
<p>{{ subtitle }}</p>
{{ end }}
{{ yield cta_section(title="Заголовок", subtitle="Подзаголовок") }}
{* Результат: title и subtitle будут false *}
Без значений по умолчанию Jet не связывает именованные аргументы с параметрами. На выходе — false вместо текста.
2. content — зарезервированное слово
Нельзя использовать content как имя параметра. Jet выдаст ошибку парсинга:
unexpected keyword 'content' (expected closing parenthesis)
content — это механизм передачи вложенного HTML в блок. Работает в паре:
{* Определение блока — yield content выводит вложенное содержимое *}
{{ block link(href="") }}
<a href="{{ href }}">{{ yield content }}</a>
{{ end }}
{* Вызов — content после параметров открывает блок для вложенного содержимого *}
{{ yield link(href="https://example.com") content }}
Текст ссылки
{{ end }}
{* Результат *}
<a href="https://example.com">Текст ссылки</a>
Если нужен параметр для текста — используйте другое имя:
{{ block card(title="", body="") }}
<h3>{{ title }}</h3>
<p>{{ body }}</p>
{{ end }}
Как отлаживать
Если блок выводит false вместо значений:
- Проверьте, что у всех параметров есть
=""или другой дефолт - Проверьте, что имена параметров не зарезервированы (
content) - Убедитесь, что при вызове используете именованные параметры:
param="value"