AI

AI エージェントに Markdown を返す — Accept ヘッダでコンテンツネゴシエーション

AI エージェントが Accept: text/markdown を送ってきたら Markdown を、ブラウザには従来どおり HTML を返す。同じ URL で表現を出し分けるコンテンツネゴシエーションの仕組みと、Vary やキャッシュで踏む落とし穴を運用者目線で整理します。

SoSoraEndo2026年8月27日 09:049 min2,036 字

動画で読む

同じ URL で HTML と Markdown を出し分ける

やることは一つです。エージェントが Accept: text/markdown を付けてきたら Markdown を返し、ブラウザには従来どおり HTML を返す。URL は変えません。HTTP のコンテンツネゴシエーション(同じ URL で相手の希望に応じて表現を切り替える仕組み)をそのまま使うだけです。

2026 年 8 月に Hacker News で話題になった acceptmarkdown.com は、この一点だけを提案しています。派手な新技術ではありません。Accept を見て Content-Type: text/markdown; charset=utf-8 を返し、キャッシュ用に Vary: Accept を添える。対応していないメディアタイプには 406 Not Acceptable を返す。仕様としてはこれで終わりです。

運用者として私が気になったのは「なぜ今か」と「入れたら何が壊れるか」の二つでした。仕組みが単純なぶん、事故は実装の細部に潜みます。この記事はその細部を運用目線で並べます。

なぜ今わざわざ Markdown を配るのか

理由は、LLM に HTML を読ませるのが単純に無駄だからです。エージェントがページを取りに来たとき、返ってくる HTML の大半はナビゲーション、スクリプト、スタイル、レイアウト用の div の入れ子で占められています。本文はその底に沈んでいます。

これはトークンの問題に直結します。1 ページ 8,000 トークンのうち本文が 2,000 トークンで、残り 6,000 がマークアップとメニューだった、というのはよくある比率です。RAG(Retrieval-Augmented Generation、検索した文書を文脈に足して回答させる手法)のパイプラインに流すなら、この 6,000 トークンは丸ごとノイズになります。広告や同意ダイアログの文言まで一緒に埋め込まれると、検索の精度も落ちます。

私自身、このサイト(AetherEchoes)で llms.txt を配っています。記事の要約と URL を機械が読みやすい 1 枚のテキストにまとめたものです。運用してみて分かったのは、llms.txt は「サイト全体の地図」には向くけれど、「個別ページの本文」を渡す用途には粒度が粗いということでした。地図と本文は役割が違います。Accept ヘッダによる出し分けは、後者、つまり個別ページの本文をきれいに渡す穴を埋めてくれます。

正直に言えば、私のサイトの HTML は広告こそ無いものの、セルフホストしたフォントが 7 種類あって、LLM に読ませるには十分うるさい部類です。装飾を削いだ Markdown を別に返せるなら、それに越したことはありません。

content negotiation の仕組み — Accept / Content-Type / Vary

コンテンツネゴシエーションは HTTP に昔からある機能です。クライアントが Accept で希望のメディアタイプを並べ、サーバがその中から一つ選んで返す。ブラウザは普段から次のようなヘッダを送っています。

Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8

末尾の q=0.9 は品質値(quality value、相手がどれだけそれを好むかの重み)です。エージェント側は Accept: text/markdown を明示的に送り、サーバはそれを見て分岐します。curl なら 1 行で試せます。

curl -H "Accept: text/markdown" https://example.com/posts/foo

サーバ側で大事なのは、レスポンスに Vary: Accept を必ず付けることです。

Content-Type: text/markdown; charset=utf-8
Vary: Accept

Vary は「このレスポンスは Accept ヘッダの内容で中身が変わる」とキャッシュに教える宣言です。これが無いと、CDN やブラウザのキャッシュが Markdown 版を HTML を求めた相手に返してしまう、あるいはその逆が起きます。対応していないタイプを強く要求された場合は 406 Not Acceptable を返すのが行儀の良い作法です。

実装で踏む 3 つの落とし穴

