Русский
Frontmatter-патчи
У вас 200 заметок в папке blog/, и каждой нужно прописать free: true. Или вы хотите, чтобы все заметки в docs/ открывались на поддомене docs.mysite.com. Руками это делать бессмысленно, а через патчи вы пишете одно правило и оно применяется ко всей папке.
Frontmatter-патч работает так: вы указываете паттерн путей и выражение, которое добавляет или меняет свойства заметок. Система применяет патч при загрузке, до рендеринга страницы. Заметки остаются чистыми, в их frontmatter ничего не меняется.
Как настроена эта документация
Сайт с документацией trip2g, который вы сейчас читаете, управляется frontmatter-патчами из папки patches/. Каждый — обычная заметка в хранилище:
| Файл | Паттерн | Что делает |
|---|---|---|
| lang-en.md | **/*.md (приоритет −1) |
Язык по умолчанию — английский для всего сайта |
| free.md | **/*.md |
Все страницы публичны (free: true) |
| ru.md | ru/**/*.md |
Язык ru, шапка и подвал для русского раздела |
| en-user-sidebar.md | en/user/**/*.md |
Сайдбар для английских доков |
| ru-user-sidebar.md | ru/user/**/*.md |
Сайдбар для русских доков |
| en-thoughts-sidebar.md | en/thoughts/**/*.md |
Сайдбар английских эссе |
| ru-thoughts-sidebar.md | ru/thoughts/**/*.md |
Сайдбар русских эссе |
| dev-no-search.md | dev/**/*.md |
Dev-заметки скрыты из поиска |
| related.md | thoughts + user (приоритет 10) | Блоки «похожее», «обратные ссылки» и оглавление |
Обратите внимание на каскад языков: lang-en.md задаёт lang: "en" с приоритетом −1 для всего сайта. ru.md с приоритетом 0 перекрывает только нужное — ru/**. Ни одной заметке не нужно прописывать lang вручную.
Дефолтный шаблон читает свойства lang, left_sidebar, header и footer из заметки. Патчи подставляют эти свойства при загрузке, не трогая исходные файлы.
Кому подходит
- У вас раздел с бесплатным контентом и раздел с платным, а каждый раз прописывать
freeвручную надоело - Вы хотите назначить layout целой папке, а не каждой заметке отдельно
- Вам нужно привязать поддомен к разделу сайта через маршруты
Как создать патч
Создайте обычный markdown-файл в хранилище. Укажите во frontmatter type: frontmatter-patch и паттерны. Jsonnet-выражение пишется в теле файла в блоке кода с языком jsonnet.
Пример — сделать все заметки в blog/ бесплатными:
---
type: frontmatter-patch
include:
- blog/*
---
Все заметки в blog/ публичны.
```jsonnet
{ free: true }
```
Синхронизируйте хранилище — патч начнёт работать сразу.
Поля frontmatter
| Поле | Обязательно | По умолчанию | Описание |
|---|---|---|---|
type |
да | — | Должно быть "frontmatter-patch" |
include |
да | — | Glob-паттерны заметок, к которым применяется патч |
exclude |
нет | [] |
Glob-паттерны для исключения |
priority |
нет | 0 |
Меньше = применяется раньше (в пределах vault-патчей) |
Поля enabled нет. Чтобы отключить патч, удалите файл или переименуйте его.
Видимость файла патча
Файлы с префиксом _ — например _rules.md или _publish-rules.md — скрыты от посетителей сайта, но продолжают работать как патчи. Файлы без префикса отображаются как обычные заметки и одновременно работают как патчи. Это позволяет писать описание патча в свободной форме, ссылаться на него из других заметок и хранить как документацию.
Несколько патчей в одном файле
Каждый блок ```jsonnet в теле файла становится отдельным патчем. Все блоки наследуют include, exclude и priority из frontmatter:
---
type: frontmatter-patch
include:
- docs/**
priority: 5
---
```jsonnet
{ layout: "doc" }
```
```jsonnet
{ free: true }
```
Получится два патча, оба с priority: 5 и include: ["docs/**"].
Ошибки в файле патча
Если в файле патча есть проблемы, сайт продолжает работать. Заметка отображается с предупреждением — вы видите проблему и можете исправить её.
| Ситуация | Что происходит |
|---|---|
| Нет jsonnet-блока в теле | Предупреждение на заметке; файл рендерится как обычная заметка |
Отсутствует include |
Предупреждение на заметке |
| Невалидный glob-паттерн | Предупреждение на заметке |
| Невалидный Jsonnet-синтаксис | Предупреждение на заметке |
Предупреждение видно при открытии заметки на сайте. Остальные патчи продолжают работать.
Примеры
Назначить layout для раздела:
---
type: frontmatter-patch
include: ["blog/*"]
---
```jsonnet
{ layout: "blog_layout" }
```
Все заметки в blog/ будут использовать шаблон blog_layout, а остальные возьмут тот, что указан у них во frontmatter или в настройках сайта.
Привязать папку к поддомену:
---
type: frontmatter-patch
include: ["docs/**"]
---
```jsonnet
{ route: "docs.mysite.com" }
```
Все заметки в docs/ и вложенных папках автоматически окажутся на docs.mysite.com. Подробнее о маршрутах читайте в мультидоменах.
Добавить суффикс к заголовкам:
---
type: frontmatter-patch
include: ["*"]
---
```jsonnet
meta + { title: meta.title + " — My Site" }
```
Переменная meta содержит текущие свойства заметки. Выражение берёт существующий заголовок и добавляет к нему название сайта.
Условный патч — поставить layout, только если он не задан:
---
type: frontmatter-patch
include: ["*"]
---
```jsonnet
if std.objectHas(meta, "layout") then {} else { layout: "default" }
```
Если в заметке уже прописан layout, патч ничего не трогает. Если нет, ставит default.
Приоритеты и цепочки
Патчи применяются по порядку: сначала с меньшим приоритетом, потом с большим. Каждый следующий патч видит результат предыдущего.
Допустим, у вас два правила:
| Приоритет | Паттерн | Выражение |
|---|---|---|
| 0 | * |
{ layout: "default" } |
| 10 | blog/* |
{ layout: "blog_layout" } |
Для заметки blog/post.md сначала применится правило с приоритетом 0 и поставит layout: "default". Потом правило с приоритетом 10 перезапишет его на blog_layout. Итого заметка получит blog_layout, а все остальные заметки останутся с default.
Что важнее: патч или заметка
Важнее патч. trip2g берёт frontmatter заметки и применяет поверх него все подходящие патчи. Каждый ключ, который вернуло выражение, заменяет значение в заметке. Ключи, которых в выражении нет, остаются как были.
Заметка ru/user/intro.md с lang: en и патч для ru/**/*.md с { lang: "ru" } дают lang: "ru". Приоритет на это не влияет: он задаёт порядок патчей между собой, а frontmatter заметки всегда идёт первым.
Чтобы патч задавал значение по умолчанию, которое заметка может переопределить, сначала проверьте ключ:
if std.objectHas(meta, "left_sidebar") then {} else { left_sidebar: "ru/user/_sidebar.md" }
Теперь заметка со своим left_sidebar его сохранит. В meta есть и значения из предыдущих патчей, поэтому проверка пропустит ключ, если его уже задал патч с меньшим приоритетом.
Второй способ — исключить заметку из патча через exclude:
include: ["ru/user/**/*.md"]
exclude: ["ru/user/landing.md"]
Типичные сценарии
Одна боковая панель для всех страниц папки. Так сделано на этом сайте (patches/ru-user-sidebar.md):
---
type: frontmatter-patch
include: ["ru/user/**/*.md"]
---
```jsonnet
{ left_sidebar: "ru/user/_sidebar.md" }
```
ru/user/**/*.md захватывает и вложенные папки, но не ru/user.md рядом с папкой.
Отключить ссылки «Назад»/«Далее» и хлебные крошки для папки. Дефолтный шаблон строит их по боковой панели. В блоге они могут быть не нужны:
---
type: frontmatter-patch
include: ["blog/**/*.md"]
---
```jsonnet
{ prev: false, next: false, breadcrumbs: false }
```
Открыть все заметки, кроме одной папки:
---
type: frontmatter-patch
include: ["**/*.md"]
exclude: ["members/**/*.md"]
---
```jsonnet
{ free: true }
```
Язык по умолчанию и отдельный язык для раздела. Самый слабый патч задаёт язык для всех заметок, более сильный — для раздела ru/:
| Приоритет | Include | Выражение |
|---|---|---|
| −1 | **/*.md |
{ lang: "en" } |
| 0 | ru/**/*.md |
{ lang: "ru" } |
Оба патча перезаписывают lang, который заметка задала сама. Если в части заметок язык прописан вручную, добавьте проверку std.objectHas из предыдущего раздела.
Страницы на всю ширину для папки с досками:
---
type: frontmatter-patch
include: ["boards/**/*.md"]
---
```jsonnet
{ wide: true }
```
Убрать папку из поиска по сайту (patches/dev-no-search.md на этом сайте):
---
type: frontmatter-patch
include: ["dev/**/*.md"]
---
```jsonnet
{ search: false }
```
Паттерны
Паттерны определяют, к каким заметкам применится патч. Синтаксис похож на .gitignore, но с некоторыми особенностями.
Спецсимволы:
| Символ | Что делает |
|---|---|
* |
Любая последовательность символов, кроме /. Не матчит скрытые файлы (начинающиеся с .) |
** |
Любое количество вложенных папок, включая ноль |
? |
Ровно один символ, кроме / |
[abc] |
Один символ из набора: a, b или c |
[a-z] |
Один символ из диапазона |
[^abc] или [!abc] |
Любой символ, кроме указанных |
{foo,bar} |
Один из вариантов: foo или bar |
Примеры:
| Паттерн | Матчит | Не матчит |
|---|---|---|
blog/* |
blog/post.md, blog/draft.md |
blog/2024/post.md (вложенная папка) |
blog/** |
blog/post.md, blog/2024/jan/post.md |
docs/post.md |
blog/**/draft* |
blog/draft1.md, blog/2024/drafts/draft-new.md |
blog/final.md |
* |
index.md, about.md |
.hidden.md, blog/post.md |
*.md |
about.md, index.md |
.secret.md |
.* |
.hidden.md, .config.md |
about.md |
blog/post-?.md |
blog/post-1.md, blog/post-a.md |
blog/post-12.md |
{blog,docs}/* |
blog/post.md, docs/api.md |
notes/post.md |
blog/[0-9]* |
blog/2024-review.md |
blog/my-post.md |
Нюансы, которые стоит знать:
** работает только между разделителями пути. Паттерн blog/**/*.md поймает все .md-файлы на любой глубине внутри blog/. А вот blog/**.md сработает так же, как blog/*.md, то есть только на первом уровне. Если нужна рекурсия, пишите blog/**/*.md.
Скрытые файлы (начинающиеся с точки) не попадают под * и ?. Чтобы поймать их, используйте явный паттерн .* или конкретное имя .config.
Пустые альтернативы в фигурных скобках работают как опциональные части: some{thing,} матчит и something, и some.
Include и exclude:
У каждого патча есть include-паттерны (к чему применять) и exclude-паттерны (что исключить). Заметка попадает под патч, если подходит хотя бы под один include и не подходит ни под один exclude.
include: ["blog/**"]
exclude: ["blog/drafts/*"]
Этот патч применится ко всем заметкам в blog/, кроме тех, что лежат в blog/drafts/.
Выражения на Jsonnet
Под капотом выражения патчей написаны на Jsonnet — языке, который расширяет JSON переменными, условиями и функциями. Простые случаи выглядят как обычный JSON, а для сложной логики есть всё необходимое.
Доступные переменные:
| Переменная | Тип | Что содержит |
|---|---|---|
meta |
объект | Текущие свойства заметки (после предыдущих патчей в цепочке) |
path |
строка | Путь к файлу, например "blog/my-post.md" |
Выражение должно вернуть объект {}. Его содержимое добавляется к свойствам заметки. Если ключ уже существует, он перезаписывается.
Простой объект — самый частый случай. Выглядит как JSON:
{ free: true, layout: "blog" }
Чтение текущих свойств через переменную meta:
meta + { title: meta.title + " — My Site" }
Оператор + для объектов работает как merge: берёт все поля из meta и перезаписывает title новым значением.
Условия через if / then / else:
if std.objectHas(meta, "layout") then {} else { layout: "default" }
Здесь std.objectHas проверяет, есть ли у заметки поле layout. Если есть, патч возвращает пустой объект (ничего не меняет). Если нет, ставит default. Без проверки через std.objectHas обращение к meta.layout упадёт на заметке, где этого поля нет.
Логика на основе пути:
if std.startsWith(path, "premium/")
then { free: false }
else {}
Хотя это можно решить паттернами ["premium/*"], бывают случаи, когда условие удобнее выразить через путь прямо в выражении.
Конкатенация строк через +:
{ description: meta.title + " — опубликовано на " + meta.site_name }
Форматирование строк через % (как в Python):
{ og_title: "%s | %s" % [meta.title, "My Site"] }
Полезные функции стандартной библиотеки:
| Функция | Что делает | Пример |
|---|---|---|
std.objectHas(obj, key) |
Проверяет наличие поля | std.objectHas(meta, "layout") |
std.length(x) |
Длина строки или массива | std.length(meta.title) > 50 |
std.startsWith(str, prefix) |
Начинается ли строка с | std.startsWith(path, "blog/") |
std.endsWith(str, suffix) |
Заканчивается ли строка на | std.endsWith(path, ".draft.md") |
std.split(str, delim) |
Разбивает строку | std.split(path, "/") |
std.join(delim, arr) |
Склеивает массив | std.join(", ", meta.tags) |
Составной пример — premium-раздел с дефолтной сложностью:
meta + {
free: false,
reading_complexity:
if std.objectHas(meta, "reading_complexity")
then meta.reading_complexity
else "advanced"
}
Патч закрывает доступ к заметкам (free: false) и ставит reading_complexity: "advanced", но только если автор не указал сложность вручную.
Пустой объект {} — возвращайте его, когда патч не должен ничего менять. Это полезно в ветке then или else условия.
Патчи в админке
Патчи можно создавать и в административном интерфейсе — через раздел Заметки и контент. Каждый патч в админке состоит из трёх полей: паттерны путей, выражение на Jsonnet и приоритет.

