Baiyuan GEO Platform Whitepaper

第 6 章 — AXP シャドウドキュメント:Cloudflare Worker で AI ボットにクリーンなコンテンツを配信

人間向けサイトと AI 向けコンテンツは同じ HTML であってはならない。同じ文書で両者に仕えようとすれば、両者とも損をする。

目次 {.unnumbered}


同じ HTML で両方に仕えると両方が損する理由

現代のウェブサイトは人間向けに設計されている:

これらは人間にとっては UX だが、AI クローラーにとっては雑音である。AI ボット(GPTBot、ClaudeBot、PerplexityBot、Googlebot など)が現代ブランドページを取得すると、3 種類の失敗がよく起きる:

  1. JS 実行失敗またはタイムアウト — 多くの AI ボットは JS を実行しないか、制限付きでしか実行しない。SPA ページは <div id="app"></div> だけを取得する
  2. 本体コンテンツ抽出失敗 — HTML ノイズが多すぎて、ブランド情報と UI 装飾を AI が区別できない
  3. 構造化データ欠落 — Schema.org JSON-LD が動的生成の位置に置かれ、AI が取得できない

結果、AI のブランド認識は誤り薄っぺらになる。解決策はサイト全体を AI に合わせて改造することではなく、AI 専用のクリーンなシャドウコンテンツを別に用意することである。

図 6-1:同一ブランドの 2 種類の視点

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:シャドウドキュメントの構造

AXP(AI-ready eXchange Page)は百原がこの種のシャドウドキュメントに付けた名前。1 つの AXP ページは 3 つの層から成る:

図 6-2: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"> に格納する。


Cloudflare Worker 注入機構

AXP の配信方法はエッジ注入——CDN レベルでリクエストを傍受し、User-Agent に応じて返すコンテンツを決める。我々は Cloudflare Workers を採用する。

図 6-3:Worker ルーティング判断フロー

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 を取りに行く。人間のリクエストは直接パススルーされ、オリジンと同じレイテンシ。

Worker 擬似コード

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' },
    });
  },
};

重要な設計ポイント


AI ボット UA 一覧と検出戦略

現在百原プラットフォームは 25 種類の AI ボット UA を識別し、機能により 4 群に分類する:

図 6-4:AI ボット UA の分類

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 プラットフォーム自身がその 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 設定ページで管理。表にない新パスはデフォルトでオリジンパススルー、保守的戦略を採る。


Sitemap 自動生成

AI ボットのクロール効率は sitemap.xml に依存する。AXP モードではAXP パスと完全一致する sitemap を動的生成せねばならない。さもなくば「sitemap に載っているが Worker がそのパスで AXP を注入しない」という混乱が生じる。

v3.0.0 より、百原プラットフォームはデュアルソースマージアーキテクチャで各顧客ドメインの sitemap を自動生成する:

デュアルソース

同一 URL が両ソースに存在する場合はAXP バージョンが重複排除優先(dedup priority)となる。

仕様準拠の出力

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 通念で隠す必要なし)。


JSON-LD フラット化の落とし穴

Schema.org 仕様はネスト配列を許容するが、実務では以下の問題にぶつかる:

図 6-6:誤りと正しい例の並列

//  誤り:ネスト配列(一部の 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 連結を維持する利点:


GSC インデックスの血涙ノート

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 類の問題をチェックする方が、本番上がってから発見するより遥かにコスト安である。


シャドウ公開ファイルスイート:llms.txt / llms-full.txt / schema.json / feed.xml

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 はブランド業種に応じて OrganizationSoftwareApplicationLocalBusiness のいずれかを選択
feed.xml /feed.xml AXP ページの RSS 2.0 フィード——RSS をフォローする AI プラットフォームがコンテンツ更新を発見できる

4 ファイル共通の特性

llms.txt 標準の背景

llms.txt フォーマットは新興の llms.txt コミュニティ標準に準拠する——robots.txt がかつてボット通信を標準化したのと同様の位置づけである。PerplexityBot や OAI-SearchBot などの AI クローラーは、ドメインに /llms.txt が存在すると既にこれを取得することが知られている。


RAG 知識ベース連携:ブランド専用 KB の自動化

AXP ページの品質は「AI ボットに配信できるか」だけでなく、コンテンツに含まれるブランド固有の知識量に左右される。LLM が推論だけで生成したページは幻覚や曖昧な記述を含みやすい。ブランド自身の知識ベース(KB)から事実を注入することで、出力品質は桁違いに向上する。

ブランド分離:ブランドごとに専用 KB

百原は中央共用 RAG エンジン(§9.4)を採用しているが、各ブランドのドキュメントは rag_kb_id で区別された専用の Knowledge Base(KB)に格納される:

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 ステップを実行する:

  1. KB 作成(存在しない場合)— ragCreateKnowledgeBase が新しい kbId を返し、brand_rag_configs に書き込む
  2. ブランドプロフィールをアップロード(テキストドキュメント)— ブランド名・業種・概要・公式サイト・コアキーワードを構造化テキストとして KB のアンカー知識に
  3. 公式サイトページ URL をアップロード(最大 20 件、geo_importance 降順)— RAG バックエンドがクロール・ベクトル化し、静的プロフィール知識を補完
// enableAXP の setImmediate ブロック内(ノンブロッキング)
await initialCrawl(brandId, tenantId);   // AXP ページ生成
await seedBrandRAGKB(brandId, queryFn);  // 並行: RAG KB シード

設計上の考慮点:

キーワード注入:ブランドの声を AXP コンテンツに

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 コメントを正しく除去できない。


統一パイプライン再構築:8 種の命名断片から 22 カテゴリへ

AXP 稼働 1 年で断片化が蓄積:同じ概念に複数の命名(facts / fact_check / factCheck)、セクション命名のドリフト、ジェネレーターロジックが散在。2026 年 4 月に P1-P9 統一パイプライン再構築を実施、目標は「1 つのパイプライン、1 つの命名、1 つの出力」。

Fig 6-10:9 段階

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 段階。各段階は独立して検証可能、前段階が完了するまで次に進まない。

22 統一 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 はアーキテクチャ的に禁止 — 孤児カテゴリは存在しない。

9 ジェネレーターの 3 規則

  1. 単一入力源:brand + RAG knowledge + pricing API のみ読む
  2. 冪等:同じ入力は同じ出力(LLM temperature=0)
  3. 追跡可能:出力に source_chunks: [{rag_chunk_id, score}] を含む

RAG クローズドループ:欠損検出 → 自動補完

3 つの cron がエンドツーエンドで連動:

  1. Detector cron(毎日 02:00):detectContentGaps(brandId) で欠損を content_gaps テーブルに記録
  2. Processor cron(2h ごと):status='pending' を取得、対応ジェネレーターを実行
  3. Verifier cron(毎日 06:00):24h 以内の出力の文字数 / 構造を検査、未達ならリキュー

5 パイロットブランドで llms-full.txt の平均文字数が 8K → 52K(6.5×)へ向上。「お客様が忘れた」死区を解消。

Schema.org 多型注入(P8)

P1-P3 で Article / WebPage を注入、P8 で 3 種を追加:

注入戦略は §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 廃止に移行パス必須、新章は規格審査を通過必須。


要点 {.unnumbered}

参考文献 {.unnumbered}


ナビゲーション← 第 5 章:複数プロバイダ AI ルーティング · 📖 目次 · 第 7 章:Schema.org フェーズ 1 →