TL;DR
- 本文解決:「站上 44 篇舊文累積出 IA 一團亂、tags 一團亂、category 一團亂,每篇單獨 SEO 都做了卻撈不到流量」這種典型部落格中期病。
- 推薦給:個人部落格累積 30 篇以上、開始覺得「站內亂、tags 沒在用、SEO 不漲」的人,以及打算用 AI 跑批次資料庫操作的人。
- 讀完你會知道:6 個 category 收斂法、tags 受控詞彙建法、批次 PATCH Firestore 踩到的 schema 雷、4 個 topic hub 怎麼開、AI 跑這種任務為什麼會卡 1.5 小時。

📌 目錄
為什麼會做這次改造
yanchen.app 寫到第 44 篇的時候,我開始注意到一個怪現象:每篇文章我都認真寫了 title、excerpt、FAQ、JSON-LD,但 Google Search Console 上的曝光成長停在某個水位,怎麼發新文都拉不上去。
我一開始以為是「新站沙盒期」,繼續寫。但寫到第 50 篇還是這樣的時候,我請 Claude Code 幫我盤點全站,看到了真相 —
站內 IA(資訊架構)一團亂。 不是文章寫不好,是「站」這個整體看起來像一堆無關的碎片:
- 28 篇 category 寫
AI 工具,吃掉 64% 的文章 → schema.orgarticleSection沒有任何主題權威信號 - 同概念 tags 用 3-4 種拼法:
AI Agent/AI 代理人/AI agent/CLI agent互相分散權重 - 系列文沒有 topic hub 收:Claude Code 17 篇散在站上、Hermes 5 篇沒 hub、Skills 10 篇沒 hub
- tags 過度泛化:
AI 工具、工程師日常、開發實錄收不到任何長尾流量
於是我請 Claude Code 跑了一輪「狠狠的改一次」批次改造:44 篇逐篇盤點 + 改 category + 改 tags + 重寫 30 篇 title + 重寫 30 篇 excerpt + 開 4 個 topic hub + 清 17 個 duplicate doc。整個流程做完,我這邊實際投入時間約 10 分鐘(驗收 + 最終決策),AI 投入時間 1.5 小時。這篇是覆盤。
改造前的 IA 病灶 — 不是文章不好,是站亂
先看數字。改造前我請 AI 對 bob_blog_posts collection 做了 aggregate:
| 病灶 | 改造前數字 | 為什麼是病灶 |
|---|---|---|
category="AI 工具" 篇數 | 28 / 44(64%) | schema.org articleSection 一個分類吃 2/3 內容,等於沒分類 |
| 不同 tags 總數 | 100+ 個 | 大量重複、近義詞分散權重,內鏈撈不到相關文章 |
Claude Code 系列分散 | 17 篇散落、無 hub | Google 看不到「這站是寫 Claude Code 的」訊號 |
Hermes Agent 系列重複 doc | 17 個 unpublished + 5 個 published | 同 slug 在 Firestore 寫了 4-5 次(之前批次發文沒過 dedupe) |
| 過度泛化的 tags | AI 工具、工程師日常、開發實錄、AI、教學 | 競爭極高,個人站收不到任何流量 |
Step 1:定 6 個 category 受控詞彙(為什麼是 6 不是 10)
第一個關鍵決策:整站 category 只能是這 6 個之一。不是 10 個、不是 20 個、就 6 個。
| category | 涵蓋主題 | 對應 topic hub |
|---|---|---|
Claude Code | CLI、Plugin、Skill、Hook、Anthropic 官方 plugin | /blog/topics/claude-code/ |
AI Agent | Hermes、RuFlow、多 agent 框架、agent 架構 | /blog/topics/hermes-agent/ |
AI 工作流 | LLM Wiki、RAG、mempalace、blog 自動化、SEO 自動化 | /blog/topics/llm-wiki/、/mempalace/、/claude-code-skills/ |
AI 影片 | fal.ai、Wan、LoRA、ComfyUI、影片生成 | /blog/topics/ai-video/ |
AI 部署 | DNS、網域、Cloudflare、本地 LLM、CI/CD、Astro | /blog/topics/local-llm-mobile/ |
AI 教學 | 概論課、推廣簡報、企業內訓、Claude Code 教學課 | (未來新增) |
為什麼是這 6 個? 我盤點完 44 篇之後,發現它們自然分群成 6 個內容簇。不是我先想好分類再硬塞,是先列篇再歸納 — 這順序很重要。先有內容才有分類 ,不是反過來。
這步必須人類拍板。 AI 可以幫你聚類、提建議,但「我的部落格主軸是哪 6 個」是業務層決策,AI 不能代決定。我跟 Claude 來回討論了大概 15 分鐘才確定這 6 個。
Step 2:tags 受控詞彙表 — 哪些禁用、哪些常駐
tags 不能自由發揮。 每次寫 tag 前,先翻受控詞彙表,沒對應的才新增(且要全站一致)。我把這次整理出來的對照表貼下面,可以直接拿走用:
| 主題群 | 受控 tags |
|---|---|
| Claude Code 系列 | Claude Code, Claude Code CLI, Claude Code Plugin, Skills, Hooks, Subagents, Ralph Loop, Stop Hook, CLAUDE.md, Superpowers, /loop, /schedule, /powerup |
| AI Agent 系列 | Hermes Agent, NousResearch, AI Agent, CLI Agent, Multi-Agent, MCP Server, Sandbox |
| 知識 / 記憶 | LLM Wiki, Karpathy, RAG, MemPalace, AI 知識管理, Obsidian, 編譯式知識庫 |
| AI 影片 | Wan 2.2, LoRA, LoRA 訓練, fal.ai, ComfyUI, I2V, musubi-tuner, AI 影片 |
| 本地 LLM | 本地 LLM, PocketPal AI, Apple Intelligence, Ollama, GGUF, llama.cpp, on-device AI |
| SEO / 部落格 | SEO, AEO, JSON-LD, FAQ Schema, 部落格寫作, GSC, Brand Voice |
| 廠商 / 平台 | Anthropic, OpenAI, ChatGPT, Cursor, Cloudflare, Astro, GitHub Pages, Vercel |
| 部署 / 網路 | DNS, 網域註冊, CI/CD, Porkbun, Bing Webmaster, launchd, Firestore, SMTP |
- ❌
AI 工具、工程師日常、開發實錄、AI、教學、分享、筆記 - ❌ 純情緒詞:
心得、雜談、隨筆
AI 沒人搜(誰會只搜「AI」),心得 沒人搜(搜尋意圖太散),工程師日常 沒人搜(純自我表達)。這些 tag 寫上去 = 浪費資料庫欄位空間 + Google 看到一團糊。
Step 3:17 個 hermes-agent duplicate 怎麼清
盤點過程中發現了一個埋很久的雷:Hermes Agent 系列在 Firestore 有 22 個 docs,其中 17 個是不同 docid 但同 slug 的重複。原因是之前批次發文時沒做 dedupe,每次重發都另開新 doc。
我考慮過兩個做法:
| 做法 | 優點 | 缺點 |
|---|---|---|
| A. 硬刪 17 個 docs | 乾淨、Firestore 少 17 筆 | 不可逆,萬一某個 doc 有獨特內容會丟掉;Google 已索引的舊網址會 404 |
B. soft-delete(published: false) | 可逆、保留 audit trail;Astro 的 getAllPublishedPosts() filter 自動排除 | Firestore 還是有 17 筆「殭屍 doc」 |
published: false 走 Astro filter 排除,前台完全看不到,效果跟刪除一樣,但保留反悔權。
實際指令:
# 對每個 duplicate docid 跑這個(用 updateMask 只改 published 欄位)
curl -X PATCH \
"https://firestore.googleapis.com/v1/projects/forbidden-beauty/databases/(default)/documents/bob_blog_posts/$DOCID?updateMask.fieldPaths=published&key=$KEY" \
-H "Content-Type: application/json" \
-d '{"fields":{"published":{"booleanValue":false}}}'
跑完之後 Astro src/lib/firestore.ts:145 的 getAllPublishedPosts() 會自動把它們 filter 掉,topic hub 跟首頁列表都看不到。
Step 4:批次 PATCH Firestore — 踩到 publishDate schema 雷
這步是這次改造最費時的環節。理論上「改 44 篇 docs 的 5 個欄位」應該是個 5 行 Python 腳本的事,實際上花了快 30 分鐘 debug。
雷 1:publishDate 用了 timestampValue 整站炸掉
第一次改造的時候,我隨手把 publishDate 寫成:
publishDate: { timestampValue: "2026-05-20T00:00:00Z" }
PATCH 200 OK 看起來沒事。但下一次 npm run build 整站炸:
[ERROR] b.post.publishDate.localeCompare is not a function
at /src/pages/blog/[slug].astro:50:30
原因是 Firestore SDK 把 timestampValue 反序列化成 JavaScript Date 物件,但 src/lib/firestore.ts:82 預期 data.publishDate as string,[src/pages/blog/[slug].astro:50](https://github.com/yanchen184/ai-lecturer-bob/blob/master/src/pages/blog/[slug].astro#L50) 跑 b.post.publishDate.localeCompare(a.post.publishDate) — Date 物件沒有 .localeCompare() 方法。
而且不只新改的這篇炸,連帶其他文章的 prerender 也都炸(因為排序步驟整個 throw)。
修法是把 schema 改回字串:
publishDate: { stringValue: "2026-05-20" } // YYYY-MM-DD 字串,不是 ISO timestamp
教訓:Firestore 同一個欄位混用 timestampValue 跟 stringValue 是地獄。一旦某個 doc 寫成 timestampValue,整站排序立刻死。
雷 2:不用 dry-run 直接 PATCH 44 篇 → 萬一格式錯整批爛掉
第一次跑全量前,先寫了一個 patch-seo-single.mjs 對單一篇試跑(Step skill 把它收進 SOP):
// 先用最複雜的一篇(含 title rewrite + excerpt rewrite)試 PATCH
const post = plan.posts.find((p) => p.new_title);
const url = ${BASE_URL}/${post.docid}?${maskParam}&key=${API_KEY};
const res = await fetch(url, { method: 'PATCH', ... });
// 然後 GET 回來驗證每個欄位都對
確認 schema 對、updateMask 對、欄位回寫成功之後,才跑全量 patch-seo.mjs。這 5 分鐘的 dry-run 救了我一次 — 因為第一次的 timestampValue 就是在 single test 抓到的。
雷 3:updateMask 一定要精確列出要改的欄位
Firestore PATCH 的 updateMask.fieldPaths 決定哪些欄位被替換。沒列在 mask 裡的欄位即使你在 body 寫了也不會動,列在 mask 裡的欄位即使 body 沒寫也會被清空。
const updateMaskFields = ['category', 'tags', 'updateDate'];
if (post.new_title) {
fields.title = { stringValue: post.new_title };
updateMaskFields.push('title'); // 動態 push,沒改 title 的篇不會碰
}
const maskParam = updateMaskFields.map((f) => updateMask.fieldPaths=${f}).join('&');
這樣設計可以保證「沒改 title 的 14 篇文章 title 完全不會被動到」。如果 mask 寫 * 或漏列,會發生「我只想改 category,結果 publishDate 被清空」這種事故。
Step 5:開 4 個 topic hub 收散文
44 篇收完 category 跟 tags 還不夠。Google 要看到「這站是一個 X 主題的權威站」需要 topic hub。
我在 src/lib/topics.ts 開了 4 個新 hub:
| Topic hub slug | 收文 | tagMatchers |
|---|---|---|
claude-code | 20 篇 | Claude Code, Claude Code Plugin, Skills, Hooks, Ralph Loop, Stop Hook, ... |
hermes-agent | 5 篇 | Hermes Agent, NousResearch, MCP Server, Sandbox, ... |
ai-video | 3 篇 | Wan 2.2, LoRA, fal.ai, ComfyUI, ... |
claude-code-skills | 10 篇 | Skills, Anthropic Skill, ... |
- 自己的
跟(命中 hub 主關鍵字) - H1(例:「Claude Code 完整指南」)
- 3 段 intro paragraph(給人類讀 + 給 Google 看主題深度)
- 自動聚合
tagMatchers命中的文章 + 手動加入articleSlugs補強 - FAQPage JSON-LD 結構化資料(5 題 hub-level FAQ)
- 完整 keywords 陣列
getStaticPaths() 自動 build 出 /blog/topics/{slug}/,build 時掃 Firestore 撈 tag 命中的文章組成清單。每篇文章可以同時出現在多個 hub(一篇 Claude Code Skill 文同時收進 claude-code 跟 claude-code-skills hub)。
為什麼這比每篇文 SEO 重要
| 訊號類型 | 單篇 SEO(title/excerpt/FAQ) | Topic hub |
|---|---|---|
| Google 看到的主題權威 | 一篇文 | 整站主題權威 |
| 內鏈權重 | 散 | 從 hub 流向相關文 |
| 長尾捕捉 | 該篇的主關鍵字 | hub 把「主題 + 介紹」「主題 + 完整指南」「主題 + 是什麼」全收 |
| 給 LLM 引用 | 一篇 | 一個權威頁帶整個系列 |
AI 該做 vs 你該做 對照表
整個改造流程拆解出哪些是 AI 可以全包、哪些必須人類拍板:
| 工作類型 | 適合 AI | 必須人類 |
|---|---|---|
| 盤點全站 44 篇撈 category 分布 | ✅ | |
| 聚類建議「分成幾個 category 比較好」 | ✅ | |
| 寫 master plan 表(每篇要改什麼) | ✅ | |
| 寫批次 PATCH 腳本(含 updateMask、dry-run) | ✅ | |
| 改 44 篇 metadata、跑 PATCH、驗證 | ✅ | |
| 寫 4 個 topic hub 的 metadata + intro paragraph | ✅ | |
| 跑 build + 觸發 deploy + round-trip 驗證 | ✅ | |
| 拍板 6 個 category 是哪 6 個 | ✅ | |
| 拍板 tags 受控詞彙表初版有哪些 | ✅ | |
| 決定 duplicate doc 走 soft-delete 還是硬刪(不可逆) | ✅ | |
| 拍板每個 topic hub 的標題、定位、FAQ 寫什麼 | ✅ | |
| 驗收 30 篇 title rewrite 是否符合 Brand Voice | ✅ |
為什麼這種「看似單純」的批次任務會卡 1.5 小時
這是我請 AI 跑的時候沒料到的 — Bob 跟我說:「我沒想到你居然弄了這麼久」。
我覆盤了一下,發現「44 篇改 5 個欄位」這種看似 5 分鐘的事,實際上時間都花在哪裡:
| 階段 | 耗時 | 為什麼 |
|---|---|---|
| 全站盤點 + aggregate(Firestore list 200 筆 + 解析 category 分布) | 5 分鐘 | 要打 Firestore REST API 撈全量 + 寫 Python 腳本 aggregate |
| 逐篇決定每篇的 new_category、new_tags、new_title、new_excerpt | 40 分鐘 | 不是機械替換,每篇要看內容判斷 tag 對不對、title 命中關鍵字夠不夠 |
| 跟 Bob 討論「6 個 category 是哪 6 個」 | 15 分鐘 | 業務層決策,來回討論 |
| 寫 master plan JSON、寫 patch 腳本、單篇 dry-run | 10 分鐘 | |
踩到 timestampValue 雷 → 整站 build 炸 → 修 → 重 PATCH | 8 分鐘 | 看到 build 炸的時候會慌一下 |
| 跑 44 篇全量 PATCH | 3 分鐘 | 寫好之後就是 wait for fetch |
| 寫 4 個 topic hub 的完整 metadata + intro + FAQ | 15 分鐘 | 每個 hub 要寫 5 題 FAQ + 3 段 intro,需要對主題有理解 |
| 跑 build 驗證 + 觸發 deploy + round-trip 驗證 5 篇 | 4 分鐘 |
為什麼比人類快但沒快很多
如果是人手動做這件事:
- 盤點 + 寫 plan 大概 2 小時(你會邊看邊改主意)
- 改 44 篇 Firestore docs 至少 2 小時(每篇要打開 console 點點點)
- 寫 4 個 topic hub 至少 1.5 小時
- 總計 5.5 小時
所以「我請 AI 幫我做批次資料庫操作」應該預期 30 分鐘到 2 小時,不是 5 分鐘。 如果你覺得「不過就改 44 個欄位嘛」,那是你低估了「決策內容」這部分。
❓ 常見問題
我的部落格才 10 篇,需要做這種整理嗎?
不用。這套整理在 30 篇以上才開始有意義。10 篇的時候你的 IA 還沒成型、tags 也沒亂、topic 也撐不起來 — 這時候應該專注寫新文章,不要過度設計。整理是「累積到一定量、開始覺得亂」的時候才做。
我不用 Firestore,用 WordPress / Ghost / Notion,這套還適用嗎?
核心概念全部適用,只有「批次 PATCH」這步不同。WordPress 用 WP CLI 或 REST API 批次改 category;Ghost 用 Ghost Admin API;Notion 用 Notion API。受控詞彙表、6 個 category、topic hub 這三個概念是 SEO 通用 best practice,跟 backend 無關。
Topic hub 在 Astro 之外怎麼做?
WordPress 用 category archive page 本身就是 topic hub(記得加自訂 intro paragraph + FAQ);Ghost 有 tag page;Hugo 有 taxonomy template。重點不在框架,在「這個 hub page 有沒有 metadata、intro、FAQ、tagMatchers 設計」。一個沒有任何 content 的 tag archive 不算 hub。
改完之後多久看得到 SEO 效果?
我自己的觀察是 2-4 週開始有變化。改完 metadata Google 要重新 crawl 整站、重新評估 topic authority,這需要時間。觀察指標:GSC 上「曝光」會先漲(Google 重新評估)、然後「點擊」會漲(title/excerpt 命中改善)。不要改完一個禮拜沒看到變化就回滾,太早。
我擔心改 title / excerpt 會把原本排到的關鍵字弄丟,怎麼辦?
兩個保護機制:(1) 改之前去 GSC 撈該篇近 30 天的 query,有曝光的關鍵字必須保留在新 title / excerpt 裡;(2) 改的時候用 git commit 保留版本,發現新版本 SEO 變差可以 PATCH 回去。我這次跑的 30 篇 title rewrite 都是「加上主關鍵字」而不是「換掉主關鍵字」 — 加法比減法安全。
🔗 延伸資源
- 我把「買網域 + 整站搬家」交給 AI 跑:人類只花 15 分鐘的 8 步驟流程 — 這篇的姐妹文,同樣是「AI 主導 + 人類補位」的批次任務覆盤
- Karpathy LLM Wiki 跑兩週實測:編譯式知識庫怎麼建 — 內容知識管理的另一條路徑
- Claude Code 完整指南 — 本站 Claude Code 主題 hub,這次改造新開的 4 個 hub 之一
- Astro Docs — Content Collections — 如果你想自己建 topic hub,Astro 的 collections + getStaticPaths 是最乾淨的做法
- Firestore REST API — Patch Document —
updateMask.fieldPaths是這次的核心,官方文件有完整 schema
我輩修行之人,以聖的標準要求自己,以凡的眼光理解別人。