仕組みは単純でも、運用で踏む地雷はだいたい決まっています。私が実際に、あるいは他人の事故報告で見た順に 3 つ挙げます。

一つ目は Vary: Accept の付け忘れです。これを忘れると CDN のキャッシュが割れず、最初にアクセスした相手向けの表現が全員に配られます。エージェントに Markdown を返した直後、人間がアクセスして Markdown が生で表示される、という間抜けな事故はここから来ます。デプロイ直後は必ず 2 種類の Accept で叩いて、返ってくる Content-Type が切り替わるか確認してください。

二つ目は品質値(q 値)を無視した雑な判定です。Accept: text/markdown;q=0.1, text/html;q=0.9 のように、相手が「Markdown でもいいけど本当は HTML が欲しい」と言っているのに、単純な文字列一致で text/markdown を拾って Markdown を返してしまう。q 値まで見て、より高いほうを選ぶのが正しい振る舞いです。

三つ目は既存の CDN・キャッシュ設定との衝突です。多くの構成では配信を速くするために Vary を意図的に落としたり、キャッシュキーから特定ヘッダを除外したりしています。そこに Vary: Accept を足すと、キャッシュのヒット率が下がる、または設定によっては無効化されます。負荷とキャッシュ効率への影響を測ってから本番に入れるのが安全です。

運用者として、いつ・何を測って入れるか

先に結論を書くと、全ページに一律で入れる必要はありません。エージェントやクローラのアクセスが実際に多い技術ドキュメント・記事・API リファレンスから、費用対効果の高い順に入れるのが現実的です。

判断材料として私が見るのは、アクセスログの User-Agent と Accept ヘッダの分布です。そもそも Accept: text/markdown を送ってくる相手がどれだけいるのか。ゼロなら、まだ待っていい提案です。逆に AI クローラが本文を取りに来ている痕跡があるなら、HTML を丸ごと読ませて余計なトークンを食わせる前に、Markdown 版を用意する価値があります。

llms.txt を既に置いているなら、両者は排他ではありません。llms.txt がサイトの目次、Accept ヘッダによる出し分けが各記事の本文、と役割を分けて共存させられます。私はこのサイトを、AI が下書きを書き、私が公開前に確認して出す形で運用していますが、機械に優しい配信の整備もまた、書き手の側の準備の一部だと考えています。

新技術ではないぶん、入れるハードルは低いです。Nginx や Rails、Next.js のミドルウェア層で Accept を見て分岐を足すだけで済みます。ただし単純さを過信せず、Vary とキャッシュだけは丁寧に確認する。それがこの提案を安全に運用に乗せる唯一のコツです。

Tags

よくある質問

Accept ヘッダで Markdown を返すと URL は変わりますか?
変わりません。同じ URL のまま、リクエストの Accept ヘッダに応じて HTML か Markdown かをサーバが選んで返します。これがコンテンツネゴシエーションの利点で、リンクや被リンクをそのまま活かせます。
Vary: Accept を付け忘れると何が起きますか?
CDN やブラウザのキャッシュが表現を区別できず、最初のアクセスで生成された Markdown 版が HTML を求めた相手にも配られる、あるいはその逆が起きます。デプロイ直後に 2 種類の Accept で叩いて確認してください。
llms.txt があれば Accept ヘッダの出し分けは不要ですか?
役割が違うため共存できます。llms.txt はサイト全体の目次に向き、Accept ヘッダの出し分けは個別ページの本文をきれいに渡す用途に向きます。粒度が異なるので片方が他方を置き換えるものではありません。
全ページに導入すべきですか?
一律導入は不要です。AI クローラやエージェントのアクセスが多い技術ドキュメント・記事・API リファレンスから、アクセスログの Accept 分布を見て費用対効果の高い順に入れるのが現実的です。

参考文献

  1. Accept: text/markdown(acceptmarkdown.com)
  2. HTTP content negotiation — MDN Web Docs
  3. RFC 7231 — HTTP/1.1 Semantics and Content (Accept / Vary / 406)

Reaction

Share

X (Twitter)