建站知識庫:從骨架到上線、被 Google 與 AI 搜尋找到

下次建行銷官網照這份做——每一步做什麼、去哪做、會踩什麼坑、怎麼驗證做對了

範例站 guidant-ai.stg.jedicotech.com TanStack Start+Cloudflare Pages 純靜態 最常踩:Cloudflare zone 層預設會擋 AI 爬蟲

建站知識庫:從骨架到上線、被 Google 與 AI 搜尋找到

給下一個要建官網、但不熟 Cloudflare 與 SEO 的人。每一節都是「要做什麼 → 去哪做 → 會踩什麼坑 → 怎麼驗」。範例值取自 Guidant AI 官網(程式碼在 ~/Projects/Billows/Audit-Manager/guidant-ai-website),換站時把網域、專案名換掉即可。

坑一律三段:症狀(你會看到什麼)→ 原因 → 修法與驗證。指令可直接複製跑;$B 代表站台網址(不帶結尾斜線),先 export B=https://guidant-ai.stg.jedicotech.com。

0. 一頁總覽

骨架 → 內容頁 → SEO 配備 → 截圖 → 部署 → 網域 → 拿 noindex → 關掉 Cloudflare 預設攔截(含 Email 模糊化)→ 填 Web Analytics token → 搜尋引擎登記 → 分析 → 驗收
# 步驟 一句話 誰做 約多久
1 骨架 用 Lovable 生出版型後拉回自己的 git repo,之後不再回 Lovable 改 工程 半天
2 靜態輸出 開 prerender,每個網址產一支 index.html 工程 半天
3 內容頁 文案進 i18n json/Markdown,元件不寫字串 工程 每 5 頁約 1 天
4 SEO 配備 head 五欄、robots、sitemap、JSON-LD、og 圖、llms.txt 工程 2~3 天
5 截圖 從自家系統的 Demo 資料截亮暗兩套,壓 WebP 工程 1~2 天(含建 Demo 資料)
6 部署 Cloudflare Pages 連 GitLab,push 即部署 決策者後台+工程 1 小時
7 網域 Pages 綁自訂網域,VITE_SITE_URL 指它 決策者後台 15 分鐘
8 拿 noindex 刪 _headers 的 X-Robots-Tag: noindex;會改 canonical 的修正(結尾斜線)要先上線 工程 10 分鐘
9 關 zone 層攔截 AI 爬蟲政策、Bot Preference Sync、Email 模糊化三個開關 決策者後台 15 分鐘
10 填 Web Analytics token 帳號層建站點、拿 token 填 Pages 環境變數、重新部署 決策者後台 15 分鐘
11 搜尋引擎登記 Google Search Console 驗證+送 sitemap,Bing 一鍵匯入 有公司帳號的人 30 分鐘
12 分析 感謝頁當轉換、看 referrer、三個後台分工 決策者後台+工程 30 分鐘
13 驗收 用各家爬蟲真實身分抓原始 HTML、schema 驗證、Lighthouse 工程 半天

三個最貴的教訓(後面各節細講):

  1. AI 爬蟲不跑 JavaScript,所以內文一定要在 HTML 裡——用 prerender 出靜態檔,不要做純前端 SPA。
  2. Cloudflare 預設會擋 AI 爬蟲、還會改寫你的 robots.txt 與 email,這三個開關在網域(zone)層,不在 Pages 專案裡,換網域要再關一次。
  3. 測爬蟲要用各家完整的 User-Agent 字串,只寫 -A GPTBot 會假綠。

1. 框架與靜態輸出

為什麼一定要靜態 HTML

Google 會分兩輪:先讀 HTML,之後才排隊跑 JavaScript 渲染;GPTBot、ClaudeBot、PerplexityBot 這些 AI 爬蟲完全不跑 JavaScript。純 React/Vue SPA 給它們的是一個空的 <div id="root">,等於沒有網站。

所以框架要能「build 時把每頁先算好成 HTML」(這叫 prerender/靜態預產生)。TanStack Start、Next.js、Astro、Nuxt 都能做;本站用 TanStack Start,因為 Lovable 產出的骨架就是它,開 prerender 即可,零轉換成本。

Lovable 骨架拉回來之後

  • Lovable 是用對話生網頁的工具,產出 TanStack Start+React+Tailwind 4+shadcn/ui。拉回本機 repo 後所有修改都在 repo 做,不要兩邊改。
  • vite.config.ts 被 @lovable.dev/vite-tanstack-config 包了一層,它已經內建 tanstackStart、react、tailwind、nitro 等 plugin。不要自己再加這些 plugin,會重複載入而壞掉;要加設定就塞進它的 defineConfig({ ... })。
  • 用 npm,不用 bun(Lovable 預設 bun,bun.lock 不必帶回來)。

開 prerender

// vite.config.ts
import { defineConfig } from "@lovable.dev/vite-tanstack-config";

export default defineConfig({
  nitro: { preset: "cloudflare-module" },          // 一定要固定,見下方坑
  tanstackStart: {
    server: { entry: "server" },
    prerender: { enabled: true, crawlLinks: true, autoSubfolderIndex: true },
    pages: [
      ...["/", "/en/", "/demo/thanks/", "/contact/thanks/", "/en/demo/thanks/", "/en/contact/thanks/"]
        .map((path) => ({ path })),
      { path: "/404/", prerender: { outputPath: "/404.html" } },
      { path: "/en/404/", prerender: { outputPath: "/en/404.html" } },
    ],
  },
});
  • npm run build 後產物在 .output/public/,每個網址一支 index.html(autoSubfolderIndex: true → /product/index.html)。這個資料夾就是要部署的東西。
  • crawlLinks: true:從首頁開始順著 <a href> 爬,爬到的頁都會產出。動態路由(例如指南 /resources/guides/$slug)只要清單頁有連到每一篇,就會自動被爬到,不必手列 slug。
  • 沒有任何連結指到的頁(表單感謝頁、404)爬不到,要在 pages 明列。

驗證:

cd ~/Projects/Billows/Audit-Manager/guidant-ai-website
npm run build
find .output/public -name index.html | wc -l     # 應等於 頁數 × 語言數
ls .output/public/404.html .output/public/en/404.html
npx serve -l 5180 .output/public                 # 本機看靜態產物(不是 dev server)

