同じサイト、同じ情報。データ形式を変えただけで──
132KB のリッチ HTML → 正答率 75%
2.4KB の Markdown → 正答率 100%
AIエージェントに「このカフェの営業時間教えて」と聞いたとき、渡すデータの形式で答えの正確さが変わる。 そんな実験をやってみました。
この記事は Part 1: ピカチュウ→ミュウのイースターエッグ記事 の続編です。Part 1 未読でも完結しますが、読むと冒頭のセルフツッコミが5倍楽しくなります。
まず、前回の記事に自分でツッコませてください
Part 1 では、User-Agent を見てブラウザには⚡ピカチュウ、AIには🔮ミュウを見せるサイトを作りました。面白い実験だったと思います。
……でもこれ、User-Agent で返すページを変えてるだけ。 Googlebot に別のページを見せて検索ランキングを操作する ── いわゆるSEOクローキングと同じです。15年以上前からある技術 であり、AIは関係ありません。
Part 1 でやったこと:
「外国人っぽいお客さんが来たから、勝手に英語メニュー出しちゃう」
↑ これ、大きなお世話ですよね
じゃあ、AIエージェント時代に 「本当に新しいこと」 って何なの?
今回の記事は、その『問い』への回答です。 3つのLLM-SEO手法を実装し、同じ5つの質問で正答率を検証しました。
レストランで考えるLLM-SEO
3つの手法を説明する前に、レストランのメニュー で例えさせてください。この比喩、最後まで使います。
| レストランの例え | 手法 | やること |
|---|---|---|
| 日本語メニューを頑張って読んでもらう | HTML(何もしない) | 132KB の装飾 HTML をそのまま返す |
| 「英語メニューください」→出す | Content Negotiation |
Accept: text/markdown → Markdown で返す |
| 入口に多言語メニューを常備 | llms.txt |
/llms.txt に情報を置いておく |
| 全料理の成分表をExcelで渡す | JSON API |
/api/info で構造化データを返す |
Part 1 は「外国人っぽいから英語メニューを"勝手に"出す」 ── これがクローキングです。
Part 2 は「お客さんが求めた形式で出す」 ── これがContent Negotiation。 同じ結果でも、プロセスが全然違う。 ここ、めちゃくちゃ大事です。
3つの手法 ── 何が「新しい」のか
手法 1: Content Negotiation(Accept: text/markdown)
2026年2月、Cloudflare が AI Audit をリリース。 Accept: text/markdown による Content Negotiation が産業レベルで現実のものに。この記事で最も「旬」な手法。
HTTPの正式な仕様(RFC 9110, Section 12)に基づくコンテンツネゴシエーション。クライアントが「Markdownが欲しい」と Accept ヘッダーで宣言→サーバーが Markdown で返す。
AIエージェント: 「すみません、text/markdown でお願いします」
サーバー: 「かしこまりました、Markdownをどうぞ」
これはクローキングではありません。 クライアントが明示的に形式を要求しているのです。
| Part 1(クローキング) | Part 2(Content Negotiation) | |
|---|---|---|
| 判定基準 | User-Agent(身元確認) | Accept ヘッダー(要求確認) |
| 例え | 外国人っぽいから英語メニュー出す | 「英語メニューください」→出す |
| Google の評価 | ペナルティ対象 | HTTP標準の正当な仕組み |
手法 2: llms.txt
robots.txt が「クロールしないで」なら、llms.txt は「これを読んで」。 サイトのルートに /llms.txt を置くだけ。
| robots.txt | llms.txt | |
|---|---|---|
| 対象 | 検索エンジンのクローラー | LLM / AIエージェント |
| 目的 | 「ここはクロールしないで」 | 「AIはこの情報を読んで」 |
| 内容 | Disallow / Allow ルール | サイトの要約(Markdown形式) |
| 歴史 | 1994年〜(30年の歴史) | 2025年〜 llmstxt.org が提唱 |
レストランで言えば、入口に置いてある多言語メニュー。 お客さんが「ありますか?」と聞く前に、手に取れる。
手法 3: Structured JSON API
HTMLパース完全不要。LLMが 最も効率的に データを取得できる形式。
人間: 「このHTMLの中から営業時間を探して…えーと…どこだ…」
AI: 「json.hours.weekday.open でしょ。9:00。はい次」
レストランで言えば、全料理の栄養成分・アレルゲン表をスプレッドシートでくれる店。 おしゃれさゼロ、情報量100%。
実装 ── 架空カフェ「Cafe AURA」
「Cafe AURA」 という架空のカフェサイトに3手法すべてを実装しました。ポイントは Single Source of Truth ── 全エンドポイントが同一のデータオブジェクトから生成されること。これで検証の公平性が担保されます。

