Markdown для ИИ-агентов¶
Примечание
Вывод Markdown — новое в Grav 2.1.
Каждую маршрутизируемую страницу сайта на Grav 2.1 можно прочитать как Markdown. Добавьте .md к URL страницы или отправьте заголовок запроса Accept: text/markdown, и Grav ответит страницей в виде Markdown вместо HTML темы:
curl https://example.com/blog/my-post.md
curl -H 'Accept: text/markdown' https://example.com/blog/my-post
Фича существует для ИИ-агентов, помощников по программированию и любых других клиентов, которым удобнее читать текст, чем разбирать HTML-страницу с темой. Она включена по умолчанию и ничего не требует от темы.
Что содержит Markdown¶
Вывод — это отрендеренная страница, преобразованная обратно в Markdown, а не исходный файл .md на диске. Grav рендерит страницу через тему точно так же, как для браузера, сохраняет основную область контента результата и преобразует её. Поэтому список блога, магазин, страница товара или что угодно ещё, чей контент строит шаблон, читается так, как отображается, и ничего не нужно менять в теме или плагине, который его рендерит. Шорткоды, Twig в контенте, модули, а также разрешённые пути изображений и ссылок проходят через преобразование. Исходный файл отдал бы агенту неразвёрнутые шорткоды, относительные пути изображений, которые разрешаются только относительно папки страницы, и относительные ссылки, ведущие в никуда.
Документ состоит из трёх частей:
- Блок YAML с заголовком, каноническим URL, собственным
.mdURL страницы, языком, датой, описанием и таксономией. Описание берётся изmetadata.descriptionстраницы, если оно есть, иначе из её аннотации. - Тело: основная область контента отрендеренной страницы. Заголовок
# Titleдобавляется, только если тема его не вывела. - Раздел навигации со ссылками на родительскую, предыдущую и следующую страницы в списке родителя и на дочерние страницы, каждая по её
.mdURL. Агент может обойти весь сайт, ни разу не покидая Markdown.
Как определяется основная область¶
Grav берёт первое из того, что есть на отрендеренной странице: <main>, элемент с role="main", затем идентификаторы main, content, main-content, body-wrapper, body и start, затем единственный <article>. Если ничего из этого нет, берётся <body> без его собственных заголовка и подвала. Внутри области отбрасываются <nav>, <aside>, всё с ролью navigation, banner, search, dialog или complementary, а также всё скрытое. Quark, Quark 2 и Kahuna оборачивают контент в <main>, как и большинство тем; тема, которая этого не делает, может получить чистый результат, добавив <main> вокруг своей колонки контента.
Попутно приводятся в порядок две вещи. Карточка, представляющая собой одну ссылку вокруг изображения, заголовка и текста, не имеет формы Markdown, поэтому её блоки извлекаются, а ссылка остаётся после них с заголовком карточки. Элементы секционирования HTML5, которые конвертер не знает (header, section, figure и остальные), сохраняются как блоки, чтобы их текст не склеивался со следующей строкой.
---
title: 'My Post'
url: 'https://example.com/blog/my-post'
markdown: 'https://example.com/blog/my-post.md'
lang: en
date: '2026-09-10'
description: 'What the post is about'
taxonomy:
tag:
- grav
---
# My Post
The post content, converted from the rendered HTML.
---
## Navigation
- Parent: [Blog](https://example.com/blog.md)
- Previous: [An Older Post](https://example.com/blog/older-post.md)
- Next: [A Newer Post](https://example.com/blog/newer-post.md)
Формы, скрипты, стили, встроенный SVG и другая разметка без эквивалента в Markdown отбрасываются. Ссылки и изображения относительно корня превращаются в абсолютные URL, чтобы они работали после того, как документ покинет сайт. Таблицы и блоки кода в ограждениях с указанием языка переживают преобразование.
Домашняя страница — /index.md. Каждый веб-сервер считает сегмент пути, начинающийся с точки, скрытым файлом, поэтому /.md был бы отклонён до того, как его увидит Grav, а index.<ext> — это то, что генераторы статических сайтов используют для той же задачи. Grav 2.1 отвечает на /index.<ext> для любого формата страницы, включая /index.rss и /index.json, если только корневая страница действительно не называется index.
Шаблону никогда не нужно особым образом обрабатывать домашнюю страницу, чтобы построить такую ссылку. page.url() принимает формат вывода пятым аргументом и сам выполняет подстановку index:
{{ page.url(false, false, true, false, 'md') }} {# /blog/post.md или /index.md на домашней странице #}
{{ page.url(true, false, true, false, 'rss') }} {# https://example.com/index.rss на домашней странице #}
markdown_url(page) — тот же вызов с md и включённым хостом.
Заголовки ответа¶
| Заголовок | Значение |
|---|---|
Content-Type |
text/markdown; charset=utf-8 |
X-Markdown-Tokens |
Примерное количество токенов; тот же заголовок отправляет Markdown for Agents от Cloudflare |
Vary: Accept |
Отправляется на каждом URL без расширения, пока фича включена, включая HTML, чтобы общий кэш никогда не отдал агенту HTML, а браузеру — Markdown. URL .md — отдельный ресурс, и такая подсказка ему не нужна |
Link: <https://example.com/page.md>; rel="alternate"; type="text/markdown" |
Отправляется с HTML-версией страницы, чтобы клиент мог найти Markdown, не угадывая URL |
HTML-страницы также несут тег <link rel="alternate" type="text/markdown"> в <head>, когда тема использует основной партиал метаданных Grav.
Перенаправление, отвечающее на запрос .md, сохраняет расширение, поэтому URL раздела, который перенаправляет на свою первую дочернюю страницу, ведёт на Markdown этой дочерней страницы.
Конфигурация¶
Настройки находятся в pages.markdown_output файла system.yaml, а в админке — в разделе Конфигурация → Система → Контент → Вывод Markdown:
pages:
markdown_output:
enabled: true # Отвечать на URL `<route>.md` и запросы `Accept: text/markdown` в формате Markdown
frontmatter: true # Начинать с блока YAML
links: true # Заканчивать разделом навигации
max_links: 100 # Максимум дочерних страниц в этом разделе (0 — без ограничения)
absolute_urls: true # Превращать ссылки и изображения относительно корня в абсолютные URL
token_header: true # Отправлять заголовок `X-Markdown-Tokens`
Отключение enabled убирает .md из типов страниц, на которые отвечает Grav, согласование Accept, а также заголовки Link и Vary вместе.
Кэширование¶
Отрендеренная страница зависит от запроса: она может показывать состояние входа, корзину, одноразовое значение формы. Grav никогда не кэширует её как HTML и не кэширует её Markdown. Запрос .md стоит столько же, сколько HTML-запрос, плюс преобразование, то есть разбор DOM страницы. Контент страницы внутри неё кэшируется Grav как обычно.
Настройка веб-сервера¶
Маршрут страницы, оканчивающийся на .md, должен доходить до index.php. Конфигурации Apache и lighttpd, поставлявшиеся до 2.1, запрещали любой URL, оканчивающийся на .md, независимо от того, файл это или нет, поэтому на этих серверах правило нужно сузить до реальных файлов. Grav делает это за вас при обновлении, если в вашем .htaccess всё ещё стандартная строка; отредактированный вручную .htaccess остаётся без изменений, и его нужно поправить вручную:
# Блокировать весь прямой доступ к файлам .md (маршрут страницы, оканчивающийся на .md, не является файлом, поэтому вывод Markdown в Grav продолжает работать):
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule \.md$ error [F,NC]
Для lighttpd удалите ".md" из списка url.access-deny и запрещайте его только как реальный файл:
url.access-deny += ("~",".inc")
$PHYSICAL["existing-path"] =~ "(?i)\.md$" {
url.access-deny = ("")
}
Исходные файлы в user/pages в любом случае остаются заблокированными правилом user/, которое стоит перед ним. Конфигурации nginx, Caddy и IIS всегда блокировали файлы .md только в user/, system/ и vendor/, поэтому их менять не нужно.
Если после обновления URL .md на сайте Apache возвращает 403, причина в этом правиле.
llms.txt¶
Плагин Sitemap 5.3 добавляет индекс, дополняющий Markdown для каждой страницы: /llms.txt перечисляет каждую страницу из карты сайта как ссылку на Markdown с её описанием, сгруппированную по разделам, следуя соглашению llms.txt, которое ищут агенты. Необязательный /llms-full.txt объединяет Markdown всех страниц в один документ. Оба учитывают правила игнорирования карты сайта.
Собственные шаблоны Markdown¶
Документ Markdown рендерится Twig-шаблоном, как и любой другой формат страницы, поэтому тема может его перехватить. Grav поставляет default.md.twig, который представляет собой один вызов:
Тема предоставляет собственный default.md.twig или <template>.md.twig для одного типа страницы (blog.md.twig, item.md.twig) и собирает документ из частей:
| Помощник Twig | Возвращает |
|---|---|
markdown_output(page) |
Весь документ |
markdown_frontmatter(page) |
Блок YAML |
markdown_body(page) |
Основную область контента отрендеренной страницы с заголовком, добавленным, если тема его не вывела |
markdown_links(page) |
Раздел навигации |
markdown_url(page) |
Абсолютный .md URL страницы |
html|html_to_markdown |
Любой отрендеренный HTML, преобразованный в Markdown |
Каждый помощник по умолчанию использует текущую страницу, если вызван без неё. В отличие от других форматов, запрос .md никогда не откатывается к HTML-шаблону темы, если ни один шаблон Markdown не подошёл: клиенту, запросившему Markdown, нельзя отдавать HTML, поэтому резервным вариантом служит default.md.twig из ядра.
Модули никогда не получают собственный шаблон Markdown. Они продолжают рендериться через обычный HTML-шаблон модуля, а документ родителя преобразует этот вывод, поэтому modular/hero.html.twig темы решает, что прочитает агент, без необходимости в Markdown-двойнике.