&lt;?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Stan Wu 吳信典</title><link>https://blog.stanwu.org/tags/static-site/</link><description>拆解被包裝過的東西、數位自主權實踐、非典型理財觀</description><language>zh-TW</language><lastBuildDate>Sun, 12 Apr 2026 09:08:40 +0800</lastBuildDate><atom:link href="https://blog.stanwu.org/tags/static-site/feed.xml" rel="self" type="application/rss+xml"/><item><title>從 WordPress 遷移到 Hugo 的水深比想像的還深</title><link>https://blog.stanwu.org/posts/wp-to-hugo-migration-is-deeper-than-expected/</link><pubDate>Sun, 12 Apr 2026 06:30:00 +0800</pubDate><guid>https://blog.stanwu.org/posts/wp-to-hugo-migration-is-deeper-than-expected/</guid><description>&lt;p&gt;把部落格從 WordPress 搬到 Hugo，一開始看起來像是一個很乾淨的工程題：資料匯出、Markdown 轉換、theme 改寫、build、deploy。&lt;/p&gt;
&lt;p&gt;真正做完之後才發現，水比想像的深。&lt;/p&gt;
&lt;h2 id="為什麼要從-wordpress-遷移到-hugo"&gt;為什麼要從 WordPress 遷移到 Hugo？&lt;/h2&gt;
&lt;p&gt;主要有兩個考量。&lt;/p&gt;
&lt;p&gt;第一個是長期成本。&lt;/p&gt;
&lt;p&gt;WordPress 很方便，但它本質上是一套動態系統。你需要 PHP、資料庫、外掛、登入後台、更新維護、備份策略，也要面對源源不絕的掃描與攻擊。即使網站流量不大，只要公開在網路上，&lt;code&gt;wp-login.php&lt;/code&gt;、&lt;code&gt;xmlrpc.php&lt;/code&gt;、舊外掛漏洞掃描就不會停止。&lt;/p&gt;
&lt;p&gt;對一個主要用來寫文章的個人 Blog 來說，這些成本有點重。&lt;/p&gt;
&lt;p&gt;Hugo 則剛好相反。文章變成 Markdown，網站變成 static rendered pages。部署出去的是 HTML、CSS、圖片與少量 JavaScript。沒有資料庫，沒有 WordPress 後台，也沒有 PHP runtime。從長期角度看，hosting 成本可以大幅下降，攻擊面也小很多。&lt;/p&gt;
&lt;p&gt;第二個是寫作流程。&lt;/p&gt;
&lt;p&gt;我平常很多內容都先在 Obsidian 裡寫。理想流程不是登入 WordPress 後台慢慢貼文，而是讓 AI agent 從 Obsidian 筆記提取內容，自動整理成 Blog 文章，並在發布前做 body clean。&lt;/p&gt;
&lt;p&gt;這裡的 body clean 很重要。筆記裡可能混到帳號、密碼、token、內部路徑、暫存資訊或其他不該公開的內容。公開 Blog 文章必須先經過清理，敏感資訊要遮蔽或移除。&lt;/p&gt;
&lt;p&gt;簡單說，我想要的是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart LR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[Obsidian 筆記] --&amp;gt; B[AI agent 整理內容]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B --&amp;gt; C[Body clean 移除敏感資訊]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; C --&amp;gt; D[Hugo Markdown]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; D --&amp;gt; E[Static Blog]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;原因也很誠實：我很懶惰。&lt;/p&gt;
&lt;p&gt;能讓工具自動處理的事情，就不要靠自己每次手動複製貼上。&lt;/p&gt;
&lt;h2 id="一開始真的很順"&gt;一開始真的很順&lt;/h2&gt;
&lt;p&gt;第一階段幾乎沒有太大阻礙。&lt;/p&gt;
&lt;p&gt;我請 Claude 從 WordPress MySQL export 裡把文章匯出成 &lt;code&gt;.md&lt;/code&gt; 檔案。WordPress 的 &lt;code&gt;wp_posts&lt;/code&gt; 裡有文章標題、日期、slug、正文內容，這些資料都能轉成 Hugo front matter 與 Markdown body。&lt;/p&gt;
&lt;p&gt;接著把原本 WordPress theme 的視覺與結構轉成 Hugo layout。這一步雖然要重寫 template，但 Hugo 的模型很清楚：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content/posts/*.md
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;layouts/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;static/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;config/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文章清單、單篇文章、RSS、sitemap、Open Graph、JSON-LD schema 都可以逐步補上。新版 Blog 也順手加了一些自己的改進，例如 SEO/AEO metadata、FAQ schema、&lt;code&gt;llms.txt&lt;/code&gt;、OG image fallback、AI crawler 相關設定。&lt;/p&gt;
&lt;p&gt;表面上看起來，遷移完成。&lt;/p&gt;
&lt;p&gt;直到我打開 Cloudflare。&lt;/p&gt;
&lt;h2 id="問題是在-cloudflare-ai-crawl-control-發現的"&gt;問題是在 Cloudflare AI Crawl Control 發現的&lt;/h2&gt;
&lt;p&gt;今天查看 Cloudflare 的紀錄時，我在 AI Crawl Control 頁面看到一堆 &lt;code&gt;3xx&lt;/code&gt;、&lt;code&gt;4xx&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;一開始直覺是：是不是 Cloudflare bot 設定又有什麼奇怪問題？&lt;/p&gt;
&lt;p&gt;但仔細看 path 之後答案很明顯。&lt;/p&gt;
&lt;p&gt;大量錯誤都是舊 WordPress URL，例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/2016/01/04/sublime-text-%E8%88%87-simplenote-%E5%85%B1%E8%88%9E
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/2016/01/22/%E7%84%A1%E6%B3%95%E7%A7%BB%E9%99%A4-dropbox-%E7%9A%84-mac-%E7%89%88%E6%9C%AC
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/2015/03/18/digitalocean-%E6%87%B6%E4%BA%BA%E4%B8%80%E9%8D%B5%E5%BF%AB%E9%80%9F%E5%AE%89%E8%A3%9D-wordpress
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;沒錯，就是您想的那樣。&lt;/p&gt;
&lt;p&gt;原本的 WordPress permalink 全部消失了。&lt;/p&gt;
&lt;p&gt;Hugo 新文章 canonical URL 長這樣。因為 slug 裡有中文，canonical 會以 URL encoded 形式呈現：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/posts/sublime-text-%E8%88%87-simplenote-%E5%85%B1%E8%88%9E/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但舊搜尋結果、外部連結、AI crawler 記錄、書籤、歷史文章引用，仍然都在打舊網址：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/yyyy/mm/dd/post-slug
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;mark&gt;&lt;strong&gt;文章內容成功搬家，不代表 URL 權重和歷史連結也一起搬家。&lt;/strong&gt;&lt;/mark&gt;&lt;/p&gt;
&lt;p&gt;這就是 migration 很容易被低估的地方。&lt;/p&gt;
&lt;h2 id="先界定5xx4xx3xx-各自代表什麼"&gt;先界定：5xx、4xx、3xx 各自代表什麼&lt;/h2&gt;
&lt;p&gt;處理前先分級。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;5xx&lt;/code&gt; 是伺服器錯誤。如果有 &lt;code&gt;500&lt;/code&gt;、&lt;code&gt;502&lt;/code&gt;、&lt;code&gt;503&lt;/code&gt;、&lt;code&gt;504&lt;/code&gt;，代表網站本身不穩，這要最先處理。&lt;/p&gt;
&lt;p&gt;這次 5xx 沒有明顯問題，所以往下看。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;4xx&lt;/code&gt; 是找不到或不能存取。WordPress 搬 Hugo 後，最常見就是舊文章 URL 變成 &lt;code&gt;404&lt;/code&gt;。這會傷使用者體驗，也會讓搜尋引擎慢慢把舊 URL 當成失效頁。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;3xx&lt;/code&gt; 是 redirect。它本身不是錯誤，但要檢查有沒有：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redirect chain
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redirect loop
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;301 後最後落到 404
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;302 應該改 301
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;導到錯誤 canonical host
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以這次策略是：&lt;/p&gt;
&lt;blockquote class="callout callout-note"&gt;
&lt;div class="callout-title"&gt;Migration 收尾順序&lt;/div&gt;
&lt;div class="callout-body"&gt;
&lt;ol&gt;
&lt;li&gt;確認沒有 5xx&lt;/li&gt;
&lt;li&gt;先修舊文章 4xx&lt;/li&gt;
&lt;li&gt;再 audit 3xx redirect chain&lt;/li&gt;
&lt;li&gt;圖片與低價值路徑最後處理&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/blockquote&gt;&lt;h2 id="匯出-cloudflare-錯誤-csv"&gt;匯出 Cloudflare 錯誤 CSV&lt;/h2&gt;
&lt;p&gt;Cloudflare 可以把 path 報表匯出成 CSV。這一步非常重要，因為不要憑感覺修 redirect，要看真實 crawler 打到哪些 URL。&lt;/p&gt;
&lt;p&gt;我匯出了兩份資料：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4xx path report
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3xx path report
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;CSV 欄位大致像這樣：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-csv" data-lang="csv"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;referralTraffic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;bytesTransferred&lt;/span&gt;&lt;span class="p"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這樣就能用實際請求數排序，先處理最常被打到的舊連結。&lt;/p&gt;
&lt;p&gt;而從這一步往後，後面那些整理 CSV、比對 WordPress 與 Hugo、產生 redirect 清單、檢查 redirect chain 的工作，基本上都是我先把規則講清楚，再交給 AI agent 協助批次處理。我自己主要負責定義判斷邏輯、看輸出結果、抽查關鍵案例，避免整件事變成手動土法煉鋼。&lt;/p&gt;
&lt;h2 id="用-wordpress-sql-和-hugo-文章做批次比對"&gt;用 WordPress SQL 和 Hugo 文章做批次比對&lt;/h2&gt;
&lt;p&gt;手動一條一條對會瘋掉，所以我先把比對規則定清楚，再讓 AI agent 幫我做一個小工具來批次比對。&lt;/p&gt;
&lt;p&gt;資料來源有兩邊。&lt;/p&gt;
&lt;p&gt;第一邊是 WordPress MySQL export。重點是 &lt;code&gt;wp_posts&lt;/code&gt;，主要欄位包括：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ID
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;post_date
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;post_name
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;post_title
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;post_type
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;post_status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第二邊是 Hugo 文章目錄：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;content/posts/*.md
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hugo front matter 裡有：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;title
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;date
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;slug
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;比對策略是保守的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 先用日期比對
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 再比對 WordPress post_name 和 Hugo slug
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 再比對 WordPress title 和 Hugo title
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. 高信心才自動產生 redirect
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. 低信心標成 manual
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這次 WordPress SQL 裡抓到 40 筆 published posts/pages，Hugo 文章有 53 篇。比對結果：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;high: 37
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;manual: 3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;接著再把 Cloudflare 4xx CSV 交給 AI agent 一起處理，只針對實際出現的錯誤 URL 產生 redirect 清單。&lt;/p&gt;
&lt;p&gt;結果分類如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;high: 30
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;page_index: 6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;manual: 23
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;asset_missing: 49
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這裡的 &lt;code&gt;asset_missing&lt;/code&gt; 大多是舊 WordPress 圖片：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/wp-content/uploads/...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因為 repo 裡沒有那些舊圖檔，所以我沒有讓 AI agent 直接亂導。圖片 404 和文章 404 的 SEO 價值不同，先修文章比較重要。&lt;/p&gt;
&lt;h2 id="為什麼不用-javascript-redirect"&gt;為什麼不用 JavaScript redirect？&lt;/h2&gt;
&lt;p&gt;Static HTML 當然可以用 JavaScript 讀 URL，然後 &lt;code&gt;location.replace()&lt;/code&gt; 導到新文章。&lt;/p&gt;
&lt;p&gt;但這只適合補救真人使用者。&lt;/p&gt;
&lt;p&gt;對 SEO、crawler、AI bot 來說，最好還是 HTTP 層的 &lt;code&gt;301&lt;/code&gt;。也就是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart LR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[舊網址] --&amp;gt; B[HTTP 301]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B --&amp;gt; C[新網址]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;而不是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart LR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[舊網址] --&amp;gt; B[200 HTML]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B --&amp;gt; C[JavaScript redirect]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我先確認過遠端 web server 是 nginx，不是 Apache，所以 &lt;code&gt;.htaccess&lt;/code&gt; 不會生效。nginx 可以做 redirect，但需要 sudo 修改 server config。最後我和 AI agent 收斂出來的做法，是直接用 Cloudflare Bulk Redirects。&lt;/p&gt;
&lt;h2 id="用-cloudflare-api-建立-bulk-redirect"&gt;用 Cloudflare API 建立 Bulk Redirect&lt;/h2&gt;
&lt;p&gt;我建立了本機設定檔：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~/.cloudflare.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;內容只放必要設定，例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CLOUDFLARE_API_TOKEN=REDACTED
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CLOUDFLARE_ZONE_NAME=stanwu.org
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;實際 token 不會進 repo，也不會寫進文章。&lt;/p&gt;
&lt;p&gt;接著由 AI agent 按照前面的規則讀取 redirect CSV，建立 Cloudflare Bulk Redirect List：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;blog_stanwu_org_legacy_redirects
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這份 list 裡放的是實際需要修補的舊 URL，例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart LR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[&amp;#34;/2016/01/04/sublime-text-%E8%88%87-simplenote-%E5%85%B1%E8%88%9E&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B[&amp;#34;/posts/sublime-text-%E8%88%87-simplenote-%E5%85%B1%E8%88%9E/&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A --&amp;gt;|301| B
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Cloudflare 這裡也踩了一個坑。&lt;/p&gt;
&lt;p&gt;一開始 AI agent 先照我的思路把 Bulk Redirect rule 掛在 zone-level ruleset，API 回：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;phase &amp;#34;http_request_redirect&amp;#34; not allowed at zone level
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;後來再一起確認，Bulk Redirect 要使用 account-level ruleset。也就是 list 是 account 資源，啟用規則也要掛在 account 的 &lt;code&gt;http_request_redirect&lt;/code&gt; phase。&lt;/p&gt;
&lt;p&gt;修正後，流程變成：&lt;/p&gt;
&lt;blockquote class="callout callout-note"&gt;
&lt;div class="callout-title"&gt;Cloudflare Bulk Redirect API 步驟&lt;/div&gt;
&lt;div class="callout-body"&gt;
&lt;ol&gt;
&lt;li&gt;建立或取得 account-level redirect list&lt;/li&gt;
&lt;li&gt;匯入 redirect items&lt;/li&gt;
&lt;li&gt;建立 account-level &lt;code&gt;http_request_redirect&lt;/code&gt; ruleset&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;from_list&lt;/code&gt; 啟用這份 redirect list&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/blockquote&gt;&lt;p&gt;最後成功匯入 36 條實際 4xx path redirect。&lt;/p&gt;
&lt;h2 id="3xx-檢查不是看到-redirect-就安心"&gt;3xx 檢查：不是看到 redirect 就安心&lt;/h2&gt;
&lt;p&gt;修好 4xx 後，下一步是看 3xx。&lt;/p&gt;
&lt;p&gt;3xx 不一定是壞事。這次我們本來就是要建立 &lt;code&gt;301&lt;/code&gt;。真正要找的是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart TD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[舊 URL] --&amp;gt; B[301]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B --&amp;gt; C[301]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; C --&amp;gt; D[200]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; E[舊 URL] --&amp;gt; F[301]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; F --&amp;gt; G[404]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; H[舊 URL] --&amp;gt; I[Redirect loop]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以我再把檢查規則交給 AI agent，讓它做一個小工具，讀 Cloudflare 3xx CSV，對每條 URL 跑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -L -I
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然後輸出 redirect chain audit。&lt;/p&gt;
&lt;p&gt;第一次結果是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ok: 27
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bad_final: 18
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;review: 2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;大多數文章都正常，一跳就到新文章：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart LR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[舊文章 URL] --&amp;gt;|301| B[新文章 URL]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B --&amp;gt; C[200]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;真正有問題的是兩條舊分頁：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/page/7
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/page/9
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hugo 文章列表只到第 6 頁，但它們被導到不存在的 &lt;code&gt;/posts/page/7/&lt;/code&gt;、&lt;code&gt;/posts/page/9/&lt;/code&gt;，結果變成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart LR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[舊分頁] --&amp;gt;|301| B[不存在的新分頁]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B --&amp;gt; C[404]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這種 &lt;code&gt;301 -&amp;gt; 404&lt;/code&gt; 比單純 404 更容易被忽略，因為錯誤被藏到下一跳。&lt;/p&gt;
&lt;p&gt;確認問題後，我再讓 AI agent 把它們改成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart LR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[&amp;#34;/page/7&amp;#34;] --&amp;gt;|301| C[&amp;#34;/posts/&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B[&amp;#34;/page/9&amp;#34;] --&amp;gt;|301| C
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;再整份替換 Cloudflare Bulk Redirect List。&lt;/p&gt;
&lt;p&gt;第二次 audit 結果變成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ok: 29
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bad_final: 16
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;review: 2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;/page/7&lt;/code&gt; 和 &lt;code&gt;/page/9&lt;/code&gt; 都變成：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart LR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[舊分頁] --&amp;gt;|301| B[&amp;#34;/posts/&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B --&amp;gt; C[200]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="成果驗收"&gt;成果驗收&lt;/h2&gt;
&lt;p&gt;最後由 AI agent 批次跑 &lt;code&gt;curl -I&lt;/code&gt; 做第一輪驗收，我再抽查舊文章、舊分頁與新文章 URL。重點很簡單：&lt;/p&gt;
&lt;blockquote class="callout callout-note"&gt;
&lt;div class="callout-title"&gt;驗收結果&lt;/div&gt;
&lt;div class="callout-body"&gt;
&lt;p&gt;舊文章 permalink 會回 &lt;code&gt;301&lt;/code&gt;，新 Hugo 文章會回 &lt;code&gt;200&lt;/code&gt;。
舊 &lt;code&gt;/page/7&lt;/code&gt;、&lt;code&gt;/page/9&lt;/code&gt; 會回 &lt;code&gt;301&lt;/code&gt;，並導到 &lt;code&gt;/posts/&lt;/code&gt;。&lt;/p&gt;
&lt;/div&gt;
&lt;/blockquote&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flowchart LR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; A[舊文章 permalink] --&amp;gt;|301| B[新 Hugo 文章]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; B --&amp;gt; C[200]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這才是 WordPress migration 裡真正應該收尾的狀態。&lt;/p&gt;
&lt;h2 id="還沒處理的項目"&gt;還沒處理的項目&lt;/h2&gt;
&lt;p&gt;不是所有錯誤都值得立刻修。&lt;/p&gt;
&lt;p&gt;目前還保留幾類：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;舊圖片 /wp-content/uploads/...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;舊 category 頁
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;舊 author 頁
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;少數找不到對應內容的文章
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;圖片如果原始檔還找得到，最好的方式是補回 &lt;code&gt;static/uploads/&lt;/code&gt;。如果找不到，就不要硬導到首頁。&lt;/p&gt;
&lt;p&gt;舊 category 和 author 頁可以之後再 mapping 到 Hugo 的 &lt;code&gt;categories&lt;/code&gt; 或 &lt;code&gt;tags&lt;/code&gt;，但這需要人工判斷，不適合全自動亂導。&lt;/p&gt;
&lt;p&gt;有些舊文章在 SQL 和 Hugo 內容裡都找不到可靠對應，例如 MarsEdit + htaccess 那篇，就先保留 manual。除非能找回內容，否則不應該假裝它有正確新頁。&lt;/p&gt;
&lt;h2 id="這次學到的事"&gt;這次學到的事&lt;/h2&gt;
&lt;p&gt;WordPress 遷移 Hugo，不只是「把文章轉成 Markdown」。&lt;/p&gt;
&lt;p&gt;真正完整的 migration 至少包含：&lt;/p&gt;
&lt;blockquote class="callout callout-note"&gt;
&lt;div class="callout-title"&gt;WordPress 到 Hugo 的完整收尾清單&lt;/div&gt;
&lt;div class="callout-body"&gt;
&lt;ul&gt;
&lt;li&gt;內容轉換&lt;/li&gt;
&lt;li&gt;theme/template 轉換&lt;/li&gt;
&lt;li&gt;SEO metadata&lt;/li&gt;
&lt;li&gt;sitemap/rss/canonical&lt;/li&gt;
&lt;li&gt;舊 URL redirect&lt;/li&gt;
&lt;li&gt;圖片資產遷移&lt;/li&gt;
&lt;li&gt;Cloudflare / CDN 規則&lt;/li&gt;
&lt;li&gt;3xx chain audit&lt;/li&gt;
&lt;li&gt;4xx 分級處理&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/blockquote&gt;&lt;p&gt;靜態網站的好處很明確：便宜、快、攻擊面小、容易納入 AI agent 自動化流程。&lt;/p&gt;
&lt;p&gt;但靜態網站不會自動記得 WordPress 的歷史包袱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果舊 URL 沒有處理好，搜尋引擎和 AI crawler 看到的不是「你成功搬家」，而是「一堆內容消失了」。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以這次最大的教訓是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Migration 上線不是結束。Cloudflare、Search Console 和 redirect audit 才是收尾。&lt;/p&gt;
&lt;/blockquote&gt;&lt;p&gt;幸好這些問題可以被系統化處理。只要有 WordPress SQL、Hugo 內容和 Cloudflare CSV，再配合 AI agent 協助寫工具、整理結果、批次驗收，就能把原本很瑣碎的收尾工作變成可重複的流程。&lt;/p&gt;
&lt;p&gt;水很深，但有 AI agent 這艘船，很多原本會讓人放棄的細節，終於可以一段一段渡過去。&lt;/p&gt;</description></item></channel></rss>