エージェントのための Markdown 配信

エージェントがページを要求すると、返ってくるのは HTML です。そしてその大半を捨てます。ナビゲーション、フッター、クッキーバナー、インラインスクリプト、class 属性、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 で異なる本文を返しているのに何も伝えなければ、最初にキャッシュされたものが全員に配られます。エージェントが Accept: text/markdown を付けて /about を要求し、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. .md の URL は単体で機能するか
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、各ツールページ、といった具合です。ID やスラッグを含むパス(/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 を生成する