坑:Cloudflare 的 build 機上 prerender 全部 500

  • 症狀:本機 npm run build 正常,push 後 Cloudflare Pages 的 build log 每一頁 prerender 都 500,部署失敗。
  • 原因:nitro(TanStack Start 底下的打包器)會看環境變數猜部署平台。Pages build 機有 CF_PAGES=1,nitro 自動切成 cloudflare-pages preset,伺服器檔改放 dist/_worker.js,prerender 找不到 server 檔。
  • 修法與驗證:vite.config.ts 固定 nitro: { preset: "cloudflare-module" }。本機模擬 build 機:
CF_PAGES=1 npm run build && find .output/public -name index.html | wc -l   # 數字要跟不帶 CF_PAGES 時一樣

坑:明列 pages 之後首頁不見了

  • 症狀:為了感謝頁加了 pages: [...],build 完 .output/public/index.html 消失,全站只剩感謝頁。
  • 原因:一給 pages,prerender 就不再預設從 / 起爬。
  • 修法與驗證:pages 裡一定要放 / 與 /en/(各語系首頁)。ls .output/public/index.html .output/public/en/index.html 都要在。

坑:404 落成 /404/index.html,Pages 不認

  • 症狀:不存在的網址回 200+首頁內容(「軟 404」),搜尋引擎會收到一堆重複首頁。
  • 原因:Cloudflare Pages 找不到檔時,只認站根的 404.html;沒有就把首頁當 SPA 回退頁、回 200。
  • 修法與驗證:路由加 src/routes/404.tsx(+en/404.tsx),在 pages 用 prerender: { outputPath: "/404.html" } 指定落檔位置;404 頁自己加 <meta name="robots" content="noindex">。
curl -s -o /dev/null -w '%{http_code}\n' $B/nonexistent-page/    # 404

坑:Markdown 前置資料套件在瀏覽器端壞掉

  • 症狀:直接開文章頁正常,但從站內點連結換到另一篇時白畫面,console 報 Buffer is not defined。
  • 原因:gray-matter 內部用 Node 的 Buffer。prerender 時跑在 Node 沒事,站內換頁時同一段程式跑在瀏覽器就壞。
  • 修法與驗證:front matter 自己用 regex 切出來、交給 js-yaml 解析。文章用 Vite 的 import.meta.glob('/content/guides/*.md', { query: '?raw', import: 'default', eager: true }) 在 build 時讀進來。驗證方式是 headless 瀏覽器從清單頁點進文章、再點文內連結換篇,console 無錯。

2. SEO 配備清單

每頁 <head> 要有什麼

欄位 範例 說明
title CMMC|Guidant AI(英文 CMMC | Guidant AI) 60 字內、每頁不同
meta description 該頁副標 150 字內
link rel=canonical https://guidant-ai.stg.jedicotech.com/solutions/cmmc/ 告訴搜尋引擎「這頁的正式網址」,含網域、帶結尾斜線
link rel=alternate hreflang zh-TW/en/x-default 三筆 中英互指;x-default 指中文
og:title/og:description/og:url/og:image/og:locale og 圖要絕對網址+og:image:width 1200、og:image:height 630 社群分享卡與部分 AI 引用會抓

做法:寫一支共用函式(本站 src/i18n/index.ts 的 seoTags(pathname)),每支路由的 head() 只傳自己的實際路徑進去,網域從 VITE_SITE_URL 組。不要放在最外層 __root 的 head:TanStack 站內換頁不一定重跑最外層的 head,canonical 會停在上一頁。

站根靜態檔(prebuild 腳本產,不手寫、不入版控)

檔 是什麼 本站腳本
robots.txt 給爬蟲的進出規則。User-agent: * 已放行全部,另外明列 AI 爬蟲是「明確表態」(部分爬蟲先找針對自己的規則) scripts/gen-sitemap.mjs
sitemap.xml 全站網址清單,中英每筆帶三組 hreflang 同上,掃 src/routes 與 content/guides 自動產
llms.txt 給 AI 助理的網站導覽:一段公司介紹+每頁「標題、網址、一句說明」(規範見 llmstxt.org) scripts/gen-llms.mjs
llms-full.txt 全站文案+指南原文攤平成純文字,AI 一次抓完 scripts/gen-llms-full.mjs

robots.txt 明列的 11 個 AI 爬蟲:GPTBot、OAI-SearchBot、ChatGPT-User、ClaudeBot、Claude-User、Claude-SearchBot、PerplexityBot、Google-Extended、Amazonbot、CCBot、meta-externalagent(Claude-Web 是已棄用舊名,不加)。

package.json 掛法:"prebuild": "node scripts/gen-sitemap.mjs && node scripts/gen-llms.mjs && node scripts/gen-llms-full.mjs"。頁清單由腳本掃路由檔產生,新加頁不用改腳本;某頁在 i18n 找不到標題說明時讓 build 直接中止,避免被靜默漏列。占位頁(隱私、條款「內容準備中」)從 llms 排除、sitemap 保留。

JSON-LD 結構化資料

JSON-LD 是放在 <script type="application/ld+json"> 的一段 JSON,用 schema.org 的詞彙告訴搜尋引擎「這頁是什麼」。B2B 軟體官網用這五型:

型別 掛哪 關鍵欄位
Organization 全站(root head) name、alternateName(中英互指)、url、logo、PostalAddress、telephone、email、sameAs;@id 固定 <網域>/#organization,其他型別用 @id 指回
BreadcrumbList 每頁 最後一層等於該頁 canonical;中間層必須是打得開的網址,沒有自己頁面的分組(如「解決方案」「資源」)不列
SoftwareApplication 產品總覽、功能細頁 applicationCategory BusinessApplication;報價制就不填 offers
Article 指南文章 headline、datePublished、dateModified、author、inLanguage
FAQPage 有問答段的頁 每題一組 Question/Answer,答案第一句就是答案

公司資訊集中一支檔(本站 src/lib/site.ts 的 companyFor(lang)),英文頁用英文公司名與地址。

坑:hreflang 頁內與 sitemap 寫法不一致

  • 症狀:Search Console 報 hreflang 衝突,或兩語版互相搶排名。
  • 原因:頁內寫 zh-Hant、sitemap 寫 zh-TW,Google 當成兩個不同語系。
  • 修法與驗證:兩邊用同一個值(本站 zh-TW)。JSON-LD 的 inLanguage 是另一件事,用 zh-Hant 沒問題。
