人間にHTML、エージェントにMarkdown
2026年5月7日 公開
このサイトは、自分のための文章置き場でありつつ、Coding Agentにも読みやすい場所にしておきたいと思っています。
人間が読むページとしては、HTMLで読み心地よく表示されていれば十分です。しかし、Agentが同じページを読むときには、navigation、stylesheet、JavaScript、footerなどは不要です。
このサイトはNext.jsで構築しVercelにdeployしているので、Vercelが何かいい方法を考えているだろうと調べたところ、Making agent-friendly pages with content negotiationと、Knowledge BaseのHow to serve documentation for agentsを見つけました。
これがまさしくだったのでやってみました。
試しに、ターミナルを開いて次のように入力すると、このページのMarkdownが表示されます。
同じURLで、HTMLとMarkdownを出し分ける
Vercelの記事では、Agentが次のようなAccept headerを送る前提で説明されています。
text/markdownを先に書くことで、Markdownがあればそれを優先してほしい、という意思表示になります。
このとき、/writing/foo.mdのような別URLをAgentに覚えてもらうのではなく、ブラウザと同じ/writing/fooにrequestしてもらい、headerに応じてHTMLかMarkdownを返します。これがcontent negotiationです。
この考え方がよいのは、Agentがサイト固有のURL規則を知らなくてもよいところです。人間が見るURLと、Agentが読むURLが同じなので、canonicalなresourceは一つのままにできます。
page.tsxでheadersを読まない
Vercelの記事を読む前に自分で最初に考えたのは、Next.jsのpage.tsxでrequest headerを見て、Accept: text/markdownならMarkdownを返す方法でした。
ただ、思いついたと同時にこれは良くないアプローチだと思いました。
page.tsxでheaders()を読むと、そのrouteはrequest-timeの情報に依存します。今回の/writing/[slug]はgenerateStaticParams()で静的に生成できているので、HTMLページはそのままSSGにしておきたいです。Agent向け対応のために、人間向けのHTML配信をdynamic寄りにするのは少しもったいない。
Vercelの記事で紹介されていた実装も、page.tsxで分岐するものではありませんでした。
next.config.tsのrewriteでAccept headerを見て、Markdown用のRoute Handlerへ内部的に流します。
ブラウザから普通にアクセスした場合は、これまで通り/writing/[slug]/page.tsxがHTMLを返します。AgentがAccept: text/markdownを送ってきた場合だけ、内部的に/writing/md/[slug]へrewriteされます。
rewriteを上手に使えている気がして嬉しいですね。
これでHTML pageはHTML pageとして静的に保ち、Markdown responseはMarkdown responseとしてRoute Handlerに閉じ込める。実装の責務がはっきりします。
Markdown用のRoute Handlerを作る
このサイトの記事は、もともとapps/web/writing/<slug>/ja.mdやen.mdとしてMarkdownで書いています。
そのため、CMSのrich textをMarkdownへ変換する必要はありません。既存のMarkdown本文を読み、Agentが扱いやすいようにfrontmatterを付け直して返すだけです。
response headerには、少なくともContent-Typeを付けます。
Vary: Acceptも大事です。
同じURLでも、Accept headerによってHTMLとMarkdownのどちらが返るかが変わります。そのことをcacheに伝えないと、共有cacheがMarkdown版をブラウザに返したり、逆にHTML版をAgentに返したりする可能性があります。
sitemap.mdを置く
VercelのKnowledge Baseでは、Agentが次にどこを読めばよいか分かるようにsitemap.mdを用意することも紹介されています。
このサイトでも、記事一覧をMarkdownで返すrouteを追加しました。
これは人間向けのsitemapというより、Agent向けの探索入口です。目的の記事だけでは足りなかったときに、関連する文章へ進めます。
sitemap.mdのRoute Handlerはrequestに依存しないので、明示的に静的化しました。
Next.jsのRoute Handlerは、通常のpageとは少し違って、デフォルトではrequest時に実行されるものとして扱われます。ただし、GETでrequest依存の情報を読まない場合は、force-staticによって静的にできます。
今回のsitemap.mdはローカルのMarkdown metadataから決まるだけなので、静的で十分です。
build結果で確認する
実装後、bun --filter web buildで確認しました。
記事のHTML pageはこれまで通りSSGです。
Markdown用のRoute HandlerもgenerateStaticParams()によってSSGになりました。
sitemap.mdも、force-staticを付けたことで静的になっています。
また、実際にcurlでも確認しました。
このrequestではMarkdownが返ります。一方で、通常のブラウザアクセスではHTMLが返ります。
Agent-friendlyにするために、人間向けのpageを犠牲にしなくてよい。この形にできたのは、かなりよかったと思います。
AgentにやさしいWebは、特別なWebではない
今回やったことは、それほど大きな実装ではありません。
Accept headerを見るrewriteを足し、Markdownを返すRoute Handlerを用意し、探索用のsitemap.mdを置く。それだけです。
ただ、考え方としては面白いです。
人間にはHTMLを返す。AgentにはMarkdownを返す。どちらも同じresourceを見ているけれど、受け取りやすい表現は違う。その違いをHTTPのcontent negotiationという昔からある仕組みに載せる。
新しいAgentのために、必ずしも特別なAPIを増やす必要はありません。既存のWebの作法の中にも、まだ使える余地があります。
自分のサイトやドキュメントがすでにMarkdownで書かれているなら、これはかなり小さく始められる対応だと思います。
まずは一つの記事で、curl -H "Accept: text/markdown"にきれいな本文が返るようにしてみる。それだけでも、Agentから見たサイトの輪郭は少し読みやすくなるはずです。