回首頁
AIEO · SEO · Performance

AIEO/SEO/速度:邊緣層設定指南

結論先說:內容寫得好不等於被讀到。AI 抓取器不會等前端渲染完成,所以要讓內容被 ChatGPT、Perplexity、Gemini 正確引用,關鍵不在多寫幾個標籤,而在邊緣層先把渲染好的完整 HTML 交到機器人手上。這一頁完整記錄本站的做法,也歡迎其他醫師與架站人員直接套用。

Why

為什麼一定要有邊緣層

結論先說:本站是前端渲染(SPA),伺服器回給爬蟲的初始 HTML 只有一個空容器,正文是瀏覽器執行 JavaScript 後才長出來的。 Googlebot 有能力執行 JavaScript,但多數 AI 抓取器(GPTBot、PerplexityBot、ClaudeBot 等)不會等你渲染完——它們讀到空殼就走了。 這不是內容不好,而是內容根本沒被看到。

  • AI 引擎抓不到正文:無法被摘要、無法被引用,等於在 AI 搜尋時代隱形。
  • 結構化資料被低估:JSON-LD 寫得再完整,若機器人拿到的是空殼頁,價值大幅折損。
  • 平台端無法根治:Base44 只提供前端渲染,也不允許自訂 HTTP 標頭與 /.well-known 檔案;這一層必須由 Cloudflare 補。

Architecture

兩層分工:誰做什麼,不重複

Base44 平台端(已完成)

  • 每頁獨立 canonical、title、description
  • JSON-LD:Person/Physician、Article+citation、FAQPage
  • 文章網址採穩定 slug,不綁資料庫 ID
  • 動態產生 sitemap.xml、llms.txt、ai.txt
  • 文章以 Markdown 結構儲存、可一鍵匯出備份

Cloudflare 邊緣端(你要設定)

  • 機器人偵測與預先渲染,回傳含正文的 HTML
  • 把 /llms.txt、/ai.txt、/sitemap.xml 導到動態端點
  • 邊緣快取與靜態資源長快取
  • CSP 等安全標頭、security.txt
  • 圖片 WebP、Brotli、HTTP/3 等加速選項

Data Flow

請求進來之後發生什麼

STEP 1

邊緣判斷來源

讀 User-Agent,區分 AI 抓取器/搜尋引擎與一般讀者。

STEP 2

機器人走預渲染

呼叫無頭瀏覽器取得渲染後 HTML,含完整正文與 JSON-LD。

STEP 3

結果進邊緣快取

同一網址 24 小時內直接命中,不重複渲染、成本極低。

STEP 4

讀者原樣轉送

真人仍走原本的 SPA 體驗,互動與登入完全不受影響。

注意:預渲染只在「內容一致」的前提下是正當做法——回給機器人的內容必須與讀者看到的相同,否則屬於偽裝(cloaking),會被搜尋引擎懲罰。 本方案回傳的是同一頁渲染後的結果,內容一致。

Step 1

