給人類看的網站和給 AI 看的內容不該相同。強行用同一份 HTML 服務兩方,兩方都吃虧。
現代網站為人類設計,充滿:
<div class="col-md-6"> 嵌套幾十層這些元素對人類是使用體驗,對AI 爬蟲是噪音。AI Bot(GPTBot、ClaudeBot、Perplexity、Googlebot 等)抓取一個現代品牌頁面時,常見三種失敗:
<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 -->|Browser| User[使用者體驗]
A -->|AI Bot| LLM[模型訓練與檢索]
Fig 6-1: 同一份品牌資料導出兩種呈現:人類版優化體驗、AI 版優化語意。
AXP(AI-ready eXchange Page)是百原對這類影子文檔的命名。一個 AXP 頁面由三個部分組成:
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 Bot Consumption]
JSONLD --> AI
MD --> AI
Fig 6-2: 三層齊備讓不同類型的 AI 爬蟲各取所需。純 HTML 給粗粒度抓取、JSON-LD 給知識圖譜、Markdown 給 RAG。
① 純 HTML 骨架
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="utf-8">
<title>百原科技 — 生成式引擎優化 SaaS</title>
<meta name="description" content="百原科技是台灣首家 GEO SaaS...">
<link rel="canonical" href="https://geo.baiyuan.io/">
</head>
<body>
<main>
<h1>百原科技</h1>
<section>
<h2>公司簡介</h2>
<p>成立於 2024 年,專注於...</p>
</section>
<section>
<h2>服務項目</h2>
<ul>
<li>GEO 掃描與評分</li>
<li>AXP 影子文檔生成</li>
</ul>
</section>
</main>
</body>
</html>
② Schema.org JSON-LD(詳見 Ch 7)
③ Markdown 原文塊(給 RAG 切片用)
# 百原科技
## 公司簡介
成立於 2024 年,專注於生成式引擎優化 SaaS 研發...
## 服務項目
- GEO 掃描與評分
- AXP 影子文檔生成
三層共存於同一個 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 Bot?}
UA -->|是| Cache{Worker Cache<br/>命中?}
UA -->|否| 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]
Fig 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 Bot:透傳原站
if (!isAIBot(ua)) {
return fetch(request); // proxy to customer origin
}
// 2. AI Bot:嘗試快取
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 失敗,fallback 原站
}
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 Bot 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
Fig 6-4: 25 種 AI Bot 的分群示意。百原平台對四類全部啟用 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 Bot |
|---|---|---|
/ |
行銷首頁(公開) | AXP「百原科技」品牌頁 |
/dashboard |
登入後儀表板(私有) | 403,不該被 AXP 化 |
/features, /pricing |
產品介紹(公開) | AXP 對應的 service 頁 |
/login, /signup |
登入/註冊(公開但無品牌資訊) | 不注入,透傳 |
flowchart TD
R[Request] --> B{AI Bot?}
B -->|否| P1[透傳原站]
B -->|是| P{路徑分類}
P -->|行銷/品牌頁| A[注入 AXP]
P -->|登入後功能| X1[回 403 Forbidden]
P -->|無品牌內容的公開頁| X2[透傳原站]
Fig 6-5: 路徑分類表由 admin 在每個品牌的設定頁維護;不在表中的新路徑預設透傳原站,採保守策略。
AI Bot 爬取效率依賴 sitemap.xml。AXP 模式下必須動態產生與 AXP 路徑完全對齊的 sitemap,否則會出現「sitemap 列了某 URL,但 Worker 該路徑沒注入 AXP」的混亂。
百原平台為每個客戶域名自動產生 sitemap,規則:
brand_locations、brand_services、brand_employees 表動態生成 URL<lastmod> 為對應實體的 updated_at<priority> 依路徑類型:首頁 1.0、服務頁 0.8、員工頁 0.6Sitemap: https://<domain>/sitemap.xmlSitemap 與 AXP 同樣由 CF Worker 注入;人類用戶若輸入 /sitemap.xml 也會看到(這是 SEO 通用慣例,不必隱藏)。
Schema.org 規範允許嵌套陣列(nested array),但實務上會踩到以下問題:
// ❌ 錯誤:nested array(部分 AI 解析器會整塊拒絕)
{
"@context": "https://schema.org",
"@graph": [
[
{ "@type": "Organization", "name": "百原科技" }
],
[
{ "@type": "Service", "name": "GEO 掃描" }
]
]
}
// ✅ 正確:flat array
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Organization", "@id": "#org", "name": "百原科技" },
{ "@type": "Service", "@id": "#svc-scan", "name": "GEO 掃描",
"provider": { "@id": "#org" } }
]
}
Fig 6-6: 實體間關聯用 @id 引用,而非用陣列嵌套表達。此為 Schema.org 工具驗證的硬性要求。
維持 flat 化 + 用 @id 連結的好處:
從 2024 年到 2025 年的實作過程中,我們在 Google Search Console(GSC)索引上踩過的坑:
| 踩坑 | 症狀 | 根因 | 解法 |
|---|---|---|---|
noindex meta 誤覆蓋 |
GSC 顯示「已被 noindex 排除」 |
UAT 環境 .env 被誤部到 PROD |
環境變數加 strict 檢查,啟動時拒絕不合理組合 |
| canonical 跨域 | PROD 頁面 canonical 指向 UAT 域名 | 同一 code base 兩環境共用 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 Bot 的抓取重試頻率低於 Googlebot,一次錯誤可能要等幾週才有機會被再次抓取。寧可先部署一個 check-prod-seo.sh 腳本在 CI 檢查上述五類問題,也不要上線後才發現。
AXP 頁面的品質不只取決於「能否服務 AI Bot」,更取決於內容本身有多少品牌知識。純由 LLM 憑空生成的頁面容易出現幻覺或空泛描述;若能在生成時注入來自品牌自身知識庫的事實,輸出品質會有量級的差距。
百原採用「中央共用 RAG 引擎」(§9.4),但每個品牌的文件存於獨立的 Knowledge Base(KB),以 rag_kb_id 區隔查詢範圍:
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-7: 品牌層級 KB 隔離。中央引擎共用,知識互不污染。
seedBrandRAGKB:AXP 啟用時自動建立並填充知識庫新品牌啟用 AXP(enableAXP API)時,系統在背景非同步執行三步驟:
ragCreateKnowledgeBase 返回新 kbId,寫入 brand_rag_configsgeo_importance 降序)— RAG 後端爬取並向量化,補充動態網站內容// enableAXP 的 setImmediate block 中(非阻塞)
await initialCrawl(brandId, tenantId); // 生成 AXP 頁面
await seedBrandRAGKB(brandId, queryFn); // 平行 seed RAG KB
關鍵設計考量:
brand.keywords(品牌設定的目標 GEO 關鍵字)以 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 欄位:取 content_md 前 150 字元(去除多行 HTML 注釋後)作為頁面卡片摘要。
這解決了一個實務問題:同一品牌 6 個頁型若全部顯示相同的「品牌指紋短語」(fingerprint_phrase),後台使用者無法確認各頁是否正確生成。
-- 去除多行 HTML 注釋,取前 150 字元
LEFT(REGEXP_REPLACE(content_md, '<!--[\s\S]*?-->', '', 'g'), 150) AS content_preview
注意 regex 需使用 [\s\S]*?(dotall 模式)而非 [^>]*,否則跨行 HTML 注釋無法正確去除。
AXP 上線一年,自然累積出碎片化:同一概念有多個命名(facts / fact_check / factCheck)、章節命名前後不一致、generator 邏輯散落各處。2026 年 4 月做了一次 P1-P9 統一產線重構,目標是「同一產線、同一命名、同一輸出」。
flowchart LR
P1[P1 命名統一<br/>22 類 page_type] --> P2[P2 generators<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: P1-P9 九個階段。每階段獨立可驗收,沒做完不進下一階段。
page_type 命名表舊系統有 homepage / home / brandHome 三個命名指同一頁;fact_check / factCheck / facts 也混用。重構後用 22 類 snake_case 一統:
| 類別 | 範例 page_type | 說明 |
|---|---|---|
| 基礎 | 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 類起) |
每一類有對應 generator(generators/brandOverview.js 等)。沒對應 generator 的 page_type 不能存在 — 從架構上禁止「孤兒類別」。
generators/
brandOverview.js — 品牌概述(以 description + keywords 為基底)
faq.js — FAQ(從 RAG 抽 30 個常問題,LLM 改寫)
productFeatures.js — 產品特色(以服務 list + USP 為基底)
pricing.js — 定價說明(直接讀 brand_visual_configs / 訂閱方案)
competitorComparison.js — 競品對照(從 ARSPanel 8 維度抽)
factCheck.js — 事實核查(取 RAG ground truth 對外公布)
reviewAggregate.js — 評論聚合(GBP API + 5 評論平台)
caseStudy.js — 案例研究(客戶提供 / RAG 自動萃取)
futurePlans.js — 未來計劃(個人 IP v3.0.0 新增,只給 brand_type=personal_ip)
每個 generator 滿足三條規則:
brand + RAG knowledge + pricing API,不讀 DB scattered tablessource_chunks: [{rag_chunk_id, score}],讓客戶看得到引用源舊系統靠人工發現「品牌 X 的競品分析寫得太薄」。新系統有自動缺口偵測(P3):
detectContentGaps(brandId),比對 22 類 page_type vs 已生成,缺哪類記入 content_gaps 表content_gaps WHERE status='pending',對應 generator 跑一次,寫回實測 5 個品牌平均 llms-full.txt 字數從 8K 提升到 52K(6.5×),主因是 22 類齊全 + 缺口自動補,不再有「客戶忘了補」的死區。
P1-P3 各 page_type 注入 Article / WebPage;P8 補完三個進階型:
faq.js 輸出抽 Q/A pair,生成 FAQPage > mainEntity[Question] 結構reviewAggregate.js 輸出注入,GBP 5 顆星評分直接灌進 aggregateRating.ratingValuecreator_profile.js 輸出 Person > knowsAbout + worksFor + award,讓 AI 平台把個人 IP 當「人物實體」處理注入策略沿用 6.7 節的扁平化原則(@id 連接而非 nested array),避免 GSC Rich Results Test 噴 nested context 警告。
| 指標 | 重構前 | 重構後 |
|---|---|---|
| 命名種類 | 8 種混用 | 22 類 snake_case |
| 重複/孤兒 page_type | 13 個 | 0 |
| llms-full.txt 平均字數 | 8K | 52K(6.5×) |
| 客戶手動補頁數量 | 高 | 0(自動補) |
| 舊 UI 死碼 | 約 3000 行 | -2997 行(P5 清掉) |
重構規格 7 條(P9 強制執行):
這 7 條把架構腐敗的速度打慢了 — 下一年回看,重構成本明顯降低。
@id flat 化而非 nested arrayrag_kb_id);seedBrandRAGKB 在 AXP 啟用時自動上傳品牌 Profile 與官網頁面 URL,無需人工干預brand.keywords 以 keywordsHint 注入 RAG 查詢,定價頁與功能頁強制生成關鍵字豐富的開場段落;content_preview 取代 fingerprint_phrase 作頁面品質監控page_type、9 個 generator、3 道 cron 閉環;llms-full.txt 平均字數從 8K 提升到 52K(6.5×),規格 7 條鎖定架構腐敗速度導覽:← Ch 5: 多 Provider AI 路由 · 📖 目次 · Ch 7: Schema.org Phase 1 →