3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【LLM-SEO】同じ情報なのにAIの正答率が75%→100%に変わった。違いは「HTMLかMarkdownか」だけだった

3
Last updated at Posted at 2026-03-03

同じサイト、同じ情報。データ形式を変えただけで──

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 のファーストビュー。amber / brown のグラデーションで構成された、よくあるカフェサイト
↑ Cafe AURA のファーストビュー。Tailwind CSS で作った「普通のカフェサイト」。これが 132KB の HTML になる

データの心臓部 ── lib/cafe-data.ts

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

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

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

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が読まされているデータ量です。

Cafe AURA のフルページスクリーンショット。上から下まで装飾が詰まったリッチHTML
↑ これが 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 ── 視覚的に小さなグレーテキスト。人間ですら見落としそうな位置です。

WiFiセクションの実際の見た目。ダークカードの下に小さなグレーテキストで「3時間制限」が書かれている
)
↑ 実際の画面。ダークカードの 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分)

public/llms.txt
# あなたのサイト名

> サイトの一行説明

## 最も重要な情報
- AIに読んでほしい情報を
- 構造化されたMarkdownで書く

Step 2: JSON API を追加(30分)

app/api/info/route.ts
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: ピカチュウ→ミュウのイースターエッグ記事

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?