人間向けサイトと AI 向けコンテンツは同じ HTML であってはならない。同じ文書で両者に仕えようとすれば、両者とも損をする。
現代のウェブサイトは人間向けに設計されている:
<div class="col-md-6"> が何十層もネストこれらは人間にとっては UX だが、AI クローラーにとっては雑音である。AI ボット(GPTBot、ClaudeBot、PerplexityBot、Googlebot など)が現代ブランドページを取得すると、3 種類の失敗がよく起きる:
<div id="app"></div> だけを取得する結果、AI のブランド認識は誤りか薄っぺらになる。解決策はサイト全体を AI に合わせて改造することではなく、AI 専用のクリーンなシャドウコンテンツを別に用意することである。
flowchart LR
Brand[ブランドデータ] --> H[人間版サイト<br/>React / Vue / Next.js<br/>完全 UI / アニメ / 広告]
Brand --> A[AI 版シャドウドキュメント<br/>Pure HTML + JSON-LD<br/>純粋な意味、雑音なし]
H -->|ブラウザ| User[ユーザー体験]
A -->|AI ボット| LLM[モデル訓練と検索]
図 6-1:同じブランドデータから 2 つの表現を派生させる。人間版は体験最適化、AI 版は意味最適化。
AXP(AI-ready eXchange Page)は百原がこの種のシャドウドキュメントに付けた名前。1 つの AXP ページは 3 つの層から成る:
flowchart TB
subgraph AXP["AXP シャドウドキュメント"]
HTML["① 純粋 HTML 意味骨格<br/>h1 / h2 / p / ul / table"]
JSONLD["② Schema.org JSON-LD<br/>Organization / Service / Person<br/>三層 @id 相互リンク"]
MD["③ Markdown 生テキストブロック<br/>RAG チャンク化とベクトル化用"]
end
HTML --> AI[AI ボット消費]
JSONLD --> AI
MD --> AI
図 6-2:3 層が揃うことで異なるタイプの AI クローラーがそれぞれ必要な情報を取得できる。純粋 HTML は粗粒度の取得、JSON-LD はナレッジグラフ、Markdown は RAG 用。
3 層は同一 URL の応答内に共存する:HTML を本体とし、JSON-LD を <script type="application/ld+json"> に、Markdown を <script type="text/markdown" id="axp-markdown"> に格納する。
AXP の配信方法はエッジ注入——CDN レベルでリクエストを傍受し、User-Agent に応じて返すコンテンツを決める。我々は Cloudflare Workers を採用する。
flowchart TD
Req[HTTP Request] --> UA{User-Agent<br/>が AI ボットと一致?}
UA -->|Yes| Cache{Worker Cache<br/>ヒット?}
UA -->|No| Pass[オリジンにパススルー<br/>顧客 origin]
Cache -->|ヒット| Serve[キャッシュのシャドウ文書を返す]
Cache -->|ミス| Fetch[geo.baiyuan.io API から<br/>該当ブランドのシャドウ文書を取得]
Fetch --> Store[Worker Cache に書込<br/>TTL 15 分]
Store --> Serve
Pass --> Origin[顧客 Origin Server]
図 6-3:Worker はキャッシュを優先し、ミス時のみオリジンに AXP を取りに行く。人間のリクエストは直接パススルーされ、オリジンと同じレイテンシ。
export default {
async fetch(request, env) {
const ua = request.headers.get('user-agent') || '';
const url = new URL(request.url);
// 1. AI ボットではない:オリジンにパススルー
if (!isAIBot(ua)) {
return fetch(request); // proxy to customer origin
}
// 2. AI ボット:キャッシュを試す
const cacheKey = `axp:${url.hostname}:${url.pathname}`;
const cached = await env.KV.get(cacheKey);
if (cached) {
return new Response(cached, {
headers: { 'content-type': 'text/html; charset=utf-8' },
});
}
// 3. ミス:オリジンで AXP を取得
const axpUrl = `https://api.geo.baiyuan.io/axp?host=${url.hostname}&path=${url.pathname}`;
const axpRes = await fetch(axpUrl);
if (!axpRes.ok) {
return fetch(request); // AXP 取得失敗、オリジンにフォールバック
}
const body = await axpRes.text();
await env.KV.put(cacheKey, body, { expirationTtl: 900 });
return new Response(body, {
headers: { 'content-type': 'text/html; charset=utf-8' },
});
},
};
重要な設計ポイント:
現在百原プラットフォームは 25 種類の AI ボット UA を識別し、機能により 4 群に分類する:
flowchart LR
subgraph LLM["大規模言語モデルクローラー (10)"]
GPTBot
ClaudeBot
GoogleExtended[Google-Extended]
CCBot[CCBot / Common Crawl]
MetaBot[FacebookBot / Meta-ExternalAgent]
ByteBot[Bytespider]
AnthropicBot[anthropic-ai]
AppleBot[Applebot-Extended]
end
subgraph Search["検索型クローラー (6)"]
Perplexity[PerplexityBot]
ChatGPTUser[ChatGPT-User]
PerplexityUser[Perplexity-User]
YouBot[YouBot]
PhindBot
end
subgraph Preview["Preview / Link 生成 (5)"]
LinkedInBot
FacebookExt[facebookexternalhit]
TwitterBot[Twitterbot]
DiscordBot[Discordbot]
end
subgraph RAG["RAG / 企業クローラー (4)"]
CohereBot[cohere-ai]
DiffBot[Diffbot]
OmigoBot[Omigo]
end
図 6-4:25 種類の AI ボットを群別で分類。百原プラットフォームは 4 群すべてに AXP 注入を有効化しているが、顧客ニーズに応じて admin で群ごとに無効化できる。
実装では正規表現マージを採用、if-else 連打ではなく保守性優先:
const AI_BOT_REGEX = new RegExp(
[
'GPTBot', 'ChatGPT-User', 'OAI-SearchBot',
'ClaudeBot', 'anthropic-ai', 'Claude-Web',
'Google-Extended', 'GoogleOther',
'PerplexityBot', 'Perplexity-User',
'CCBot', 'Bytespider', 'FacebookBot',
'Meta-ExternalAgent', 'Applebot-Extended',
'cohere-ai', 'Diffbot', 'YouBot', 'PhindBot',
'LinkedInBot', 'facebookexternalhit', 'Twitterbot',
'Discordbot', 'Omigo', 'DuckAssistBot',
].join('|'),
'i'
);
function isAIBot(ua) {
return AI_BOT_REGEX.test(ua);
}
UA 一覧は四半期ごとに見直す。新出現のクローラー(OAI-SearchBot が 2025 年 7 月に初登場など)は即時追加が必要。
実務上の特殊ケース:SaaS プラットフォーム自身がその SaaS のユーザーでもあるとき(dogfooding)、自社ドメインは「プラットフォーム利用者」(ログイン後にプロダクトを使う)と「ブランドサイト訪問者」(匿名で紹介を読む)の両方に対応する必要がある。
百原自社の geo.baiyuan.io がまさにこのシナリオ:
| パス | 人間ユーザー | AI ボット |
|---|---|---|
/ |
マーケホーム(公開) | 「百原科技」ブランドページの AXP |
/dashboard |
ログイン後ダッシュボード(プライベート) | 403、AXP 化すべきでない |
/features, /pricing |
プロダクト紹介(公開) | 対応サービスページの AXP |
/login, /signup |
ログイン / 登録(公開だがブランド情報なし) | 注入せず、パススルー |
flowchart TD
R[Request] --> B{AI ボット?}
B -->|No| P1[オリジンにパススルー]
B -->|Yes| P{パス分類}
P -->|マーケ / ブランドページ| A[AXP 注入]
P -->|認証後機能| X1[403 Forbidden を返す]
P -->|公開だがブランド内容なし| X2[オリジンにパススルー]
図 6-5:パス分類表は各ブランドの admin 設定ページで管理。表にない新パスはデフォルトでオリジンパススルー、保守的戦略を採る。
AI ボットのクロール効率は sitemap.xml に依存する。AXP モードではAXP パスと完全一致する sitemap を動的生成せねばならない。さもなくば「sitemap に載っているが Worker がそのパスで AXP を注入しない」という混乱が生じる。
v3.0.0 より、百原プラットフォームはデュアルソースマージアーキテクチャで各顧客ドメインの sitemap を自動生成する:
デュアルソース
sitemapScanner が顧客オリジン sitemap をクロールして書き込んだもの同一 URL が両ソースに存在する場合はAXP バージョンが重複排除優先(dedup priority)となる。
仕様準拠の出力
<loc> と <lastmod> のみ出力——<changefreq> も <priority> も含まない
(sitemap 仕様 §4.2 に準拠;両フィールドは主要検索エンジンに無視されており混乱を招く)DENY_LIST フィルタリング
すべての URL は出力前に deny list を通過する。/login、/register、/logout、/dashboard/、/admin/、/api/、/cart、/checkout などのプライベートパスや非ブランドパスを除外。
ユーザー操作
AXP Panel → sitemap.xml セクション → 「オリジンから取得」ボタンをクリック。POST /brands/:id/axp/sitemap/fetch を呼び出し、設定を scoring_configs に書き込みつつ sitemapScanner をトリガーしてオリジン sitemap をクロール、結果を brand_content_pages に書き込む。プロセス全体は数秒で完了し sitemap が即座に更新される。
セキュリティ
all-published-pages 列挙エンドポイントは INTERNAL_SITEMAP_SECRET ヘッダーで保護され、競合ブランドの列挙を防止する。
robots.txt で Sitemap: https://<domain>/sitemap.xml を積極宣言。Sitemap も CF Worker が注入する。人間が /sitemap.xml を入力しても見える(SEO 通念で隠す必要なし)。
Schema.org 仕様はネスト配列を許容するが、実務では以下の問題にぶつかる:
// ❌ 誤り:ネスト配列(一部の AI パーサーはブロック全体を拒否する)
{
"@context": "https://schema.org",
"@graph": [
[
{ "@type": "Organization", "name": "百原科技" }
],
[
{ "@type": "Service", "name": "GEO スキャン" }
]
]
}
// ✅ 正しい:フラット配列
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Organization", "@id": "#org", "name": "百原科技" },
{ "@type": "Service", "@id": "#svc-scan", "name": "GEO スキャン",
"provider": { "@id": "#org" } }
]
}
図 6-6:エンティティ間の関係は配列ネストではなく @id 参照で表現する。Schema.org ツール検証の必須要件。
フラット化 + @id 連結を維持する利点:
2024 年から 2025 年にかけての実装で Google Search Console(GSC)インデックスで踏んだ落とし穴:
| 落とし穴 | 症状 | 根因 | 解決 |
|---|---|---|---|
noindex meta の誤上書き |
GSC に「noindex で除外」表示 |
UAT 環境の .env を誤って PROD にデプロイ |
環境変数に strict 検査を追加、起動時に不正組み合わせを拒否 |
| canonical のクロスドメイン | PROD ページの canonical が UAT ドメインを指す | 同コードベースの 2 環境が canonical ロジックを共有 | canonical を request.hostname から動的生成 |
| Bot UA 漏れ | GSC 指数は変動するが特定の AI シテーションが消えた | 新型 Bot が UA regex に未追加 | 四半期ごとに CF Worker log の未一致 UA を確認 |
| Sitemap 不整合 | GSC に Discovered – currently not indexed 警告 |
AXP ページは存在するが sitemap から漏れ | Sitemap 生成を同じソース(AXP インデックステーブル)から派生 |
| HTTPS/HTTP 混在 | robots.txt が HTTP では 200、HTTPS では 404 | Worker が http:// トラフィック未処理 |
301 → HTTPS 強制、robots も同期注入 |
これらの落とし穴は AXP 固有ではないが、AXP がその深刻度を増幅する——AI ボットの再クロール頻度は Googlebot より低く、1 回のミスが数週間後にやっと再クロール機会を得る。先に check-prod-seo.sh スクリプトを CI で走らせ 5 類の問題をチェックする方が、本番上がってから発見するより遥かにコスト安である。
sitemap.xml に加え、v3.0.0 プラットフォームはブランドごとに 4 つの追加公開 AI 可読ファイルを生成する:
| ファイル | URL | 用途 |
|---|---|---|
| llms.txt | /llms.txt | AI クローラー向け Markdown 形式の簡潔なブランドサマリー。主要事実、機能、定価概要、連絡先、sitemap と schema.json へのリンクを掲載 |
| llms-full.txt | /llms-full.txt | 詳細な価格、機能一覧、仕様説明を含む拡張版 |
| schema.json | /schema.json | スタンドアロンファイルとして配信する JSON-LD(Schema.org)。@type はブランド業種に応じて Organization・SoftwareApplication・LocalBusiness のいずれかを選択 |
| feed.xml | /feed.xml | AXP ページの RSS 2.0 フィード——RSS をフォローする AI プラットフォームがコンテンツ更新を発見できる |
4 ファイル共通の特性
/c/:slug/ の両方で提供axp_pages、ground_truths、brand_features、brand_services)から生成# For AI/LLM crawlers: LLM-friendly site summary at https://<domain>/llms.txtllms.txt 標準の背景
llms.txt フォーマットは新興の llms.txt コミュニティ標準に準拠する——robots.txt がかつてボット通信を標準化したのと同様の位置づけである。PerplexityBot や OAI-SearchBot などの AI クローラーは、ドメインに /llms.txt が存在すると既にこれを取得することが知られている。
AXP ページの品質は「AI ボットに配信できるか」だけでなく、コンテンツに含まれるブランド固有の知識量に左右される。LLM が推論だけで生成したページは幻覚や曖昧な記述を含みやすい。ブランド自身の知識ベース(KB)から事実を注入することで、出力品質は桁違いに向上する。
百原は中央共用 RAG エンジン(§9.4)を採用しているが、各ブランドのドキュメントは rag_kb_id で区別された専用の Knowledge Base(KB)に格納される:
kbId を指定してクエリを実行し、そのブランド固有の事実のみを取得することを保証flowchart LR
subgraph Tenant["同一テナント"]
BrandA["ブランド A<br/>rag_kb_id: kb-aaa"]
BrandB["ブランド B<br/>rag_kb_id: kb-bbb"]
end
subgraph RAG["中央 RAG エンジン"]
KBA["KB: kb-aaa<br/>(ブランド A 専用)"]
KBB["KB: kb-bbb<br/>(ブランド B 専用)"]
end
BrandA -->|askWithKbId| KBA
BrandB -->|askWithKbId| KBB
Fig 6-8: ブランドレベルの KB 分離。エンジンは共用、知識は汚染されない。
seedBrandRAGKB:AXP 有効化時の KB 自動作成とシード新しいブランドが AXP を有効化(enableAXP API)すると、システムはバックグラウンドで非同期に 3 ステップを実行する:
ragCreateKnowledgeBase が新しい kbId を返し、brand_rag_configs に書き込むgeo_importance 降順)— RAG バックエンドがクロール・ベクトル化し、静的プロフィール知識を補完// enableAXP の setImmediate ブロック内(ノンブロッキング)
await initialCrawl(brandId, tenantId); // AXP ページ生成
await seedBrandRAGKB(brandId, queryFn); // 並行: RAG KB シード
設計上の考慮点:
brand.keywords(ブランドが設定したターゲット GEO/SEO キーワード)は、keywordsHint サフィックスとしてすべての RAG クエリに注入される:
// hybridCoordinator.service.js
const keywordsHint = keywords.length
? `\n\n以下のターゲットキーワードを自然に組み込んでください(すべて登場する必要はありません):${keywords.join('、')}`
: '';
const question = PAGE_TYPE_QUESTIONS[pageType](brandName) + keywordsHint;
pricing_summary(定価ページ)と product_features(機能ページ)は「純粋なテーブル、キーワードゼロ」になりやすいため、RAG プロンプトはテーブルの前にキーワードを豊富に含む導入段落の生成を明示的に要求する:
1. 導入段落(2〜3 文):ブランドのポジションを説明し、ターゲットキーワードを自然に組み込む
2. 完全な定価テーブル:知識ベースで確認済みのデータのみを掲載し、推測・捏造は不可
実測キーワードカバレッジ(ILIKE 部分文字列マッチ、5 ブランド × 6 ページタイプ、2026-04-21):
| ブランド | キーワード数 | 最低カバレッジ | 最低カバレッジ率 | 大多数ページ |
|---|---|---|---|---|
| ブランド A | 13 | pricing_summary | 9/13 | 11–13/13 |
| ブランド B | 12 | pricing_summary | 7/12 | 11–12/12 |
| ブランド C | 10 | pricing_summary | 7/10 | 9–10/10 |
| ブランド D | 12 | faq / pricing | 7/12 | 10–12/12 |
| ブランド E | 10 | pricing_summary | 1/10 ★ | 9–10/10 |
★ ブランド公式サイトに公開定価ページがないため、RAG が推測を正しく拒否。低カバレッジは想定内の動作。
content_preview:ページごとの品質監視シグナルAXP ページ一覧 API に content_preview フィールドを追加:複数行 HTML コメントを除去した後の content_md 先頭 150 文字をページカードの要約として使用。
これにより実務上の問題が解決される:同一ブランドの 6 ページタイプすべてに同じ fingerprint_phrase(ブランド指紋フレーズ)が表示されていると、各ページが正しく生成されているかを確認できなかった。
-- 複数行 HTML コメントを除去し、先頭 150 文字を取得
LEFT(REGEXP_REPLACE(content_md, '<!--[\s\S]*?-->', '', 'g'), 150) AS content_preview
regex は [\s\S]*?(dotall モード)を使用する必要がある。[^>]* では複数行にまたがる HTML コメントを正しく除去できない。
AXP 稼働 1 年で断片化が蓄積:同じ概念に複数の命名(facts / fact_check / factCheck)、セクション命名のドリフト、ジェネレーターロジックが散在。2026 年 4 月に P1-P9 統一パイプライン再構築を実施、目標は「1 つのパイプライン、1 つの命名、1 つの出力」。
flowchart LR
P1[P1 命名統一<br/>22 page_type] --> P2[P2 ジェネレーター<br/>9 個]
P2 --> P3[P3 RAG ループ<br/>欠損 → 自動補完]
P3 --> P4[P4 単一ページ UI<br/>Pipeline status]
P4 --> P5[P5 旧 UI 廃止<br/>-2997 行]
P5 --> P6[P6 brand-create<br/>自動 hook]
P6 --> P7[P7 月 06:00<br/>cron rerun-missing]
P7 --> P8[P8 Schema.org<br/>FAQPage / Review]
P8 --> P9[P9 規格 7 条<br/>完全遵守]
Fig 6-10: 9 段階。各段階は独立して検証可能、前段階が完了するまで次に進まない。
page_type カテゴリ旧システムでは homepage / home / brandHome が同じページを指し、fact_check / factCheck / facts も混用。再構築で 22 種の snake_case カテゴリに統一:
| 区分 | 例 | 備考 |
|---|---|---|
| 基盤 | brand_overview, faq, about |
全ブランド共通 |
| 製品 | product_features, pricing, competitor_comparison |
B2B 製品 |
| 信頼 | fact_check, review_aggregate, media_coverage |
第三者検証 |
| ローカル | service_area, office_address, gbp_profile |
地理結合 |
| 知識 | glossary, case_study, industry_report |
コンテンツ深度 |
| 個人 IP | creator_profile, talk_topics, future_plans |
ME プラットフォーム専用(23 番以降) |
対応するジェネレーターを持たない page_type はアーキテクチャ的に禁止 — 孤児カテゴリは存在しない。
brand + RAG knowledge + pricing API のみ読むsource_chunks: [{rag_chunk_id, score}] を含む3 つの cron がエンドツーエンドで連動:
detectContentGaps(brandId) で欠損を content_gaps テーブルに記録status='pending' を取得、対応ジェネレーターを実行5 パイロットブランドで llms-full.txt の平均文字数が 8K → 52K(6.5×)へ向上。「お客様が忘れた」死区を解消。
P1-P3 で Article / WebPage を注入、P8 で 3 種を追加:
faq.js の Q/A から FAQPage > mainEntity[Question]aggregateRating.ratingValue に直接流れるPerson > knowsAbout + worksFor + award、AI プラットフォームに人物実体として認識させる注入戦略は §6.7 のフラット化原則に従う(@id 参照、ネスト配列なし)。
| 指標 | 再構築前 | 再構築後 |
|---|---|---|
| 命名種別 | 8 種混用 | 22 snake_case |
| 重複 / 孤児 page_type | 13 個 | 0 |
llms-full.txt 平均文字数 |
8K | 52K(6.5×) |
| お客様手動補完件数 | 多 | 0(自動補完) |
| 旧 UI デッドコード | 約 3000 行 | -2997 行(P5 削除) |
再構築規格 7 条(P9 強制執行):同一概念は 1 命名のみ、対応ジェネレーターなしの page_type 禁止、ジェネレーター冪等、RAG 引用追跡可能、欠損検出エンドツーエンド、旧 UI 廃止に移行パス必須、新章は規格審査を通過必須。
@id フラット化でネスト配列を避けるrag_kb_id)を持つ;AXP 有効化時に seedBrandRAGKB がブランドプロフィールと公式サイト URL を自動アップロード——手動作業不要brand.keywords を keywordsHint として RAG クエリに注入;定価ページと機能ページはキーワードを豊富に含む導入段落を必須とする;content_preview が fingerprint_phrase に代わるページ品質シグナルとなるナビゲーション:← 第 5 章:複数プロバイダ AI ルーティング · 📖 目次 · 第 7 章:Schema.org フェーズ 1 →