Перейти до основного вмісту
БЛОГ

Markdown для ШІ-агентів: правильний content negotiation

Одна демо-сторінка Cloudflare стиснулася з 12 345 HTML-токенів до 725 у Markdown. Віддавайте його ШІ-агентам через Accept: text/markdown, правильний Content-Type і Vary.

Автор: IMozzОновлено 2026-06-07
Serve Markdown to AI agents — aiSiteReady

Віддавати 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).

Порівняння поряд: картка HTML-сторінки, де nav, сайдбар, cookie-банер, скрипти й футер виділені як шум і позначені 12 345 токенами, поряд компактна картка Markdown лише із заголовками й текстом, позначена 725 токенами, і значок скорочення на 94% між ними.

Чому це важливіше за просто трафік? Кожне меню, футер, 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).

Схема content negotiation. Запит клієнта з Accept: text/markdown приходить до розгалуження — чи потрібен Markdown і чи є він у ресурсу? Якщо так, сервер повертає 200 з Content-Type text/markdown; charset=utf-8 і Vary: Accept. Інакше за замовчуванням 200 text/html з Vary: Accept. Якщо прийнятного подання немає і сервер не робить fallback, він повертає 406 Not Acceptable з Vary: Accept.

Дві деталі збивають з пантелику. По-перше, 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 був правильним; багом був ключ кешу.

Послідовність отруєння кешу в чотири кроки: ШІ-агент запитує /guide з Accept text/markdown, origin коректно повертає Markdown, CDN без Vary кешує це як звичайний обʼєкт /guide, і потім браузер отримує сирий Markdown замість HTML — позначено як хибний результат.

Виправлення залежать від 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/markdowncurl -i показує заголовок, але тіло починається з <!doctype html>Поверніть справжній Markdown або виправте Content-Type
Negotiation без Vary: AcceptПорівняйте два значення Accept через CDNДодайте Vary: Accept до обох варіантів відповіді
Відсутній charsetПеревірка заголовківВіддавайте Content-Type: text/markdown; charset=utf-8
Відсутній .md повертає 200curl -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, сканер лише для читання, що перевіряє, чи може ШІ-агент прочитати сайт. Сканер серверно рендерить власний контент як робочий приклад.

Часті запитання

Чи потрібен спеціальний протокол, щоб віддавати Markdown ШІ-агентам?
Ні. Це звичайне content negotiation у HTTP: агент шле Accept: text/markdown, а ви відповідаєте Content-Type: text/markdown; charset=utf-8 плюс Vary: Accept. Сам медіатип зареєстровано в RFC 7763, а правила узгодження — це стандартний RFC 9110. Жодного нового ШІ-специфічного стандарту немає; ви просто використовуєте HTTP так, як він задуманий.
Що обрати: .md-URL чи узгодження на тому самому URL?
Часто обидва одразу. Vercel рекомендує підтримувати і .md-ендпоінти, і negotiation разом, бо багато агентів узагалі не шлють Accept: text/markdown. Окремі .md-файли — найбезпечніший і кешований запуск, і вони працюють на статиці «з коробки». Узгодження на тому самому URL чистіше архітектурно, але лише якщо ваш CDN реально враховує Accept у ключі кешу.
Чи достатньо Vary: Accept, щоб CDN був безпечним?
Сам по собі — ні. Fastly трактує Vary як вторинний ключ кешу, але CloudFront за замовчуванням ігнорує заголовки запиту, доки ви не додасте Accept у cache policy, а Cloudflare не враховує загальний Vary без кастомного ключа кешу чи Worker. Завжди перевіряйте публічний edge-URL, а не лише origin, перш ніж довіряти конфігурації.
Чи не зашкодить віддача Markdown моєму SEO?
Не зашкодить, якщо HTML залишається канонічною версією. Оголосіть Markdown-копію альтернативним форматом тегом link rel=alternate type=text/markdown, а на самому .md-файлі віддайте rel=canonical через HTTP-заголовок Link — Google підтримує це для не-HTML документів. Уникайте soft 404: відсутній .md має повертати 404 або 410, а не 200 із заглушкою.