Порядок применения. Патчи из админки применяются первыми, затем — vault-патчи из хранилища. Внутри каждой группы порядок определяется приоритетом: меньшее число — раньше. При одинаковом приоритете в vault-патчах применяется алфавитно-первый файл раньше, алфавитно-последний позже; в патчах из админки — сначала созданный ранее (по ID), потом созданный позже.
Vault-патчи всегда применяются после патчей из админки — даже если у vault-патча приоритет ниже. Это значит, что vault-патч может перекрыть любое правило из админки.
Что может пойти не так
Патч не применяется. Проверьте паттерн: blog/* не поймает blog/drafts/post.md, для вложенных папок нужен blog/**.
Ошибка в выражении. Если выражение падает на конкретной заметке (например, обращается к несуществующему полю), патч пропускается только для этой заметки. Остальные заметки обработаются нормально, сайт не сломается.
Два vault-патча конфликтуют. Побеждает тот, у кого выше приоритет (большее число). При одинаковом приоритете побеждает тот, чей путь алфавитно позже.
Vault-патч и патч из админки конфликтуют. Vault-патч всегда применяется последним и перекрывает патч из админки, независимо от приоритета.
Забыли про порядок. Патч с приоритетом 10 видит meta после всех патчей с приоритетом 0–9. Если вы добавляете суффикс к title в патче с высоким приоритетом, он возьмёт уже изменённый заголовок.
Обращение к несуществующему полю. meta.tags упадёт, если у заметки нет поля tags. Оборачивайте в проверку: if std.objectHas(meta, "tags") then meta.tags else [].
Живые примеры на этом сайте
Эта документация работает на собственных патчах. Каждый — настоящая заметка, её можно открыть:
- Опубликовать все заметки — помечает весь Markdown как free
- Язык по умолчанию — английский —
lang: enна весь сайт - Русский раздел — язык плюс общий header и footer под
ru/ - Сайдбар английских доков и Сайдбар русских доков
- Сайдбар английских эссе и Сайдбар русских эссе
- Скрыть dev-доки из поиска
- Блоки «похожее», «обратные ссылки» и оглавление
Вместе они заменяют frontmatter, который иначе пришлось бы повторять в сотнях заметок.