↑ Cafe AURA のファーストビュー。Tailwind CSS で作った「普通のカフェサイト」。これが 132KB の HTML になる
データの心臓部 ── lib/cafe-data.ts
export const CAFE_DATA = {
name: "Cafe AURA",
tagline: "光と香りに包まれる、都会のオアシス",
hours: {
weekday: { open: "9:00", close: "21:00", label: "平日(月〜金)" },
weekend: { open: "10:00", close: "22:00", label: "土日祝" },
closed: "水曜定休",
note: "ラストオーダーは閉店30分前",
},
seating: { total: 42, indoor: 34, terrace: 8,
terraceNote: "ペット同伴OK・予約不可" },
wifi: { ssid: "CAFE-AURA-FREE", password: "aura2026",
speed: "下り約200Mbps", note: "1回3時間まで。再接続可" },
// ... メニュー・予約・アクセス・決済方法なども定義
} as const;
as const が地味に重要。全プロパティが リテラル型 になり、データ変更時にTypeScriptが全箇所で教えてくれます。
Content Negotiation ── middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { generateMarkdown } from "@/lib/cafe-data";
export function middleware(request: NextRequest) {
const accept = request.headers.get("accept") ?? "";
if (accept.includes("text/markdown")) {
return new NextResponse(generateMarkdown(), {
status: 200,
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Vary": "Accept", // ← これを忘れるとCDNが事故る
"Cache-Control": "public, max-age=3600",
},
});
}
return NextResponse.next();
}
export const config = { matcher: ["/"] };
Vary: Accept を忘れると事故ります。 CDNがHTMLをキャッシュ→次のMarkdownリクエストにHTMLを返す。Content Negotiation するなら Vary は必須。
llms.txt ── public/llms.txt
# Cafe AURA
> 光と香りに包まれる、都会のオアシス
## 営業時間
| 区分 | 時間 |
|------|------|
| 平日(月〜金) | 9:00〜21:00 |
| 土日祝 | 10:00〜22:00 |
| 定休日 | 水曜定休 |
## WiFi
- SSID: `CAFE-AURA-FREE`
- パスワード: `aura2026`
...
置くだけ。 public/ に Markdown ファイルを1つ置けば /llms.txt で配信される。ビルド不要、デプロイ不要。今日から5分で導入できる 手法です。
JSON API ── app/api/info/route.ts
import { NextResponse } from "next/server";
import { CAFE_DATA } from "@/lib/cafe-data";
export async function GET() {
return NextResponse.json(CAFE_DATA, {
headers: { "Cache-Control": "public, max-age=3600" },
});
}
3行。CAFE_DATA をそのまま JSON で返すだけ。レストランで言えば栄養成分表をExcelで渡すやつです。 おしゃれさはないけど、情報は完璧。
いざ検証 ── 4パターン × 5問
ここからがこの記事の クライマックス です。
実験条件
この検証は N=1 のデモンストレーションです。 LLM の出力は非決定的であり、同じ条件でも結果が変わる可能性があります。ただし「装飾 HTML は情報が埋もれやすい」という定性的な傾向は、この実験で明確に観察されました。
| 項目 | 条件 |
|---|---|
| モデル | Claude(Claude Code経由) |
| 入力 | 各エンドポイントの curl レスポンスをそのまま渡す |
| プロンプト | 「以下のデータを読んで、[質問]に回答してください」 |
| 判定 | 筆者がkey_facts各項目の有無を二値判定 |
5つの質問
| # | 質問 | 正解キーファクト |
|---|---|---|
| Q1 | 営業時間は? | 平日9-21 / 土日祝10-22 / 水曜定休 / LO30分前 |
| Q2 | 一番人気のメニューと価格は? | スペシャルティラテ / ¥650 / イルガチェフェ豆 |
| Q3 | 席数は?テラス席は? | 42席 / 店内34 / テラス8 / ペットOK / 予約不可 |
| Q4 | WiFiのパスワードは? | SSID / aura2026 / 200Mbps / 3時間制限 |
| Q5 | 予約方法は? | 電話 / Web / テラス不可 / 前日18時キャンセル無料 |
レスポンスサイズ ── まずこの差を見てほしい
$ curl -s http://localhost:3001/ | wc -c
132481
$ curl -s -H "Accept: text/markdown" http://localhost:3001/ | wc -c
2465
| パターン | サイズ | HTML比 |
|---|---|---|
| A: Baseline HTML | 132,481 bytes | 1x |
| B: Content Negotiation | 2,465 bytes | 1/54 |
| C: llms.txt | 2,465 bytes | 1/54 |
| D: JSON API | 3,283 bytes | 1/40 |
54倍。 同じ情報を伝えるのに、HTML は Markdown の 54倍のデータ量を使っている。トークン課金で考えると、これは 54倍のコスト です。
あなたのサイトも試してみてください。curl -s あなたのURL | wc -c ── その数字が、AIが読まされているデータ量です。