curl -s $B/product/ | /usr/bin/grep -ao 'hrefLang="[^"]*"' | sort -u
curl -s $B/sitemap.xml | /usr/bin/grep -ao 'hreflang="[^"]*"' | sort -u     # 兩邊同一組值

坑:網址沒有結尾斜線,每筆都被 308 轉一次

  • 症狀:sitemap 裡的網址打下去先 308 才 200;canonical 指向一個會轉址的網址,Google 判成訊號衝突。
  • 原因:靜態檔是 /product/index.html,Pages 對 /product 會 308 到 /product/。產網址的程式(sitemap、canonical、hreflang、og:url、llms、站內連結)若沒帶斜線,就和伺服器不一致。
  • 修法與驗證:全部一律帶結尾 /(共用 withSlash()),TanStack Router 設 trailingSlash: "always" 讓站內 Link 自動補。
curl -s $B/sitemap.xml | /usr/bin/grep -o '<loc>[^<]*' | sed 's/<loc>//' \
  | while read u; do c=$(curl -s -o /dev/null -w '%{http_code}' "$u"); [ "$c" = 200 ] || echo "$c $u"; done   # 無輸出才對

坑:og 圖用 webp,LinkedIn 不顯示

  • 症狀:貼到 LinkedIn、部分 LINE 版本沒有預覽圖。
  • 原因:這些平台不支援 webp 當分享圖。
  • 修法與驗證:og 圖出 PNG(1200×630、每張 < 300KB),og:image 指 PNG、絕對網址;webp 可留著。產圖腳本(本站 scripts/make-og.mjs)用 headless Chromium 截一張 HTML。
curl -sI $B/og/home.png | /usr/bin/grep -i 'content-type'       # image/png
curl -s $B/ | /usr/bin/grep -ao 'property="og:image" content="[^"]*"'   # https://… .png

坑:產 og 圖時中文悄悄變成別的字型

  • 症狀:og 圖的中文字形怪、粗細不對,但腳本沒報錯。
  • 原因:Chromium 找不到指定字型時會靜默改用系統字。
  • 修法與驗證:腳本在截圖前 await document.fonts.ready 並檢查 document.fonts.check('700 48px "Noto Sans TC"'),沒載到就丟錯。產完逐張開圖目視。

坑:JSON-LD 裡的文案把 <script> 提早關掉

  • 症狀:某頁 JSON-LD 解析失敗,或頁面後半段被當成文字。
  • 原因:文案含 </script> 字樣,原樣塞進 <script> 會提早結束標籤(也是注入面)。
  • 修法與驗證:輸出前 JSON.stringify(data).replace(/</g, "\\u003c")(本站 src/lib/jsonld.ts 的 jsonLdScriptHtml)。把整頁原始碼貼到 https://validator.schema.org/ 要 0 錯。

坑:hover 才出現的選單,grep prerender HTML 驗不到

  • 症狀:驗收「導覽下拉有六個連結」用 grep 靜態 HTML 得到 0,以為壞了。
  • 原因:下拉選單與手機選單是點開或 hover 才渲染,prerender 的 HTML 裡本來就沒有。
  • 修法與驗證:這類 UI 用 headless 瀏覽器實際 hover/點開後抓連結數,不要 grep 靜態檔。(反過來說:只在 hover 後才出現的連結,爬蟲看不到——重要頁面要另外從頁尾或內文連到。)

坑:grep 壓成一行的 HTML 數不出關鍵字

  • 症狀:grep -c CMMC index.html 得 1 或被判成 binary。
  • 原因:prerender 輸出整頁是一行;grep -c 數的是「行」,中文還可能讓 grep 判成 binary。
  • 修法與驗證:先去標籤再數出現次數:sed 's/<[^>]*>//g' f.html | /usr/bin/grep -ao 'CMMC' | wc -l。

3. 網域、環境變數、noindex

一個變數驅動全部網址

