Layouts API
Техническая документация для разработчиков шаблонов. Описывает API internal/templateviews — обёртки над моделями для использования в Jet-шаблонах.
Архитектура
Jet-шаблон
↓
templateviews (Note, NVS, Meta, NoteQuery)
↓
model.NoteView, model.NoteViews
↓
База данных
templateviews изолирует шаблоны от внутренних изменений модели. Шаблоны работают со стабильным API.
Note
Обёртка над model.NoteView. Представляет одну заметку в шаблоне.
Методы
| Метод | Возвращает | Описание |
|---|---|---|
Title() |
string |
Заголовок из frontmatter |
HTMLString() |
string |
Отрендеренный HTML контент |
ContentString() |
string |
Сырой markdown |
PathID() |
int64 |
ID для data-атрибутов |
Permalink() |
string |
URL страницы |
CreatedAt() |
time.Time |
Дата создания |
ReadingTime() |
int |
Время чтения в минутах |
ReadingComplexity() |
int |
Сложность (0-2) |
IsHomePage() |
bool |
Является ли домашней страницей подграфа |
Description() |
string |
SEO-описание |
PartialRenderer() |
NoteViewPartialRenderer |
Рендерер для разбивки контента |
TOC() |
NoteViewHeadings |
Оглавление |
M() |
*Meta |
Доступ к frontmatter |
Пример
<article>
<h1>{{ note.Title() }}</h1>
<time>{{ note.CreatedAt().Format("02.01.2006") }}</time>
<span>{{ note.ReadingTime() }} мин</span>
{{ note.HTMLString() }}
</article>
NVS (NoteViews)
Набор заметок сайта (NoteViews) в обёртке для шаблонов. Доступен в шаблоне как nvs.
Методы доступа
| Метод | Описание |
|---|---|
ByPath(path) |
Заметка по пути файла ("/_sidebar.md", "docs/intro.md") |
ByPermalink(url) |
Заметка по URL ("/docs", "/about") |
List() |
Все видимые заметки (без системных /_*) |
Методы для навигации
| Метод | Описание |
|---|---|
Sidebars(note) |
Сайдбары для заметки |
HomePages(note) |
Домашние страницы подграфов |
BackLinks(note) |
Обратные ссылки (кто ссылается на эту заметку) |
ResolveURL(note) |
Полный URL с версией |
Методы запросов
| Метод | Описание |
|---|---|
ByGlob(pattern) |
Query builder с glob-фильтром |
Query() |
Query builder без фильтра |
Примеры
{* Загрузить заметку по пути *}
{{ sidebar := nvs.ByPath("/docs/_sidebar.md") }}
{{ if sidebar }}
{{ sidebar.HTMLString() }}
{{ end }}
{* Загрузить по URL *}
{{ about := nvs.ByPermalink("/about") }}
{* Обратные ссылки *}
{{ range i, link := nvs.BackLinks(note) }}
<a href="{{ link.Permalink() }}">{{ link.Title() }}</a>
{{ end }}
NoteQuery
Ленивый query builder. Операции накапливаются и выполняются при вызове терминального метода.
Фильтрация
nvs.ByGlob("blog/*.md") {* Все .md в папке blog *}
nvs.ByGlob("docs/**/*.md") {* Рекурсивно все .md в docs *}
nvs.ByGlob("projects/**/README.md") {* Все README.md *}
nvs.Query() {* Все заметки без фильтра *}
Поддерживаемые glob-паттерны:
*— любые символы кроме/**— любая вложенность?— один символ
Сортировка
.SortBy("Title") {* По заголовку *}
.SortBy("CreatedAt") {* По дате создания *}
.SortBy("Permalink") {* По URL *}
.SortBy("created_at") {* snake_case тоже работает *}
.SortByMeta("order") {* По полю frontmatter *}
.SortByMeta("weight")
Направление
.Desc() {* Последний критерий — по убыванию *}
.Asc() {* Последний критерий — по возрастанию (по умолчанию) *}
Множественная сортировка
{* Сначала по категории, внутри — по заголовку *}
nvs.ByGlob("blog/*.md").SortByMeta("category").SortBy("Title")
Пагинация
.Limit(10) {* Первые 10 *}
.Offset(5) {* Пропустить 5 *}
.Offset(10).Limit(10) {* Вторая страница *}
Терминальные методы
| Метод | Возвращает | Описание |
|---|---|---|
All() |
[]*Note |
Все результаты |
First() |
*Note |
Первый результат или nil |
Last() |
*Note |
Последний результат или nil |
Полный пример
{* Последние 5 постов блога *}
{{ range i, post := nvs.ByGlob("blog/*.md").SortBy("CreatedAt").Desc().Limit(5).All() }}
<article>
<h2><a href="{{ post.Permalink() }}">{{ post.Title() }}</a></h2>
<time>{{ post.CreatedAt().Format("02.01.2006") }}</time>
</article>
{{ end }}
{* Документация с ручным порядком *}
{{ range i, doc := nvs.ByGlob("docs/*.md").SortByMeta("order").All() }}
<a href="{{ doc.Permalink() }}">{{ doc.Title() }}</a>
{{ end }}
{* Последний пост *}
{{ latest := nvs.ByGlob("blog/*.md").SortBy("CreatedAt").Desc().First() }}
{{ if latest }}
<a href="{{ latest.Permalink() }}">{{ latest.Title() }}</a>
{{ end }}
Meta
Типобезопасный доступ к frontmatter.
Методы
| Метод | Описание |
|---|---|
Has(key) |
Проверка наличия ключа |
Get(key) |
Сырое значение (interface{}) |
GetString(key, default) |
Строка или default |
GetInt(key, default) |
Число или default |
GetBool(key, default) |
Булево или default |
GetStringSlice(key) |
Массив строк или nil |
Приведение типов
GetBool понимает:
true,false(bool)"true","yes","1"(string → true)1,0(int → bool)
GetInt понимает:
int,int64,float64
Примеры
{* Проверка наличия *}
{{ if note.M().Has("featured") }}
<span class="badge">Featured</span>
{{ end }}
{* Получение значений *}
{{ author := note.M().GetString("author", "Anonymous") }}
{{ order := note.M().GetInt("order", 999) }}
{{ published := note.M().GetBool("published", false) }}
{* Теги *}
{{ range i, tag := note.M().GetStringSlice("tags") }}
<span class="tag">{{ tag }}</span>
{{ end }}
PartialRenderer
Разбивает markdown на логические блоки. Возвращается методом note.PartialRenderer().
Методы
| Метод | Описание |
|---|---|
Introduce() |
Контент до первого заголовка |
Sections(level) |
Секции под заголовками уровня level |
Section(title) |
Секция по тексту заголовка |
Структура секции
type Section struct {
TitleHTML string // Текст заголовка (без тега)
ContentHTML string // Контент до следующего заголовка
}
Примеры
{* Вступление *}
{{ intro := note.PartialRenderer().Introduce() }}
<div class="lead">{{ intro.ContentHTML }}</div>
{* FAQ из H3 *}
{{ range i, q := note.PartialRenderer().Sections(3) }}
<details>
<summary>{{ q.TitleHTML }}</summary>
<div>{{ q.ContentHTML }}</div>
</details>
{{ end }}
{* Конкретная секция *}
{{ faq := note.PartialRenderer().Section("FAQ") }}
{{ if faq }}
{{ faq.ContentHTML }}
{{ end }}
Автоимпорт компонентов
Как загрузчик подключает компоненты, которые страница вызывает через {{ yield }} без явного {{ import }} (internal/layoutloader/auto_import.go, buildAutoImportPreamble):
- Собирает все
{{ yield name(...) }}страницы. - Находит файл layout'а, где определён каждый блок, затем блоки, которые yield'ят эти файлы, и так далее по графу.
- Вставляет содержимое найденных файлов в начало страницы внутри
{{ if false }} … {{ end }}— после ведущих{{ extends }}и{{ import }}страницы (Jet требует сначалаextends, потом импорты). Jet при разборе регистрирует определения блоков, а выполнения и вывода нет: условие всегда ложно. Это обход того, что блок в Jet должен быть объявлен в том же шаблоне или импортирован.
Файл, который начинается с {{ extends }}, — это страница, а не компонент, и автоимпорт его никогда не подключает (buildBlockRegistry, internal/layoutloader/registry.go). Иначе база с голым {{ yield main() }} нашла бы main в странице-наследнике, вставила бы её целиком вместе с {{ extends }} в свою преамбулу, и Jet отказался бы её разбирать.
Анализ статический: читается исходник, поэтому подключаются все достижимые компоненты, даже за ложным if. Преамбула вставляется только в рендерящуюся страницу: шаблоны, до которых дошли через include, extends или exec, разбираются из своего исходника без неё, поэтому компоненты, которые они вызывают, они импортируют сами. Пользовательская сторона — ru/user/components / en/user/components; там эта механика намеренно не описана, чтобы авторы шаблонов не повторяли обёртку {{ if false }} руками.
Jet-синтаксис
Шаблонизатор на основе Go templates с расширениями.
Переменные
{{ x := "value" }} {* Объявление *}
{{ x = "new value" }} {* Присваивание *}
{{ x }} {* Вывод *}
Условия
{{ if condition }}
...
{{ else if other }}
...
{{ else }}
...
{{ end }}
Циклы
{* range возвращает индекс и значение *}
{{ range i, item := list }}
{{ i }}: {{ item }}
{{ end }}
{* Только значение — НЕПРАВИЛЬНО, item будет индексом! *}
{{ range item := list }} {* item = 0, 1, 2... *}
Блоки и наследование
{* blocks.html *}
{{ block header() }}
<header>Default header</header>
{{ end }}
{* page.html *}
{{ import "blocks" }}
{{ yield header() }} {* Вызов блока *}
Переопределение блоков
{* page.html *}
{{ import "blocks" }}
{{ block header() }}
<header>Custom header</header>
{{ end }}
{{ yield main_layout() content }}
...
{{ end }}
Фильтры
{{ value }} {* Экранируется по умолчанию *}
{{ value | unsafe }} {* Вывод строки без экранирования *}
Вывод экранируется. Методы, которые возвращают готовый HTML (HTMLString(), TitleHTML, ContentHTML, FirstListHTML(), FormSpecJSON(), asset()), имеют тип model.SafeHTML и выводятся как есть. | html экранирует один раз и тоже возвращает SafeHTML, поэтому старые шаблоны с | html не экранируют дважды.
Исходный код
internal/templateviews/
├── note.go # Note — обёртка заметки
├── nvs.go # NVS — обёртка над model.NoteViews для шаблонов
├── query.go # NoteQuery — query builder
├── meta.go # Meta — доступ к frontmatter
└── *_test.go # Тесты