Markdown для ИИ-агентов: правильное content negotiation
Одна демо-страница Cloudflare сжалась с 12 345 HTML-токенов до 725 в Markdown. Отдавайте его ИИ-агентам через Accept: text/markdown, верный Content-Type и Vary.

Отдавать Markdown ИИ-агентам — это не новый протокол, а обычное content negotiation в HTTP. Агент шлёт Accept: text/markdown, ваш сервер отвечает Content-Type: text/markdown; charset=utf-8 и сообщает кэшам Vary: Accept. Вот и весь механизм. Медиатип text/markdown зарегистрирован ещё в RFC 7763, а правила — это обычный RFC 9110 (IETF). Никакого ИИ-специфичного стандарта не нужно.
Воспринимайте это как один слой в стеке готовности, а не отдельный трюк. Сначала бот должен дойти до страницы и прочитать её без запуска JavaScript. Markdown-представление делает это чтение чище. Оно отдаёт модели ваш текст вместо DOM, забитого навигацией, стилями и виджетами. Это естественный следующий шаг после того, как вы перечислите .md-файлы в llms.txt: там вы указываете на Markdown, здесь — реально его отдаёте.
Ключевые выводы
- Markdown для агентов — это стандартное content negotiation поверх зарегистрированного медиатипа, а не новый ИИ-протокол (IETF RFC 7763).
- На одной демо-странице Cloudflare HTML оценили в 12 345 токенов, а Markdown — в 725. Это пример для одной страницы, а не средняя по индустрии (Cloudflare).
- Три способа отдачи:
.md-варианты URL, согласование на том же URL в рантайме или экспорт на этапе сборки. Гибрид часто лучше любой одиночной стратегии (Vercel).Vary: Acceptсам по себе спасает не на каждом CDN — CloudFront и Cloudflare требуют явной настройки ключа кэша, иначе кэш «отравляется».- Поломки почти всегда одни и те же: HTML, помеченный как Markdown, отсутствие
Vary, отсутствиеcharsetи soft 404. aiSiteReady проверяет это и оценивает сайт от 0 до 100.
Почему ИИ-агенты предпочитают Markdown, а не HTML?
Это операционное предпочтение, а не правило. Ни один RFC не требует, чтобы агенты «предпочитали Markdown». Модели, пайплайны извлечения и инструменты браузинга в основном извлекают и обрабатывают текст, а не DOM-деревья. Поэтому и Cloudflare, и Vercel рекомендуют отдавать агентам Markdown, и часть ИИ-клиентов уже запрашивает его напрямую (Cloudflare, Vercel).
Самый явный выигрыш — плотность токенов. В собственном примере Cloudflare для одной документационной страницы HTML оценили в 12 345 токенов, а Markdown — в 725, примерно на 94% меньше (Cloudflare). Считайте это демонстрацией одной страницы, а не средней величиной. Анонсирующий пост Cloudflare приводит более скромную цифру: 16 180 HTML-токенов против 3 150 в Markdown, примерно на 80% меньше (Cloudflare).
Почему это важнее, чем просто трафик? Каждое меню, футер, cookie-баннер и клиентский виджет, который читает модель, — это контекст, потраченный не на то. RFC 7763 проводит линию чётко: HTML — формат публикации, Markdown — формат письма (IETF). Отдать агенту уже чистый текст проще и предсказуемее, чем заставлять его каждый раз восстанавливать основной контент из разметки.
Как работает content negotiation для text/markdown?
Семантика полностью стандартная. Accept — это заголовок запроса, в котором клиент перечисляет приемлемые медиатипы. Сервер выбирает представление и объявляет его через Content-Type. Веса q идут от 0 до 1: q=1 — максимальный приоритет, q=0 означает «неприемлемо», а отсутствие q по умолчанию равно 1 (IETF RFC 9110).
Две детали сбивают с толку. Во-первых, charset обязателен для text/markdown. RFC 7763 говорит это прямо, так что всегда шлите ; charset=utf-8. Во-вторых, Vary: Accept выполняет двойную работу. Он сообщает кэшам, что ответ можно переиспользовать только для запросов с тем же Accept, что фактически расширяет ключ кэша. Он же сигнализирует, что тело выбрано через negotiation (IETF RFC 9110). Вот тот же URL, отвечающий на три разных запроса.
GET /guide HTTP/1.1
Accept: text/markdown, text/html;q=0.8
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Vary: Accept
Cache-Control: public, s-maxage=600
# Guide
Page text...
GET /guide HTTP/1.1
Accept: application/json
HTTP/1.1 406 Not Acceptable
Content-Type: text/plain; charset=utf-8
Vary: Accept
No acceptable representation for this resource.
Когда сервер не может удовлетворить Accept и не хочет давать fallback, корректный статус — 406 Not Acceptable. Но RFC 9110 также разрешает серверу проигнорировать предпочтение и отдать дефолтное представление вместо 406. Так что это решение политики, а не автоматическое поведение (IETF RFC 9110). RFC 7763 определяет необязательный параметр variant для flavor Markdown, а RFC 7764 регистрирует конкретные flavor. Это лишь идентификатор, а не настоящий механизм совместимости, поэтому большинство публичных деплоев его не используют.
Какие есть три способа отдавать Markdown?
Практических паттернов три, и правильный зависит от того, насколько вы контролируете свой CDN. Отдельные .md-файлы — самый безопасный и кэшируемый вариант. Согласование на том же URL — самый чистый, но самый чувствительный к кэшу. Экспорт на этапе сборки — самый предсказуемый для статических сайтов. Vercel рекомендует поддерживать и .md-эндпоинты, и negotiation, потому что многие агенты вообще не шлют Accept: text/markdown (Vercel).
| Подход | Сложность | Кэширование | Лучше для | Когда выбирать |
|---|---|---|---|---|
.md-варианты URL | Низкая | Самое простое | Статический хостинг | Нужен быстрый и безопасный запуск |
| Согласование на том же URL | Средняя/высокая | Самое капризное | API-логики | Вы контролируете origin и CDN |
| Экспорт на этапе сборки | Средняя | Очень предсказуемое | Docs / SSG | Контент из единого источника |
.md-варианты URL дают каждой HTML-странице соседа, например /docs/intro и /docs/intro.md. Диагностика тривиальна, статический хостинг работает «из коробки», и вы никогда не зависите от Vary. Цена — дублирование URL, поэтому держите HTML каноническим, а Markdown рекламируйте как альтернативу. В Next.js это делает rewrite плюс Route Handler. Обработчики App Router используют стандартный Web API Request/Response (Next.js).
// app/api/md/[...path]/route.ts
export async function GET(req: Request, { params }) {
const { path } = await params;
const doc = await loadDocAsMarkdown(path.join('/'));
if (!doc) return new Response('Not found', { status: 404 });
return new Response(doc.body, {
status: 200,
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'Link': `</docs/${path.join('/')}>; rel="canonical"`,
},
});
}
Затем объявите альтернативу на HTML-странице: <link rel="alternate" type="text/markdown" href="/docs/intro.md" />.
Согласование на том же URL позволяет человеку и агенту обращаться по одному адресу, пока сервер выбирает формат. Архитектурно это самый чистый вариант: один ресурс, без дублей в навигации. Загвоздка — кэширование. Ответ обязан нести Vary: Accept, иначе первый пришедший вариант отравляет кэш для всех остальных. Обработчик на Express передаёт логику.
app.get('/guide', async (req, res) => {
res.vary('Accept');
const page = await loadPage('guide'); // { html, markdown }
if (!page) return res.status(404).type('text/plain').send('Not found');
const best = req.accepts(['text/markdown', 'text/html']);
if (best === 'text/markdown')
return res.type('text/markdown; charset=utf-8').send(page.markdown);
if (best === 'text/html')
return res.type('text/html; charset=utf-8').send(page.html);
return res.status(406).type('text/plain').send('No acceptable representation');
});
Экспорт на этапе сборки публикует и HTML, и Markdown из одного источника во время сборки. Нет рантайм-конвертера, кэш максимально предсказуем, а артефакты можно проверить до деплоя. Hugo считает это нативным паттерном через несколько output-форматов (Hugo).
[mediaTypes."text/markdown"]
suffixes = ["md"]
[outputFormats.MARKDOWN]
mediaType = "text/markdown"
isPlainText = true
permalinkable = true
[outputs]
page = ["HTML", "MARKDOWN"]
Почему Vary: Accept важен на CDN?
Контент, зависящий от Accept, безопасен только тогда, когда кэш реально различает варианты, а реальное поведение CDN расходится со спецификацией. Стандартный ответ — Vary: Accept. Но Fastly трактует его как вторичный ключ кэша (Fastly), CloudFront по умолчанию не кэширует по заголовкам запроса (AWS), а Cloudflare вообще не учитывает общий Vary при кэш-решениях — поэтому ему нужен кастомный ключ кэша или Worker, чтобы различать Markdown и HTML (Cloudflare). Так что фраза «я выставил Vary: Accept, значит всё безопасно» верна лишь отчасти.
Вот failure mode, который Vercel описывает прямо. Агент первым запрашивает URL с Accept: text/markdown, origin честно возвращает Markdown, CDN кэширует этот объект как обычный /guide, и следующий браузер получает Markdown вместо страницы (Vercel). На CloudFront это поведение по умолчанию, потому что без cache policy он вообще не варьирует кэш по заголовкам.
Когда я строил проверку Markdown в aiSiteReady, именно на эту ловушку я натыкался чаще всего. На дефолтной политике CloudFront Markdown-ответ первого агента кэшировался как обычный /guide и затем отдавался каждому браузеру следом. Код origin был верным; багом был ключ кэша.
Исправления зависят от CDN. На CloudFront добавьте Accept в ParametersInCacheKeyAndForwardedToOrigin вашей cache policy. Учтите, что response-headers policy не чинит ключ кэша: она лишь меняет заголовки, которые CloudFront возвращает клиентам, и применяется к любому ответу — из кэша он или из origin (AWS). На Fastly сначала нормализуйте Accept до двух бакетов, потому что перестановок Accept огромное количество и они могут просадить hit ratio (Fastly). На Cloudflare добавьте кастомный ключ кэша или используйте managed-функцию Markdown-for-Agents. Правило простое: если не уверены в своём CDN, отдавайте .md-соседей.
Какие ошибки ломают Markdown для агентов?
Почти любой сломанный деплой попадает в один и тот же короткий список. Каждая ошибка прямо вытекает из правил Content-Type / Accept / Vary или из того, как поисковики трактуют статус-коды.
| Ошибка | Как обнаружить | Как исправить |
|---|---|---|
HTML-тело, помеченное как text/markdown | curl -i показывает заголовок, но тело начинается с <!doctype html> | Верните настоящий Markdown или исправьте Content-Type |
Negotiation без Vary: Accept | Сравните два значения Accept через CDN | Добавьте Vary: Accept к обоим вариантам ответа |
Отсутствует charset | Проверка заголовков | Отдавайте Content-Type: text/markdown; charset=utf-8 |
Отсутствующий .md возвращает 200 | curl -i /page.md, Search Console | Верните 404 или 410, а не «красивую» 200-страницу |
| Неверный статус для неподдерживаемых типов | Проверьте Accept: application/json | Возвращайте 406, когда не делаете fallback |
| Canonical только на HTML | Проверьте заголовки на .md | Объявите .md альтернативой; добавьте canonical через заголовок Link |
Строка про soft 404 важнее всего для SEO. Google определяет soft 404 как страницу, контент которой говорит «не найдено», тогда как сервер всё равно отвечает статусом 2xx вроде 200 OK (Google). Если .md-ресурса больше нет, возвращайте 404 или 410. Если же сломался сам слой negotiation, это 5xx — никогда не 200 с заглушкой. А если ваши факты появляются только после запуска клиентского JavaScript, ни один статический экспорт Markdown их не захватит.
Как проверить свою настройку Markdown?
Проверяйте сразу две плоскости: заголовки negotiation и само тело. Несколько вызовов curl подтвердят статус, Content-Type, наличие Vary: Accept и то, что тело соответствует заявленному типу. Запускайте их по публичному edge-URL, а не только по localhost — большинство багов negotiation живёт в кэше, а не в коде origin.
# только заголовки
curl -sSI -H 'Accept: text/markdown' https://example.com/guide
# полное тело — должно быть Markdown, а не <html>
curl -sS -H 'Accept: text/markdown' https://example.com/guide | head -20
# негативный тест: неподдерживаемый тип должен дать 406 (или fallback по политике)
curl -sSI -H 'Accept: application/json' https://example.com/guide
Встройте это в CI как smoke-тест, чтобы регрессия не уехала в прод. Проверка маленькая и ловит частые поломки до продакшена.
#!/usr/bin/env bash
set -euo pipefail
URL="${1:?usage: check-md.sh https://example.com/page}"
HEADERS="$(curl -sSI -H 'Accept: text/markdown' "$URL")"
BODY="$(curl -sS -H 'Accept: text/markdown' "$URL")"
grep -qi '^content-type: text/markdown;.*charset=utf-8' <<<"$HEADERS" || { echo 'FAIL: Content-Type'; exit 1; }
grep -qi '^vary: .*accept' <<<"$HEADERS" || { echo 'FAIL: missing Vary'; exit 1; }
grep -qi '<html\|<!doctype' <<<"$BODY" && { echo 'FAIL: body is HTML'; exit 1; }
echo OK
Одна честная оговорка, прежде чем вкладываться по-крупному: поддержка Accept: text/markdown у агентов всё ещё неравномерна. Многие агенты не шлют этот заголовок, и именно поэтому гибрид (.md-файлы плюс negotiation) надёжнее ставки на один путь (Vercel). Держите агентский Markdown консервативным, потому что flavor-расширения стандартизованы не одинаково у разных потребителей.
Как проверить весь сайт сразу?
Проверить один маршрут вручную — нормально. Проверять каждый шаблон на каждом релизе, по Markdown-negotiation и по слоям под ним, — нет. Эту работу делает aiSiteReady. Он загружает ваш сайт так, как это сделал бы агент, и запускает тест Markdown-negotiation внутри блока доступности контента, возвращая приоритизированные исправления и воспроизводимое HTTP-доказательство — как часть оценки от 0 до 100.
Это стоит рядом с проверками обнаружимости и протоколов, поверх шлюза доступности контента, который решает, может ли агент вообще прочитать вашу страницу. Точные проверки и веса описаны на странице методологии. Этот гайд — отдающая половина к карте обнаружения, которую вы публикуете через llms.txt. Обе — спицы под материалом о том, что значит готовность к ИИ-агентам.
Запустите бесплатное сканирование, чтобы увидеть, могут ли ChatGPT, Claude, Perplexity и Google AI получить чистую Markdown-версию ваших страниц: честен ли ваш Content-Type, присутствует ли Vary: Accept и какие исправления сделать первыми — на английском, украинском или русском.
Если коротко: чтобы кормить агентов чистым текстом, новый стандарт не нужен. Нужны верный Content-Type, честный Vary и кэш, который различает HTML и Markdown.
У IMozz 20 лет в разработке ПО, а последний год он строит продукты с помощью LLM. Он развивает aiSiteReady, сканер только для чтения, который проверяет, может ли ИИ-агент прочитать сайт. Сканер серверно рендерит собственный контент как рабочий пример.