部署 Worker:AI 索引檔與預先渲染

  • Cloudflare 控制台 → Workers & Pages → Create Worker,貼上下方程式碼。
  • Settings → Variables:新增 CF_ACCOUNT_ID 與 CF_API_TOKEN(Browser Rendering 權限)。
  • Settings → Domains & Routes:加上 drkao.org/* 與 www.drkao.org/*。
  • 文章頁與主要頁面走 B-1 的後端鏡像(零成本、含文末互指連結),不需要 Browser Rendering 也能生效;CF_ACCOUNT_ID 與 CF_API_TOKEN 只影響 B-2 的其餘頁面,可暫時留空。
Worker:索引檔導流 + 機器人預渲染 + 邊緣快取
// Cloudflare Worker:drkao.org 邊緣層
// 1) 把 AI 索引檔導到 Base44 動態端點  2) 機器人請求走預先渲染  3) 其餘原樣轉送
const APP = "https://drkao.base44.app";

const DOCS = {
  "/llms.txt": "llms",
  "/ai.txt": "ai",
  "/sitemap.xml": "sitemap",
};

const BOT_RE = /(GPTBot|OAI-SearchBot|ChatGPT-User|PerplexityBot|Perplexity-User|Google-Extended|Googlebot|Bingbot|ClaudeBot|Claude-Web|anthropic-ai|Applebot|Applebot-Extended|Amazonbot|CCBot|YouBot|facebookexternalhit|Twitterbot|LinkedInBot|Slackbot)/i;

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // A. AI 索引檔:由後端動態產生,內容永遠與文章同步
    const doc = DOCS[url.pathname];
    if (doc) {
      const upstream = APP + "/functions/aiFeed?doc=" + doc;
      const res = await fetch(upstream, { cf: { cacheTtl: 1800, cacheEverything: true } });
      const headers = new Headers(res.headers);
      headers.set("Cache-Control", "public, max-age=1800");
      return new Response(res.body, { status: res.status, headers });
    }

    // B. 機器人:回傳預先渲染的完整 HTML(含正文、互指連結與結構化資料)
    const ua = request.headers.get("user-agent") || "";
    if (request.method === "GET" && BOT_RE.test(ua)) {
      const cache = caches.default;
      const key = new Request(url.toString(), { method: "GET" });
      const hit = await cache.match(key);
      if (hit) return hit;

      // B-1. 文章與主要頁面:優先用後端 pageMirror(零成本、含 peer_links 互指連結)
      const mirrored = await fetchMirror(url.pathname);
      if (mirrored) {
        const res = new Response(mirrored, {
          headers: {
            "Content-Type": "text/html; charset=utf-8",
            "Cache-Control": "public, max-age=86400",
            "X-Prerender": "mirror",
          },
        });
        ctx.waitUntil(cache.put(key, res.clone()));
        return res;
      }

      // B-2. 其餘頁面:退回無頭瀏覽器渲染(需 Workers 付費方案)
      const rendered = await renderWithBrowser(url.toString(), env);
      if (rendered) {
        const res = new Response(rendered, {
          headers: {
            "Content-Type": "text/html; charset=utf-8",
            "Cache-Control": "public, max-age=86400",
            "X-Prerender": "1",
          },
        });
        ctx.waitUntil(cache.put(key, res.clone()));
        return res;
      }
    }

    // C. 一般讀者:原樣轉送給 Base44
    return fetch(new Request(APP + url.pathname + url.search, request));
  },
};

// 從後端純文字鏡像端點取得該路徑的完整 HTML(含 peer_links 互指連結)
// mode=edge 表示由邊緣層在正式網址上輸出,此時鏡像頁不會加 noindex
async function fetchMirror(pathname) {
  try {
    const upstream =
      APP + "/functions/pageMirror?mode=edge&path=" + encodeURIComponent(pathname);
    const res = await fetch(upstream, { cf: { cacheTtl: 3600, cacheEverything: true } });
    if (!res.ok) return null; // 404 表示此路徑無鏡像,交給 B-2
    return await res.text();
  } catch (e) {
    return null;
  }
}

// 用 Cloudflare Browser Rendering(需綁定 BROWSER)取得渲染後 HTML
async function renderWithBrowser(target, env) {
  try {
    const res = await fetch(
      "https://api.cloudflare.com/client/v4/accounts/" + env.CF_ACCOUNT_ID + "/browser-rendering/content",
      {
        method: "POST",
        headers: {
          Authorization: "Bearer " + env.CF_API_TOKEN,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ url: target, waitUntil: "networkidle0" }),
      }
    );
    const data = await res.json();
    return data?.result || null;
  } catch (e) {
    return null; // 渲染失敗就退回一般轉送,不讓爬蟲拿到錯誤頁
  }
}
部署後驗證:模擬 AI 爬蟲抓文章,應該看到正文與文末互指連結
curl -A "GPTBot" https://drkao.org/sanctuary/physician-risk-assessment | grep -i "drhao33"

# 若尚未部署 Worker,可先直接測後端鏡像:
curl "https://drkao.base44.app/functions/pageMirror?mode=edge&path=/sanctuary/physician-risk-assessment"

Step 2

驗證 llms.txt 與 ai.txt

這兩個檔案由本站後端動態產生,內容會自動跟著文章更新,不需手動維護。llms.txt 告訴 AI 引擎「哪些是本站權威內容、該怎麼引用」; ai.txt 宣告抓取與引用政策並要求標註來源。sitemap.xml 同樣改為動態,會自動排除會員專屬文章。

Worker 上線後,用瀏覽器或 curl 確認這三個網址都回傳內容
https://drkao.org/llms.txt
https://drkao.org/ai.txt
https://drkao.org/sitemap.xml

# 未設 Worker 前,可先直接檢查後端來源:
https://drkao.base44.app/functions/aiFeed?doc=llms
https://drkao.base44.app/functions/aiFeed?doc=ai
https://drkao.base44.app/functions/aiFeed?doc=sitemap

上線後記得到 Google Search Console 與 Bing Webmaster Tools 重新提交 sitemap 網址。

Step 3

快取與速度設定

Caching → Cache Rules
# Cache Rules(Caching → Cache Rules)
# 規則名稱:Static assets long cache
當 URI Path 符合  /assets/*  或  副檔名 in (js, css, woff2, png, jpg, webp, svg)
→ Cache eligibility: Eligible for cache
→ Edge TTL: 1 month
→ Browser TTL: 1 week

# 規則名稱:HTML short edge cache
當 URI Path 不符合 /functions/*  且  不符合 /account*  且  不符合 /admin*
→ Cache eligibility: Eligible for cache
→ Edge TTL: 2 hours(Respect origin 關閉)
→ Browser TTL: 4 hours
Speed 與 Network 建議開關
Speed → Optimization
  ✅ Auto Minify(JS / CSS / HTML)
  ✅ Brotli 壓縮
  ✅ Early Hints
  ✅ HTTP/3(with QUIC)
  ✅ 0-RTT Connection Resumption
Speed → Image Optimization
  ✅ Polish:Lossy + WebP
Network
  ✅ Tiered Cache(Argo Smart Routing 為付費加值,可選)
  • 不要快取的路徑:/functions/*、/account*、/admin* 必須排除,否則會員與後台資料可能被跨使用者快取。
  • 發文後清快取:Caching → Configuration → Purge Everything,或用 API 針對該篇文章網址做單一清除。
  • 字體:本站已用 display=swap 與 preconnect;若要再快,可把字體自架並做繁中子集化。

Step 4

安全標頭與 security.txt

  • Rules → Transform Rules → Modify Response Header:新增 Content-Security-Policy,內容沿用 index.html 中的那份策略(HTTP 標頭優先於 meta)。
  • 同一處可補上 Permissions-Policy 與 Cross-Origin-Opener-Policy。
  • security.txt:Cloudflare 的 Security → Settings 內建產生器,或在上方 Worker 的 DOCS 表加一筆自訂回應。
  • DNSSEC 可直接在註冊商開啟;CAA 暫緩,避免擋掉 Universal SSL 續簽。

Step 5

長期維運:可引用、可搬遷、少維護

  • 內容可長期被引用:每篇文章有穩定 slug 與自身 canonical,並附正式引用格式;外部引用不會因改版而失效。
  • 降低平台依賴:文章以標準 Markdown 儲存,可一鍵匯出成 .md 檔+網址對應表;換平台時網址結構照舊,連結不斷裂。
  • 維護成本低:sitemap/llms.txt/ai.txt 全部動態生成,新增文章不需要改任何設定檔。
  • 建議節奏:每月看一次 Search Console 的收錄數與 Core Web Vitals;每季匯出一次內容備份。