Markdown для агентов: Accept: text/markdown
Агент запрашивает вашу страницу и получает HTML. Затем выбрасывает почти всё: навигацию, подвал, баннер о куках, встроенные скрипты, атрибуты классов, иконки SVG. Остаётся несколько абзацев текста.
Та версия, которая ему была нужна, у вас уже есть. Просто почти никто её не отдаёт.
Во что обходится разметка
Сравните то, что вы отправляете, с тем, что выживает:
curl -s https://example.com/about | wc -c
curl -s -H "Accept: text/markdown" https://example.com/about | wc -c
Если числа совпали, значит вы отдаёте одно представление всем подряд, а вычищать приходится самому агенту. Эта работа не бесплатна. Всё содержимое <head>, каждый обёрточный <div>, каждый атрибут data- и каждый фрагмент аналитики попадают в контекстное окно раньше, чем что-либо решит, что это был шум. На странице, реальное содержимое которой — заголовок и восемь абзацев, большая часть байтов приходится на леса.
Двойник в Markdown сохраняет уровни заголовков, ссылки, списки, таблицы и блоки кода. Он теряет навигацию, оформление и скрипты, из которых всё равно ничего никогда не процитировали бы.
Как работает согласование через Accept
Клиент сообщает, что он умеет читать:
GET /about HTTP/1.1
Host: example.com
Accept: text/markdown
Сервер выбирает представление для того же самого URL:
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
Vary: Accept
Браузер отправляет Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 и получает HTML. Клиент, запросивший text/markdown, получает Markdown. Канонический URL не меняется, поэтому по-прежнему есть один адрес для ссылки и одна страница, которой можно поделиться.
Vary: Accept, или как браузер скачивает текстовый файл
Vary не опция, и именно эту часть обычно пропускают.
Любой кеш между вами и читателем — ваш CDN, обратный прокси, хранилище самого браузера — индексирует ответы по URL. Если вы отдаёте по одному URL два разных тела и не сообщаете об этом, первый закешированный ответ достанется всем. Агент запрашивает /about с Accept: text/markdown, ваш CDN сохраняет Markdown, и следующий человек, открывший /about, видит страницу из звёздочек или окно загрузки файла.
Vary: Accept
Эта строка велит кешу включить заголовок Accept в ключ, и тогда HTML и Markdown хранятся раздельно. Отправляйте её в обоих ответах, а не только в том, что с Markdown.
Плата вполне реальна: заголовки Accept сильно различаются между клиентами, поэтому варьирование по ним дробит кеш. Это честный довод в пользу следующего варианта.
Двойник .md
Более простой путь — второй URL. /about отдаёт HTML, /about.md отдаёт то же содержимое в Markdown. Никакого согласования, никакого Vary, нечему ломаться в кеше. Это обычный URL, значит его можно поставить ссылкой, положить в закладки, включить в карту сайта и скачать любым клиентом, в том числе таким, который вообще не присылает пригодного заголовка Accept.
А потом сообщите, что он существует. В HTML:
<link rel="alternate" type="text/markdown" href="https://example.com/about.md">
И в заголовках ответа — для тех, кто делает HEAD и никогда не разбирает тело:
Link: <https://example.com/about.md>; rel="alternate"; type="text/markdown"
Сделать оба варианта стоит по одной строке и покрывает клиентов, которые смотрят только в одно место.
Проверьте свой сайт
# 1. Отвечает ли один и тот же URL по-разному?
curl -s -H "Accept: text/markdown" https://example.com/about | head -20
# 2. Признаёт ли HTML-ответ, что двойник существует?
curl -sI https://example.com/about | grep -i "vary\|link"
# 3. Работает ли URL с .md сам по себе?
curl -sI https://example.com/about.md | grep -i "content-type\|vary"
Вторая команда ловит самую распространённую поломку. У сайта может быть безупречный двойник .md, которого никто никогда не найдёт, потому что на него не указывают ни заголовок, ни тег.
Что делаем мы
У каждой страницы этого сайта есть двойник в Markdown, и весь механизм умещается в packages/web/src/lib/content-negotiation.js — около девяноста строк.
Фиксированная таблица перечисляет статические пути и их двойников: / → /index.md, /about → /about.md, каждая страница инструмента и так далее. Пути с идентификатором или слагом — отчёты вида /r/<uuid>, руководства, записи блога вроде этой — определяются по шаблону и получают <путь>.md.
Проверка запроса намеренно грубая: если заголовок Accept содержит строку text/markdown, отдаём Markdown. Никакого ранжирования по значениям q. Браузер эту строку не присылает никогда, так что на практике случаи разделяются чисто, но клиент, указавший text/markdown с низким приоритетом, тоже получит Markdown.
Каждый ответ в Markdown уходит с такими заголовками:
Content-Type: text/markdown; charset=utf-8
Content-Signal: search=yes, ai-input=yes, ai-train=no
Vary: Accept
X-Markdown-Tokens: 4213
Cache-Control: public, max-age=3600
Content-Signal говорит, что можно делать с содержимым, а не только можно ли его скачать. X-Markdown-Tokens — метка размера: сегодня в ней число символов тела, а не настоящий подсчёт токенов.
HTML-ответы страницы, у которой есть двойник, получают Vary: Accept и заголовок Link, а в <head> попадает тег rel="alternate" из той же таблицы.
Эта последняя деталь нас укусила. Шаблон брал данные из таблицы, но маршруты, определяемые по шаблону, в таблицу не входили. Поэтому каждый отчёт об анализе отдавал свой двойник в Markdown и объявлял его в заголовке Link, а его HTML не говорил об этом ни слова. Двойник существовал и оставался невидимым для всего, что читает разметку.
Это соглашение, а не стандарт
Accept: text/markdown — обычное согласование содержимого HTTP, и вот оно как раз стандарт. Использовать его для Markdown — соглашение, выросшее из предложения llms.txt, и ни один краулер не обязан о нём спрашивать. Честная позиция такая: надёжно работает именно двойник .md, потому что это URL как любой другой и скачать его может что угодно, а согласование стоит одного заголовка и окупается на тех клиентах, которые спрашивают. Ни то ни другое вам не повредит, а узнать, что из этого используется, можно по логам, а не по объявлениям.
Проверьте своё согласование Markdown · Сгенерируйте свой llms.txt