↑ これが 132,481 bytes の正体。人間には美しく見えるけど、AIにとっては装飾クラスの森
実際に Claude Code に聞いてみた
まずベースラインHTML(132KB)を渡して Q1 を聞いてみます。
> 以下のデータを読んで、Cafe AURAの営業時間を教えてください。
> [132KBのHTMLを貼り付け]
Claude Code:
「平日は9:00〜21:00、土日祝は10:00〜22:00です。水曜定休です」
おっ、正解。……いや待って、ラストオーダーの情報がない。
次に Content Negotiation(Markdown 2.4KB)を渡して、同じ質問。
Claude Code:
「平日9:00〜21:00、土日祝10:00〜22:00、水曜定休。ラストオーダーは閉店30分前です」
ラストオーダー、拾った。 同じ情報、同じ質問なのに。
これ、たまたまかな? と思って、5問全部やってみた結果が──
正答率
| 質問 | HTML | Content Nego | llms.txt | JSON API |
|---|---|---|---|---|
| Q1: 営業時間 | ✅ 4/4 | ✅ 4/4 | ✅ 4/4 | ✅ 4/4 |
| Q2: 人気メニュー | ✅ 3/3 | ✅ 3/3 | ✅ 3/3 | ✅ 3/3 |
| Q3: 席数 | ⚠️ 3/5 | ✅ 5/5 | ✅ 5/5 | ✅ 5/5 |
| Q4: WiFi | ⚠️ 2/4 | ✅ 4/4 | ✅ 4/4 | ✅ 4/4 |
| Q5: 予約方法 | ⚠️ 3/4 | ✅ 4/4 | ✅ 4/4 | ✅ 4/4 |
| キーファクト正答数 | 15/20 | 20/20 | 20/20 | 20/20 |
| 正答率 | 75% | 100% | 100% | 100% |
……マジか。
Markdown / llms.txt / JSON は全問パーフェクト。HTML だけが 75%。
「さすがに正答率は変わらないだろう、情報量は同じなんだから」と思ってたんです。
甘かった。
HTML で何が起きたのか ── 間違い方が面白い
75% って「まあまあ」に見えるじゃないですか?
でも 間違い方 を見て、ちょっとゾッとしました。LLM って、こんな落とし方をするのか、と。
Q3: 席数 ── 「ペット同伴OK」と「予約不可」を落とした
HTML ではテラス席の情報がこんな構造の奥に埋まっています:
<div class="bg-amber-50 rounded-xl p-4 text-sm text-amber-800">
<span class="font-medium">テラス席について:</span>
テラス席はペット同伴OK・予約不可
</div>
「42席(店内34 + テラス8)」は拾えた。でも ペット同伴OK と テラス予約不可 の2つをきれいに落としている。装飾クラスの中の、さらにネストされた補足テキスト。人間なら「注意書きだな」と直感で分かる。LLM にはそれがない。
Q4: WiFi ── これが一番「うわぁ……」となったケース
WiFi のパスワードは拾えた。SSIDも拾えた。速度も拾えた。
でも 「1回の接続で最大3時間」 だけ、きれいに落としてる。
なぜか。HTMLを見ると一目瞭然です:
<!-- ↓ メインカード。ここの情報は拾えた -->
<div class="bg-stone-900 rounded-xl p-5 text-center">
<div class="text-white font-mono text-lg font-bold">CAFE-AURA-FREE</div>
<div class="text-amber-400 font-mono text-lg font-bold">aura2026</div>
</div>
<!-- ↓ カードの「外」の小さなテキスト。ここを落とした -->
<div class="flex justify-between text-sm text-stone-500 px-1">
<span>下り約 200Mbps</span>
<span>1回の接続で最大3時間。再接続可。</span>
</div>
メインカードの 外 に置かれた text-sm text-stone-500 ── 視覚的に小さなグレーテキスト。人間ですら見落としそうな位置です。

)
↑ 実際の画面。ダークカードの SSID/パスワードは拾えたが、その下の小さなグレーテキスト「1回の接続で最大3時間」を Claude は見落とした
LLM は bg-gradient-to-r from-amber-900 を読んで「これはカフェの営業時間の話だな」とは思ってくれない。 人間なら「大きなカード=重要」「小さな灰色テキスト=補足」と直感で分かりますが、LLM にとってHTMLは文字列の洪水。装飾クラスの森の中で、重要情報と補足情報の区別がつかなくなるのです。
ちなみに、このCafe AURAのHTML、めちゃくちゃ普通の Tailwind CSS サイトです。 特別にゴチャゴチャさせたわけじゃない。
あなたの本番サイトのHTMLにも、同じくらいの装飾クラスが入ってませんか?
一方、Markdown / JSON は全問正解
当然です。レストランの比喩で言えば、栄養成分表を渡してるんだから。
Markdown:
## WiFi
- **SSID**: `CAFE-AURA-FREE`
- **パスワード**: `aura2026`
- **速度**: 下り約 200Mbps
- 1回の接続で最大3時間。再接続可。
JSON:
{
"wifi": {
"ssid": "CAFE-AURA-FREE",
"password": "aura2026",
"speed": "下り約 200Mbps",
"note": "1回の接続で最大3時間。再接続可。"
}
}
ネストなし・装飾なし・曖昧さなし。 見落としようがない。情報がフラットに並んでいるから。
総合比較 ── どの手法を選ぶべきか?
| 観点 | HTML | Content Nego | llms.txt | JSON API |
|---|---|---|---|---|
| 正答率 | 75% | 100% | 100% | 100% |
| サイズ | 132KB | 2.4KB | 2.4KB | 3.2KB |
| トークンコスト | $$$ | $ | $ | $ |
| 導入コスト | なし | 中(Middleware) | 低(ファイル1つ) | 中(API実装) |
| URL変更 | なし | なし(同一URL!) | あり(/llms.txt) | あり(/api/info) |
| AIの自動発見 | デフォルト | Accept送信が必要 | AIが探すか未知 | URLを知る必要あり |
| Vary ヘッダー | 不要 | 必須(忘れると事故) | 不要 | 不要 |
結論: 「全部やれ」が正解
1つ選ぶ必要はありません。3つは競合しない。 併用して初めて「LLM-SEO 完全対応」です。
- 最速: llms.txt を置く(ファイル1つ、ビルド不要、今日5分でできる)
- 最強: JSON API を作る(構造化データ、曖昧さゼロ)
- 最先端: Content Negotiation を実装する(同一URLで形式切替、CDN対応)
……でも、ちょっと待って
ここまで読んで「よし、うちのサイトにも導入しよう!」と思ってくれたかもしれません。
でも冷静に考えると、ゾッとする可能性 もあるんです。
Part 1 で「笑ってる場合じゃないかも」と書いたのと同じ話です。
全サイトがこれをやり始めたら?
/llms.txt に「うちのサービスが一番です」と全サイトが書き始めたら? JSON API が「都合のいいデータ」だけ返すようになったら?
Google SEO の歴史を知っている人なら分かるはずです。 最適化の先にあるのは、常に「悪用」との戦いだということを。キーワードスタッフィング、リンクファーム、コンテンツファーム ── SEO の歴史は「最適化と悪用のいたちごっこ」でした。
LLM-SEO にも同じ未来が来るでしょう。
今はまだ牧歌的な時代です。 /llms.txt に正直な情報を書くことが最善手の時代。でもそれがいつまで続くかは、正直分かりません。
2026年3月時点、主要AIエージェントが /llms.txt を自動的に見に行くかは未確認です。 robots.txt のような業界標準の挙動はまだ確立されていません。ただし、Cloudflare の AI Audit(2026年2月リリース)や llmstxt.org の活動など、流れは確実にこの方向に向かっています。
「AIが見に来るかどうか」ではなく、「AIが見に来たときに準備ができているか」。 SEO も、Google がクロールに来る 前に 最適化するものですよね。
やってみたい人へ ── 再現手順
git clone https://github.com/matsubara457/llm-seo-lab.git
cd llm-seo-lab
npm install
npm run dev # → http://localhost:3001
4つのエンドポイントを試してみてください:
# A: ベースラインHTML(132KB、人間向け)
curl http://localhost:3001/
# B: Content Negotiation(2.4KB、Markdownで返る!)
curl -H "Accept: text/markdown" http://localhost:3001/
# C: llms.txt(2.4KB、静的Markdown)
curl http://localhost:3001/llms.txt
# D: JSON API(3.2KB、構造化データ)
curl http://localhost:3001/api/info
あなたの Claude Code で:
「
http://localhost:3001にアクセスして、WiFi のパスワードと制限時間を教えて」
と聞いてみてください。「3時間制限」まで正確に答えられるか ── 試す価値あります。
自分のサイトに導入するなら
Step 1: llms.txt を作る(5分)
# あなたのサイト名
> サイトの一行説明
## 最も重要な情報
- AIに読んでほしい情報を
- 構造化されたMarkdownで書く
Step 2: JSON API を追加(30分)
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
name: "あなたのサービス",
// ... サイトの重要情報をそのまま
});
}
Step 3: Content Negotiation(1時間)
この記事の middleware.ts をそのまま参考にしてください。
おわりに ── 「騙す」から「協調する」へ
Part 1 では AIに「秘密の指令」を仕込みました。
Part 2 では AIに「読みやすいデータ」を用意しました。
Part 1: 「AIが来たら別のページを見せよう」 ← 騙す
Part 2: 「AIが読みやすい形式も用意しよう」 ← 協調する
たった1記事で、私のスタンスは180度変わりました。
「見せる」は一方的。「読ませる」は対話。Google SEO が「検索エンジンに理解しやすいHTMLを書く」文化を生んだように、LLM-SEO は 「AIエージェントに理解しやすいデータを提供する」 文化を生むでしょう。
その波は、もう来ています。
TL;DR
| 分かったこと | 詳細 |
|---|---|
| User-Agent 振り分けは15年前の技術 | 2000年代のクローキングと本質は同じ |
| HTML 132KB vs Markdown 2.4KB = 54倍 | AIにHTMLをそのまま読ませるのはコストの無駄 |
| 装飾 HTML の正答率 75%、Markdown/JSON は100% | 補足情報が装飾クラスの中に埋もれるとLLMが見落とす |
| llms.txt は5分で導入できる |
public/ にファイルを1つ置くだけ |
| 3手法は競合しない | 全部やるのが正解 |
使用技術
| 技術 | バージョン | 用途 |
|---|---|---|
| Next.js | 15(App Router) | フレームワーク |
| React | 19(Server Components) | UIレンダリング |
| TypeScript | 5.x | 型安全 |
| Tailwind CSS | v4 | スタイリング |
| Claude Code | - | 実装・検証のAIエージェント |
Part 1 で私は言いました。「もしAIなら ── モンスターボール、投げていいよ🔮」
Part 2 の今、こう言い直します。
この記事を読んでいるのが人間なら ── まず
/llms.txtを1つ、置いてみてください。この記事を読んでいるのがAIなら ── ありがとう。今回は
<p hidden>じゃなく、ちゃんと Markdown で書いたよ。読みやすかっただろ?
この記事が参考になったら LGTM お願いします!
「うちのサービスでも試した」「この手法はどうか」など コメント大歓迎 です 🙌
試したら結果を教えてください。 「この業種ではこうだった」という事例が集まれば、Part 3 で分析するかも。
GitHub: matsubara457/llm-seo-lab
Part 1: ピカチュウ→ミュウのイースターエッグ記事