VITE_SITE_URL(例:https://guidant-ai.stg.jedicotech.com)決定 canonical、hreflang、og:url、og:image、sitemap、robots 的 Sitemap 行、llms、JSON-LD 的 url 與 @id。換網域只改這一個值、重新 build。

  • 本機寫在 .env.local(不入版控);Cloudflare Pages 寫在專案環境變數。
  • 沒設就讓 build 直接失敗(throw new Error("缺 VITE_SITE_URL")),否則會默默產出 undefined/product。
  • VITE_ 開頭的變數是 build 時寫死進 HTML,改了後台的值要重新部署才生效。

測試期全站 noindex

上線前不想被收錄,用 public/_headers(Pages 讀這支檔加 HTTP 標頭):

/*
  X-Robots-Tag: noindex
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()

/og/*
  Cache-Control: public, max-age=86400
  • noindex 只擋「收進搜尋結果」,不擋爬蟲讀內文——爬蟲驗收、結構化資料驗證、社群預覽卡都照樣能做。
  • 要開放收錄那天,刪掉 X-Robots-Tag: noindex 那一行(其餘安全標頭留著),push 部署。
  • 感謝頁、404 頁的頁內 <meta name="robots" content="noindex"> 是刻意的,永遠保留。
curl -sI $B/ | /usr/bin/grep -i x-robots-tag      # 測試期有 noindex;開放後應無輸出

只對某個網域 noindex

同一個 Pages 專案綁多個網址(例如 stg 網址+正式網址+*.pages.dev),內容一模一樣,要避免被當重複內容。Cloudflare 官方文件寫明 _headers 支援絕對網址規則,可以依網域分開:

https://:project.pages.dev/*
  X-Robots-Tag: noindex

https://:version.:project.pages.dev/*
  X-Robots-Tag: noindex

https://guidant-ai.stg.jedicotech.com/*
  X-Robots-Tag: noindex

本站尚未實測這個寫法;用之前先在 preview 部署跑下面兩行確認,兩個網址結果不同才算成功。若驗不過,退而用兩個 Pages 專案(同 repo、不同 production branch 或不同 _headers 產生方式)。另外 canonical 一律指正式網址,本身就會把權重集中過去。

curl -sI https://guidant-ai-website.pages.dev/ | /usr/bin/grep -i x-robots-tag     # noindex
curl -sI https://<正式網域>/ | /usr/bin/grep -i x-robots-tag                        # 無輸出

網址裡有 stg 會不會影響排名

不會。Google 不看網址裡的字樣判斷是不是測試站,只看 noindex、內容與連結。用 stg 網址先開放收錄「試水溫」是可行的,代價在換網址那天(下一段)。

拿 noindex 的時機:會改 canonical 的修正(例如第 2 節的結尾斜線)要先上線,再拿 noindex。反過來的話,Google 會先收錄舊的(會被轉址的)網址,之後還要再轉址一輪才收斂。canonical 都穩了,測試網址(含 stg 字樣)直接開放收錄沒問題。

之後換正式網址要付的代價

收錄與排名綁在網址上,不綁帳號也不綁網站內容。換網址時:

  1. 舊網址全部 301 轉到新網址的對應頁(不是都轉首頁)。Cloudflare 可用 zone 的 Redirect Rules(「規則 → 重新導向規則」)整個 hostname 轉,保留路徑。
  2. Search Console 在舊資源用「網址變更」工具通報新網址(需兩邊都已驗證)。
  3. Google 搬移權重要幾週到幾個月;AI 搜尋的引用更慢。期間兩邊都會短暫出現。

4. Cloudflare Pages 設定逐步

Cloudflare Pages 是 Cloudflare 的靜態網站託管:連 git repo,每次 push 自動 build 並部署到全球 CDN,免費額度對官網足夠。不需要 Workers(Workers 是每次請求即時跑程式,官網用不到)。

建專案

  1. Cloudflare 後台 → Workers & Pages → 建立 → Pages 分頁 → 連接到 Git(Connect to Git)。
  2. 授權 GitLab,選 repo(本站 auditmanager/guidant-ai-website)。
  3. 設定:
欄位 本站值 說明
專案名稱 guidant-ai-website 決定備用網址 guidant-ai-website.pages.dev
Production branch feature/FR-139 push 到這支就部署正式;其他 branch 產 preview 網址
Framework preset None 不用選 preset,自己填下面兩欄
Build command npm run build prebuild 會自動先跑
Build output directory .output/public 不是 dist、不是 out
Root directory 留空
  1. 環境變數(設定 → 變數和密碼),Production 與 Preview 兩邊都要填,只填 Production 的話 preview 部署會因缺 VITE_SITE_URL build 失敗:
變數 本站值 必填
VITE_SITE_URL https://guidant-ai.stg.jedicotech.com 必填
NODE_VERSION 22 必填(build 機預設 Node 版本可能太舊)
VITE_CF_BEACON_TOKEN Web Analytics 的 token(第 7 節) 要分析才填
VITE_FORM_KEY 表單服務 key(例 Web3Forms) 要表單寄信才填;留空時表單走 mailto 退路
  1. 儲存並部署。第一次 build 約 2~4 分鐘。

綁自訂網域

專案 → 自訂網域(Custom domains)→ 設定自訂網域 → 輸入 guidant-ai.stg.jedicotech.com。

  • 網域的 DNS 在同一個 Cloudflare 帳號:CNAME 自動建,幾分鐘生效、自動發 SSL 憑證。
  • DNS 不在 Cloudflare:後台會顯示要加的 CNAME(指到 <專案名>.pages.dev),交給管 DNS 的人加。

日常

  • push 即部署:push 到 production branch 自動 build。這代表「push」等於「上線」,要由有權決定上線的人按。
  • Deploy Hook:設定 → 組建 → 部署掛鉤,產生一個 URL,POST 它就重新 build。接 CMS(例如 Payload)時,存檔打這個 URL 讓內容 1~2 分鐘後生效。
  • 回滾:部署清單裡任一筆舊部署 → 「回復到此部署」。
curl -s -o /dev/null -w '%{http_code}\n' https://guidant-ai-website.pages.dev/     # 200
curl -s -o /dev/null -w '%{http_code}\n' $B/                                      # 200
curl -s $B/ | /usr/bin/grep -ao 'rel="canonical" href="[^"]*"'                     # 從 pages.dev 打也該指正式網址

5. Cloudflare zone 層會擋你的預設(最重要的一節)

zone 是 Cloudflare 對「一個網域」的管理單位(例如 jedicotech.com),底下所有子網域共用 zone 的安全設定。下面三個預設都在 zone 層,Pages 專案裡看不到、_headers 也改不動。網站掛到新的網域(新 zone)就要再關一次;同 zone 加子網域則不用。

坑 5-1:AI 爬蟲被 403

  • 症狀:Googlebot、Bingbot 正常 200,但用 ClaudeBot、GPTBot、Amazonbot 的真實 UA 抓會拿到 403,內容是一行純文字(含 blocked 字樣),不是你的網頁。在後台 AI Crawl Control → 網路安全(AI Crawl Control → Security)的爬蟲清單裡,每個爬蟲一列、藍色開關代表「封鎖中」;直接去關開關關不掉,會提示 "being blocked by an existing AI bot policy → Configure AI bot policies"。
  • 原因:Cloudflare 新帳號在 AI 爬蟲政策對「訓練用途」的 AI 爬蟲預設不允許。ClaudeBot、GPTBot、Amazonbot、CCBot 等被歸在訓練類;清單上的開關是政策的結果,不是獨立設定。
  • 修法:左欄 AI Crawl Control → 網路安全(Security)→ 按提示的 Configure AI bot policies(設定 AI 機器人政策),對話框裡有三格:搜尋(Search)、代理(Agents)、訓練(Training)。把訓練改為允許(不封鎖)(Allow)並儲存;搜尋、代理兩格一併確認是允許。回到清單,開關會自動變灰(不再封鎖)。舊版介面路徑是 Security → Bots →「Block AI bots」,關掉。
  • 驗證:用第 8 節腳本跑 13 個 UA,全部 200 且關鍵字次數 > 0。

坑 5-2:robots.txt 被換成 Cloudflare 的版本

  • 症狀:curl $B/robots.txt 看到的不是你寫的,開頭多了 Content-Signal: search=yes, ai-train=no,還 Disallow 了 Amazonbot 等。
  • 原因:AI Crawl Control 的 Bot Preference Sync(又稱 Content Signals/管理 robots.txt)會把 Cloudflare 產的規則蓋在你的 robots.txt 前面。
  • 修法:在坑 5-1 同一個「設定 AI 機器人政策」(Configure AI bot policies)對話框,最下面一格是 Bot Preference Sync(機器人偏好同步),關掉並儲存。網域總覽(Overview)頁右側欄的「管理 AI 機器人存取 → 啟用機器人偏好同步」(Manage AI bot access → Enable bot preference sync)是同一個開關的另一個入口,看到它是「啟用」按鈕代表目前沒開。
  • 驗證:
curl -s $B/robots.txt | /usr/bin/grep -ic 'content-signal'     # 0
curl -s $B/robots.txt | /usr/bin/grep -c '^User-agent:'        # 等於自己寫的數量(本站 12)

坑 5-3:email 變成 [email protected]

  • 症狀:頁面原始碼裡的 email 被換成 [email protected] 加一個 /cdn-cgi/l/email-protection 連結;爬蟲與不跑 JS 的使用者讀不到信箱;React 頁面還丟 hydration 錯誤(React #418:伺服器 HTML 與瀏覽器端渲染結果對不上)。對 CRA 法定的漏洞通報頁來說,信箱讀不到是合規問題。
  • 原因:zone 的「電子郵件位址模糊化」(Email Address Obfuscation,屬 Scrape Shield)預設開啟,在 CDN 邊緣改寫 HTML。
  • 修法:後台 → 該網域 → 左欄 網路安全(Security)→ 設定(Settings)→ 電子郵件位址模糊化(Email Address Obfuscation)關閉。新版介面沒有「Scrape Shield」這個名稱,舊文件寫的 Scrape Shield 就是這一頁。只想對單一網站關,可用 Configuration Rules 針對 hostname 關。
  • 驗證:
curl -s $B/trust/security/ | /usr/bin/grep -ac 'email-protection'    # 0
curl -s $B/trust/security/ | /usr/bin/grep -ao 'security@[a-z.]*'     # 看得到明文信箱

換網域時的檢查單

新網域若在另一個 zone:上面三項全部重做一次。若在同一個 zone 只是新子網域:不用重做,但第 8 節腳本照樣對新網址跑一次。

6. 跟搜尋引擎登記

Google Search Console 是什麼

Google 給網站主的後台:證明「這個網址是我的」之後,可以送 sitemap 請 Google 來抓、看收錄了幾頁、哪些查詢詞帶來曝光與點擊、有沒有抓取錯誤。不登記 Google 也會慢慢找到,登記是讓它快一點、並讓你看得到數據。

驗證網址

  1. https://search.google.com/search-console → 新增資源。
  2. 選 網址前置字元(URL prefix),填 https://guidant-ai.stg.jedicotech.com/(含結尾斜線)。另一種「網域」資源要加 DNS TXT 紀錄、涵蓋整個網域所有子網域,需要管 DNS 的權限;只管一個網站時用網址前置字元較單純。
  3. 驗證方式選 HTML 標記,會給一行 <meta name="google-site-verification" content="…">。
  4. 把它加進全站共用的 head(本站 src/routes/__root.tsx 的 head().meta:{ name: "google-site-verification", content: "…" }),commit、push 部署。
  5. 確認線上有了再回後台按「驗證」:
curl -s $B/ | /usr/bin/grep -ao 'google-site-verification" content="[^"]*"'

這行 meta 要一直留著,拿掉會失去驗證。

送 sitemap

驗證後左側 Sitemap(站點地圖)→ 填 sitemap.xml → 提交。當天「已找到的網址」多半是 0,幾天內會長出來。若站還在 noindex,報表會列「已排除:遭『noindex』標記排除」,這是預期結果。

坑:sitemap 送出後狀態顯示「無法擷取」

  • 症狀:提交當天,sitemap 狀態是「無法擷取」(Couldn't fetch),「上次讀取時間」(Last read)空白。同一份 sitemap 在 Bing 當天就顯示 Success。
  • 原因:Google 的抓取是排程的,還沒輪到;這個狀態在新送的 sitemap 上是常態,不是檔案有錯。
  • 修法與驗證:先確認 Google 真的抓得到,抓得到就不用動,隔一兩天再看:
curl -s -o /dev/null -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' -w '%{http_code} %{content_type}\n' $B/sitemap.xml   # 200 application/xml

要即時確認,用頁面頂端的 網址檢查(URL Inspection)貼 sitemap 網址,按 測試線上網址(Test live URL)。

Bing

  1. https://www.bing.com/webmasters 第一頁是「Welcome」兩欄。左欄 Import from GSC(從 Google Search Console 匯入)按 Import,不要填右邊的手動新增。
  2. 跳 Google 登入授權(用同一個 Google 帳號),勾選要匯入的站,確認匯入;網站與 sitemap 一起帶過來,不用重做驗證。
  3. 成功的樣子:左上站名下拉選單出現該站,頂端提示「up to 48 hours」(資料最多 48 小時才會出現)。
  4. 左側 Sitemaps 會列出匯入的 sitemap,本站當天即顯示 Success、48 URLs discovered。Bing 後台資料反應慢屬正常,數字沒立刻出現不必動。
  5. 左側 AI Performance(Beta) 是 Bing 提供的「網站在 Copilot/AI 搜尋被引用」數據,登記後約兩週再看才有東西。
  6. IndexNow 可以拿一把 key,新頁上線時主動通知 Bing 來抓,不必等它輪到(做法見下一小節)。

ChatGPT 的搜尋功能用 Bing 的索引,所以 Bing 對 AI 搜尋特別重要。

IndexNow:主動推新頁給 Bing

  • key 去哪拿:Bing Webmaster 左側的 IndexNow 點進去是對外介紹站,不是後台功能;key 在該站 Get Started 產生。key 是一串 32 字元的十六進位字串,公開無妨(本來就要放在網站上讓人驗證)。
  • key 檔:站根放一支 public/<key>.txt,內容就是 key 本身(本站 public/a42033575e8a46da956cfc2978fd63f0.txt)。
  • 推送:POST https://api.indexnow.org/indexnow,JSON 欄位 host、key、keyLocation(key 檔的完整網址)、urlList(一次最多 10,000 筆)。本站用 sitemap 的全部 <loc>(48 筆)一次推:
export B=https://guidant-ai.stg.jedicotech.com KEY=a42033575e8a46da956cfc2978fd63f0
urls=$(curl -s $B/sitemap.xml | /usr/bin/grep -ao '<loc>[^<]*' | sed 's/<loc>//' | python3 -c 'import sys,json;print(json.dumps(sys.stdin.read().split()))')
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://api.indexnow.org/indexnow \
  -H 'Content-Type: application/json' \
  -d "{\"host\":\"${B#https://}\",\"key\":\"$KEY\",\"keyLocation\":\"$B/$KEY.txt\",\"urlList\":$urls}"     # 202
  • 坑:先推再放 key 檔也會回 202,看不出失敗
    • 症狀:推送回 202,但 Bing 一直沒收這批網址。
    • 原因:202 只代表收件。Bing 之後才會去抓 keyLocation 驗證你擁有這個網站,key 檔 404 或內容不等於 key,整批作廢,而且不會回頭通知你。
    • 修法與驗證:先部署 key 檔、再推送。推送前確認:
curl -s -o /dev/null -w '%{http_code}\n' $B/$KEY.txt    # 200
curl -s $B/$KEY.txt | grep -c "^$KEY$"                  # 1,內容等於 key
  • 看結果:Bing Webmaster 的 IndexNow 頁會顯示 submitted URLs 與處理狀態,反應慢屬正常。
  • 之後:有新頁或改版時再推;正式上線後掛進部署流程自動推。
  • Google 沒有 IndexNow:只能在 Search Console 用 網址檢查(URL Inspection)→ 要求建立索引(Request indexing)逐頁送。

帳號的常見疑問

  • 一個資源可加多位擁有者/使用者(設定 → 使用者和權限),建議用公司帳號當擁有者,個人帳號加為使用者。
  • 換管理帳號不影響收錄與排名;排名綁的是網址,不是帳號。
  • 換網址等於新資源,要重新驗證、重新送 sitemap(見第 3 節換網址代價)。

結構化資料的 Google 版檢查

https://search.google.com/test/rich-results 要登入、沒有 API,只能手動貼:首頁、一篇指南、一個解決方案頁各一次。它和 schema.org validator 規則不完全相同,Google 對 FAQPage 的顯示資格另有限制(不一定會顯示成摺疊問答),以「沒有錯誤」為準。

7. 分析工具

Cloudflare Web Analytics(預設首選)

  • 不用 cookie,所以不需要 cookie 同意橫幅;免費;看得到瀏覽數、來源網站(referrer)、國家、裝置、Core Web Vitals。
  • 開法:入口在帳號層左欄 Observability → Web Analytics(不在網域底下,也不在 Analytics & Logs)→ 新增網站(Add a site)→ 輸入 Pages 的自訂網域 → 按完成(Done)→ 複製 snippet 裡 data-cf-beacon 的 token。這個 token 是公開的,可以出現在 HTML;填進 Pages 環境變數時類型選「文字」(Text)即可,不必用「密碼」。
  • 填 Pages 自訂網域時會跳出「此主機名稱不屬於 Cloudflare 帳戶中的任何網站,必須安裝 JS 程式碼片段」(This hostname is not part of any site in your Cloudflare account; JS snippet required)。這是正常的:Pages 自訂網域不算 zone 的 proxy 主機,不能「自動設定」,只能走 snippet。列表上主網域(本站是 jedicotech.com)那筆「自動設定」是另一個站的,不要混用它的 token。
  • 掛法:token 放 Pages 環境變數 VITE_CF_BEACON_TOKEN,在 root head 只在「正式 build 且有 token」時載入 beacon:
const token = import.meta.env["VITE_CF_BEACON_TOKEN"] as string | undefined;
const cfBeacon = import.meta.env.PROD && token
  ? [{ src: "https://static.cloudflareinsights.com/beacon.min.js", defer: true,
       "data-cf-beacon": JSON.stringify({ token, spa: true }) }]
  : [];

spa: true 讓它攔截站內換頁,每次換頁算一次瀏覽。填完 token 要重新部署(VITE_ 變數是 build 時寫死的)。

curl -s $B/ | /usr/bin/grep -ao 'static.cloudflareinsights.com/beacon[^"]*'    # 有輸出才算掛上

轉換怎麼算:感謝頁

Web Analytics 不支援自訂事件。做法是表單送出成功後導到真的感謝頁(/demo/thanks/、/contact/thanks/),後台看這兩頁的瀏覽數就是轉換數。感謝頁加頁內 noindex、不進 sitemap、在 prerender pages 明列。

坑:用 pushState 假造虛擬路徑記轉換

  • 症狀:表單送出後 history.pushState 到 /demo/thanks 想讓分析記一筆,結果畫面變 404。
  • 原因:TanStack Router 會攔截 pushState 當成真的換頁,沒有對應路由就渲染 404。
  • 修法與驗證:做成真的路由頁。重新整理感謝頁也要正常顯示。

GA4 什麼時候才需要

要做廣告投放(Google Ads 轉換追蹤、再行銷名單)、要細到按鈕點擊的自訂事件、要漏斗分析時才上 GA4。代價是要 cookie 同意橫幅(歐盟訪客)、設定複雜。純官網曝光與詢問量,Web Analytics+感謝頁就夠。

誰來爬我們網站:各後台的分工

後台 位置 看的是什麼 視角
AI Crawl Control → 網路安全(Security)清單 zone 層左欄 每家 AI 爬蟲來了幾次、被放行還是封鎖 爬蟲
Web Analytics 的 Referrers 帳號層 Observability 從 ChatGPT、Perplexity 等 AI 平台點連結進站的真人 人
Search Console(以及 Bing 的 Sitemaps/AI Performance) search.google.com/bing.com/webmasters 搜尋引擎收錄了幾頁、哪些查詢詞帶來曝光 Google/Bing
Bing Webmaster 的 IndexNow 頁 bing.com/webmasters 左側 我們主動推送的網址(submitted URLs)與處理狀態 我們推給 Bing

各後台互補不重疊:爬蟲來了不代表被引用,被引用不代表有人點,有人點才會出現在 Referrers。

「AI SEO 成效」怎麼看

沒有任何工具能直接告訴你「ChatGPT 推薦了你幾次」。只能拼三個間接訊號:

  1. AI 平台帶來的流量:Web Analytics 的 Referrers 看 chatgpt.com、perplexity.ai、claude.ai、copilot.microsoft.com、gemini.google.com。
  2. Bing 收錄數:Bing Webmaster 的頁面收錄(ChatGPT 搜尋靠它)。
  3. 每月手動問 AI:固定一組題目(例:「CMMC 合規管理工具推薦」「台灣 ISO 27001 稽核軟體」),在 ChatGPT、Perplexity、Claude、Gemini 各問一次,記錄有沒有提到、引用哪一頁。

對外貼的連結一律帶 utm_source/utm_medium,才分得出是自己投放還是自然引用。

8. 怎麼驗證「爬得到」

原則

  • 打線上網址、抓原始 HTML,不是用瀏覽器看(瀏覽器會跑 JS,爬蟲不會)。
  • 用各家完整的 UA 字串。Cloudflare 的 AI 爬蟲規則認的是完整 UA 樣式;curl -A GPTBot 這種裸名字它認不出來,就當一般訪客放行 → 你看到 200,真的 GPTBot 卻被 403。這就是「假綠」。
  • 數關鍵字前先去掉 <script> 與標籤,確認是內文裡有,不是 JSON-LD 裡有。
  • 從你的電腦用真 UA 測的是「UA 規則」;Cloudflare 另有以 IP 驗證真假爬蟲的機制,本機模擬不到。上線一週後到 AI Crawl Control 的流量頁看各爬蟲實際請求與狀態碼,作為最終確認。

13 個爬蟲 UA 全文(2026-10,以各家官方頁為準,改版時更新)

名稱 用途 UA 字串
GPTBot OpenAI 訓練 Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)
OAI-SearchBot ChatGPT 搜尋索引 Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot)
ChatGPT-User 使用者叫 ChatGPT 開網頁 Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot
ClaudeBot Anthropic 訓練 Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
Claude-User 使用者叫 Claude 開網頁 Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-User/1.0; +Claude-User@anthropic.com)
Claude-SearchBot Claude 搜尋索引 Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-SearchBot/1.0; +Claude-SearchBot@anthropic.com)
PerplexityBot Perplexity 索引 Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)
Amazonbot Amazon(Alexa 等) Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Amazonbot/0.1; +https://developer.amazon.com/support/amazonbot) Chrome/119.0.6045.214 Safari/537.36
CCBot Common Crawl(多家模型的訓練來源) CCBot/2.0 (https://commoncrawl.org/faq/)
meta-externalagent Meta AI meta-externalagent/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)
Googlebot Google 搜尋 Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Bingbot Bing 搜尋(ChatGPT 搜尋也靠它) Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)
Applebot Apple(Siri、Spotlight) Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot)

Google-Extended 不是獨立爬蟲(它是 robots.txt 裡控制 Gemini 訓練的代號,實際抓取用 Googlebot),所以不在測試清單。官方 UA 頁:OpenAI https://platform.openai.com/docs/bots 、Anthropic https://support.anthropic.com/en/articles/8896518 、Perplexity https://docs.perplexity.ai/guides/bots 。

爬蟲檢查腳本

存成 check-crawlers.sh,用法 bash check-crawlers.sh <站台網址> <路徑> <關鍵字>:

#!/usr/bin/env bash
# 例:bash check-crawlers.sh https://guidant-ai.stg.jedicotech.com /solutions/cmmc/ CMMC
B=${1:?站台網址}; P=${2:-/}; KW=${3:-Guidant}
GREP=/usr/bin/grep
while IFS='|' read -r name ua; do
  [ -z "$name" ] && continue
  body=$(curl -s -A "$ua" -w '\n%{http_code}' "$B$P")
  code=${body##*$'\n'}; html=${body%$'\n'*}
  n=$(printf '%s' "$html" | sed 's/<script[^>]*>[^<]*<\/script>//g; s/<[^>]*>//g' | $GREP -ao "$KW" | wc -l | tr -d ' ')
  blocked=$(printf '%s' "$html" | head -c 300 | $GREP -aic 'blocked\|access denied')
  printf '%-20s %s  %s=%s %s\n' "$name" "$code" "$KW" "$n" "$([ "$blocked" != 0 ] && echo '← 被擋')"
done <<'UAS'
GPTBot|Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)
OAI-SearchBot|Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot)
ChatGPT-User|Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot
ClaudeBot|Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
Claude-User|Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-User/1.0; +Claude-User@anthropic.com)
Claude-SearchBot|Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-SearchBot/1.0; +Claude-SearchBot@anthropic.com)
PerplexityBot|Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)
Amazonbot|Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Amazonbot/0.1; +https://developer.amazon.com/support/amazonbot) Chrome/119.0.6045.214 Safari/537.36
CCBot|CCBot/2.0 (https://commoncrawl.org/faq/)
meta-externalagent|meta-externalagent/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)
Googlebot|Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Bingbot|Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)
Applebot|Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15 (Applebot/0.1; +http://www.apple.com/go/applebot)
UAS

合格標準:13 行全部 200、關鍵字 > 0、沒有「← 被擋」。中英各挑一頁內容頁跑(例 /solutions/cmmc/、/en/solutions/cmmc/)。

整站健檢腳本

存成 site-check.sh,用法 bash site-check.sh <站台網址>:

#!/usr/bin/env bash
B=${1:?站台網址,不帶結尾斜線}
G="/usr/bin/grep -a"
echo "== 標頭"
curl -sI "$B/" | $G -i 'x-robots-tag\|x-frame-options\|x-content-type'
echo "== robots.txt(數 User-agent;Content-Signal 應為 0)"
curl -s "$B/robots.txt" | $G -c '^User-agent:'
curl -s "$B/robots.txt" | $G -ic 'content-signal'
curl -s "$B/robots.txt" | $G '^Sitemap:'
echo "== sitemap 每筆狀態碼(只印非 200)"
urls=$(curl -s "$B/sitemap.xml" | $G -o '<loc>[^<]*' | sed 's/<loc>//')
echo "共 $(echo "$urls" | wc -l | tr -d ' ') 筆"
for u in $urls; do c=$(curl -s -o /dev/null -w '%{http_code}' "$u"); [ "$c" = 200 ] || echo "$c $u"; done
echo "== 首頁 head"
home=$(curl -s "$B/")
echo "$home" | $G -o 'rel="canonical" href="[^"]*"'
echo "$home" | $G -o 'hrefLang="[^"]*" href="[^"]*"'
echo "$home" | $G -o 'property="og:image" content="[^"]*"'
echo "JSON-LD 段數:$(echo "$home" | $G -o 'application/ld+json' | wc -l | tr -d ' ')"
echo "== 404"
curl -s -o /dev/null -w '/nonexistent-page/ → %{http_code}(應 404)\n' "$B/nonexistent-page/"
echo "== email 沒被 Cloudflare 改寫(應 0)"
curl -s "$B/" "$B/contact/" | $G -c 'email-protection'
echo "== llms"
curl -s -o /dev/null -w 'llms.txt %{http_code}\n' "$B/llms.txt"
curl -s -o /dev/null -w 'llms-full.txt %{http_code}\n' "$B/llms-full.txt"

其他驗證

驗什麼 怎麼驗
結構化資料 https://validator.schema.org/ 貼網址或整頁原始碼,0 錯 0 警告;Google 版見第 6 節
社群預覽卡 用平台爬蟲 UA 抓 og 標籤(下方指令);要看實際渲染再登入 Facebook Sharing Debugger、LinkedIn Post Inspector
效能與無障礙 Lighthouse(npx lighthouse $B/ --only-categories=performance,seo,accessibility --form-factor=mobile --chrome-flags=--headless),目標手機 Performance ≥ 85;noindex 期間 SEO 分數只扣 is-crawlable 一項是正常
手機版不爆版 headless 390 寬開每頁,document.documentElement.scrollWidth - innerWidth 要 0
for ua in 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)' 'LinkedInBot/1.0 (compatible; Mozilla/5.0; +http://www.linkedin.com)' 'Twitterbot/1.0' 'Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)'; do
  curl -s -A "$ua" $B/ | /usr/bin/grep -ao 'property="og:\(title\|image\)" content="[^"]*"' | head -2; echo; done

坑:首頁手機 LCP 太慢

  • 症狀:Lighthouse 手機 Performance 70 幾,lcp-lazy-loaded 不及格。
  • 原因:首屏主圖有亮暗兩張,只有亮色那張 loading="eager";手機量測抓到的最大元素是 loading="lazy" 的那張。
  • 修法與驗證:首屏主圖亮暗都 loading="eager"+fetchpriority="high"。同時檢查標題層級不跳級(h1 後直接 h3 會扣無障礙分)。重跑 Lighthouse 手機版確認。

9. 多人/多 session 共用工作樹的紀律

多個人或多個 Claude session 同時在同一個 repo 資料夾改不同頁,最容易出事的是共用檔:src/i18n/zh-TW.json、en.json、SiteHeader.tsx、src/i18n/index.ts、vite.config.ts。

坑 9-1:整檔 git add 帶走別人的改動

  • 症狀:你的 commit 裡多了一段不是你寫的 i18n key;對方 commit 時發現自己的改動「已經被 commit 了」,而且在別人的 commit message 裡完全沒提。
  • 原因:共用工作樹裡,別人還沒 commit 的 hunk 就在同一支檔裡,git add <檔名> 會整檔帶走。
  • 修法與驗證:
git status --short                 # 先看有沒有不是自己的改動
git diff src/i18n/zh-TW.json       # add 共用檔前一定看
git add -p src/i18n/zh-TW.json     # 有別人的 hunk 就逐段挑,只 y 自己的
git diff --cached --stat           # commit 前再看一次 staged 範圍

禁止 git add -A、git commit -am。

坑 9-2:別人改一半,整站 build 失敗

  • 症狀:你的頁面沒問題,但 npm run build 在別人的元件報 undefined.map。
  • 原因:另一個 session 改到一半(例如 json 改了 key 名,元件還沒跟上)。
  • 修法與驗證:不要替別人修。用乾淨的 worktree 只帶自己的改動驗:git worktree add ../site-verify HEAD,把自己的檔複製過去 npm install && npm run build,驗完 git worktree remove ../site-verify。

坑 9-3:一個 session 的修改把另一個的 head 函式蓋掉

  • 症狀:加 JSON-LD 的人改了 headFrom(),把加 canonical 的人寫的 seoTags() 呼叫拿掉,全站 canonical 消失。
  • 原因:兩邊同時改同一個共用函式,後 commit 的人用自己的舊版本覆蓋。
  • 修法與驗證:改共用函式前 git log -3 -- <檔> 看最近誰動過、先 pull 到最新;commit 前 build 一次並確認別人的成果還在:
for f in $(find .output/public -name index.html); do /usr/bin/grep -aq 'rel="canonical"' "$f" || echo "MISSING $f"; done

坑 9-4:macOS 的 sed -E -i 產生怪檔

  • 症狀:跑了 sed -i -E 's/…//' *.md,內容沒變,資料夾多出一堆 *.md-E。
  • 原因:macOS 內建的是 BSD sed,-i 後面一個參數是備份檔副檔名,-E 被吃掉當副檔名。
  • 修法與驗證:用 sed -i '' -E 's/…//' file,或改用 python/perl(perl -pi -e 's/…//' file)。改完 git status 確認沒有多出 -E 結尾的檔。

坑 9-5:這台機器的 grep 把 HTML 當 binary

  • 症狀:grep -q canonical index.html 全部失敗,驗收迴圈把每頁都印成 MISSING。
  • 原因:shell 把 grep 包成函式/別名,加了會把含特殊字元的單行大檔判成 binary 的選項。
  • 修法與驗證:驗收一律寫 /usr/bin/grep -a。type grep 可看出是不是被包過。

坑 9-6:已 staged 的 rename 還原不乾淨

  • 症狀:想丟掉一批實驗改動,git checkout -- .+git clean 之後還有殘留。
  • 原因:已經 git add 的 rename 不受 checkout -- . 影響。
  • 修法與驗證:這是破壞性操作,只在確認沒有別的 session 在這個工作樹時才做;先 git status 看清楚、必要時先問人。真的要整個回到某個 commit 才用 git reset --hard <commit>。

10. 正式上線 checklist

從測試網址(或試水溫網址)換到正式網址,照順序做。打勾前每項都跑對應的驗證指令。

A. 程式(工程,一個 commit)

B. Cloudflare 後台(決策者)

C. 線上驗證(工程)

export B=https://<正式網域>
curl -sI $B/ | /usr/bin/grep -ic x-robots-tag                            # 0
curl -s $B/sitemap.xml | /usr/bin/grep -c '<loc>'                         # 等於頁數 × 語言數
curl -s $B/sitemap.xml | /usr/bin/grep -c 'stg\.'                          # 0,舊網域沒殘留
curl -s $B/product/ | /usr/bin/grep -ao 'rel="canonical" href="[^"]*"'    # 新網域、結尾 /
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://<舊網域>/product/   # 301 → 新網域同路徑
bash check-crawlers.sh $B /solutions/cmmc/ CMMC                           # 13 行全 200
bash site-check.sh $B

D. 搜尋引擎(有帳號的人)

E. 站外訊號(行銷,正式網址定了才做)

AI 搜尋判斷「推不推薦你」很看別人怎麼說你。公司名、地址、電話、產品名在各平台一字不差:

F. 上線一週後