前言

Hack543 開站以來,一直跑在自己架設的 VPS 主機上,用 WordPress 寫文章。

架站很簡單,維護卻是另一回事。每個禮拜,我都要花時間升級佈景主題、更新 VPS 主機的軟體套件,還要留意網站上每個安裝元件的資安漏洞新聞,看看有沒有輪到自己中標。

長期下來,我有一半的力氣,都花在「讓這台主機能安全地上線」這件事上,而不是寫文章。

直到 nginx 與 php2shell 的漏洞發生後,我才發覺:在這樣的 AI 時代,找漏洞的速度只會越來越快。

以個人的 side project 來說,我維護的力氣,已經追不上目前漏洞被發現的速度,更何況很多還是 zero day。

與其繼續追著漏洞跑,不如換一個維護方式:把網站變成一個沒有資料庫、沒有後台、沒有 PHP 的靜態網站。沒有盲腸,就沒有盲腸炎。

所以我決定搬家,把 Hack543 從 WordPress 搬到 Hugo 靜態網站。

封面圖片出處:AI 生成。

先說清楚:這次搬家主要是 AI 完成的

這一篇要先老實交代:這次搬家,主要是靠 AI 完成的。

我用的是 AI 程式助理 (Claude Code)。從分析主機狀況、寫轉換程式、建置 Hugo 網站、客製化版面,到用 Playwright 逐篇驗證,大部分的工作都是 AI 做的,連這篇文章的初稿也是 AI 依照我的口吻寫的,我再修改。

我負責的部分,主要是「做決定」跟「需要主機權限的操作」。下面的步驟裡,凡是我自己動手做的,都會標註 【我手動完成】,其他的就是 AI 做的。

工作項目誰做的
決定要搬到 Hugo、佈署到 Cloudflare Pages、草稿與留言不搬我
開放主機防火牆,讓 AI 的工作環境可以直連 WordPress 主機我
在主機上安裝 WP-CLI、匯出文章、安裝缺少的 PHP 套件我(照 AI 給的指令)
打包圖片、把檔案交給 AI我
診斷錯誤訊息、發現主機憑證過期AI
撰寫 WordPress 轉 Hugo 的轉換程式AI
建置 Hugo 網站、首頁、側欄、文章頁範本AI
安裝 Playwright,逐篇比對內容、檢查版面AI
版面風格、底色、字型、標題字重等審美決定與驗收我
發現分享到 Facebook / LINE 的預覽圖不對我
修正分享預覽圖、舊 sitemap 轉址、加上 Search Console 驗證與 GAAI
Search Console 驗證、提供 GA 評估 ID、更新 LINE 預覽、確認 GA 收到資料我
這篇文章的初稿AI,我修改
封面圖片AI

為什麼選 Hugo?

靜態網站產生器 (Static Site Generator) 有很多選擇,像是 Hugo、Astro、Jekyll 等。最後選了 Hugo,原因很簡單:

  • 一個執行檔就搞定:Hugo 是 Go 寫的,下載一個執行檔就能用,不用裝 Node.js,也不用擔心 npm 套件的供應鏈攻擊。對資安人來說,相依套件越少,睡得越安穩。
  • 建置速度超快:Hack543 幾十篇文章,建置不到一秒。
  • 程式碼高亮內建:我的文章有一大堆指令跟程式碼,Hugo 在建置時就會幫程式碼上色,不需要再載入 JavaScript。

佈景主題用的是 PaperMod,再由 AI 重做首頁、側欄與文章頁,讓新網站版面看起來清爽。

搬家步驟

接下來是實際的搬家步驟。

*以下操作請在自己的主機上進行,並記得先備份!

STEP 1

讓 AI 連得到 WordPress 主機 【我手動完成】

我的網站前面有 Cloudflare 代管,但 AI 的工作環境需要繞過 Cloudflare,直接連到 WordPress 主機抓資料、比對網頁。

我不想為此把主機整個打開,所以在主機防火牆上,只開放 AI 工作環境的對外 IP 當作白名單。搬家完成後,這條規則就撤掉了。

AI 連線時用 curl 的 --resolve 參數,把網域直接指到主機 IP,不需要改 DNS。

$ curl --resolve hack543.com:443:<主機 IP> https://hack543.com/

結果 AI 一連上去就發現:主機上的 SSL 憑證,早在兩個月前就過期了。因為前面有 Cloudflare 擋著,我完全沒察覺。

其實原因很單純:我之前把這台 VPS 關機了快三個月。主機沒開,憑證自動續期的排程當然也不會跑,重新開機以後,憑證早就過期了。

主機關機,維護也跟著停擺。

STEP 2

匯出 WordPress 文章 【我手動完成,照 AI 給的指令】

WordPress 文章可以用 WP-CLI 的 wp export 指令,匯出成 WXR 格式 (WordPress eXtended RSS) 的 XML 檔案。我在主機上安裝 WP-CLI 之後執行匯出。

$ cd /var/www/html
$ wp export --dir=/tmp/

結果第一個坑馬上就來了。

Error: Strange wp-config.php file: wp-settings.php is not loaded directly.

這個錯誤訊息看起來像是 wp-config.php 有問題。我把 ls -l 的結果貼給 AI,AI 一看就判斷出原因:wp-config.php 的權限是 400,只有 www-data 讀得到。WP-CLI 讀不到檔案內容,才報了這個看起來八竿子打不著的錯誤。

$ ls -l wp-config.php
-r-------- 1 www-data www-data 4484 Mar  4  2022 wp-config.php

wp-config.php 裡面有資料庫密碼,權限設成 400 是對的,不要為了匯出就把權限打開。改用 www-data 的身分執行就好。

$ sudo -u www-data wp export --dir=/tmp/

第二個坑接著來。

PHP Fatal error:  Uncaught Error: Class "DOMDocument" not found

AI 判斷是主機的 PHP 沒有裝 XML 擴充套件,我裝了以後就解決了。

$ sudo apt install php-xml

如果你沒有主機的 shell 權限,也可以直接從後台匯出:「工具」→「匯出」→「所有內容」,產出的 XML 檔案跟 wp export 一樣。

STEP 3

打包圖片 【我手動完成】

文章裡的圖片都放在 wp-content/uploads 資料夾裡,直接打包,連同 XML 檔一起交給 AI。

$ tar czf uploads.tar.gz -C /var/www/html/wp-content uploads

這裡有個資安小提醒:uploads 資料夾裡不是只有圖片!

AI 在解壓縮之前,先檢查了壓縮檔的內容,發現裡面還混了資安外掛的紀錄檔,內容有登入失敗紀錄、登入成功紀錄等資料。外掛的快取、表單上傳的檔案,也都放在這裡。

所以 AI 只取出「年份資料夾」裡的圖片,其他的一律不碰。這些東西如果一起推到 GitHub,萬一 repo 哪天設成公開,那就真的是自己把資料送給大家了。

STEP 4

轉換成 Markdown

接下來要把 XML 裡的文章,轉成 Hugo 用的 Markdown 檔案。AI 寫了一支 Python 轉換程式來處理,過程中踩到好幾個坑,這邊特別拿出來分享。

坑一:XML 解析失敗

XML 一讀進來就報錯。

xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 4851, column 43

追進去看,是某一篇文章裡混了一個看不見的退格字元 (Backspace,0x08),大概是當年從別的地方複製貼上時帶進來的。XML 1.0 規格不允許這種控制字元,解析前先濾掉就好。

坑二:程式碼區塊吞掉後半篇文章

有一篇文章轉完之後,後半段的標題、圖片跟內文全部不見了。

原因是那篇文章在引用區塊 (blockquote) 裡面放了程式碼區塊。轉成 Markdown 時,只有程式碼的第一行有加上引用的 > 符號,後面幾行沒有,結果程式碼區塊永遠沒有結束,把後面整篇文章都吞進去了。

坑三:整行文字憑空消失

C2 複刻系列有一篇文章,清單裡有這麼一行:

- [esp]: 解密完成的字串會放在此變數中

轉完之後,這一行在網頁上直接消失。

原來 [esp]: 文字 這種寫法,在 Markdown 裡剛好是「連結參照定義」(Link Reference Definition) 的語法,Markdown 把它當成連結定義吃掉了,不會顯示出來。解法是把一般文字裡的 [ 跳脫成 \[。

這種 bug 很難用肉眼發現,坑二跟坑三,都是靠下一個步驟抓出來的。

STEP 5

用 Playwright 逐篇比對內容

38 篇文章、161 個程式碼區塊,一篇一篇用眼睛比對,眼睛會脫窗。

我請 AI 自己安裝 Playwright 來驗證。AI 在工作環境裡裝好 Playwright 跟 Chromium 瀏覽器,連瀏覽器缺少的系統函式庫、顯示中文需要的字型,都自己補齊了。

接著 AI 寫了一支比對程式,用 Playwright 同時打開舊的 WordPress 跟新的 Hugo 網站,一篇一篇比對:

  • 標題與段落標題是否一致
  • 程式碼區塊數量,以及每一塊的內容長度是否一致
  • 舊網站的每一段文字,在新網站是否都找得到
  • 圖片是否都正常顯示
  • 手機版與桌面版是否有版面跑掉

第一輪比對就抓出了坑二跟坑三。修到最後,38 篇文章全部通過,161 個程式碼區塊一個都沒少。

STEP 6

重做 Hugo 佈景主題

內容對了,接下來是版面。我請 AI 以開源的 PaperMod 為基礎,重新做了一套 Hugo 的佈景主題,讓新網站版面看起來清爽。

做好之後,AI 再用 Playwright 檢查每一頁,確認桌面版跟手機版的版面都沒有跑掉。

程式碼區塊的字型,則是我另外挑的。AI 比較了幾款等寬字型給我看,我選了 JetBrains Mono,0 跟 O、1 跟 l 比較好分辨,看指令比較不會看錯。【這個決定是我做的】

STEP 7

保留原本的網址

搬家最怕的就是網址改變。Google 搜尋結果、別人文章裡的連結,全部都會變成 404。

Hugo 可以設定 permalinks,讓文章網址維持跟 WordPress 一樣的 /文章代稱/ 格式。

permalinks:
  page:
    posts: /:slug/

分類頁的 /category/分類名稱/、圖片的 /wp-content/uploads/... 路徑,也都照原樣保留。舊的 RSS 網址 /feed/,則用 Cloudflare Pages 的 _redirects 檔案轉址到新的 /index.xml。

/feed/        /index.xml   301
/author/*     /            301

佈署:GitHub 私人 repo + Cloudflare Pages

網站轉好了,接下來要決定放在哪裡。我選的組合是:

  • GitHub 私人 repo:放網站原始碼,文章就是 Markdown 檔案,所有修改都有 Git 紀錄
  • Cloudflare Pages:每次 push 到 GitHub,就自動建置並佈署到全球 CDN

私人 repo 也能接 Cloudflare Pages。連接時,Cloudflare 會請你在 GitHub 安裝 Cloudflare Pages 的 GitHub App,授權範圍記得選「Only select repositories」,只開放部落格這一個 repo,Cloudflare 就看不到其他 repo。

Cloudflare Pages 的建置設定很簡單:

Framework preset : Hugo
Build command    : hugo --gc --minify
Output directory : public
環境變數          : HUGO_VERSION = 0.168.0

記得用 HUGO_VERSION 指定 Hugo 版本,讓 Cloudflare 建置時用的版本,跟自己電腦上的一樣,不然哪天 Hugo 改版,網站可能就莫名其妙壞掉。

為什麼選私人 repo?

理由很簡單:免得麻煩。

網站本身是公開的,但網站的原始碼不一定要公開。用公開 repo,每次 commit 都要想一下:這個檔案能不能給人看?草稿會不會被看到?Git 紀錄裡有沒有不該出現的東西?

用私人 repo,這些麻煩就少了一大半,草稿也可以放心 commit。

不過私人 repo 不是保險箱,該注意的事一樣要注意。

  • 不要放任何密碼、Token、API Key:Cloudflare 的設定請放在 Cloudflare 後台的環境變數
  • WordPress 匯出的 XML 跟 uploads 打包檔不要 commit:XML 裡有作者帳號等資訊,uploads 打包檔則可能混了外掛的紀錄檔
  • commit 的作者 email 用 GitHub 的 noreply 地址:萬一哪天 repo 改成公開,Git 歷史紀錄裡的 email 就會跟著曝光
  • commit 之前先檢查一次:git status、git diff --cached 養成習慣

如果你想用公開 repo,上面這些就更重要了:推上去的東西全世界都看得到,而且 Git 歷史紀錄會永遠留著。

另外,AI 也在 Cloudflare Pages 的 _headers 檔案加上了基本的安全性標頭 (Security Headers)。

/*
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Strict-Transport-Security: max-age=31536000

切換以後

Cloudflare Pages 上線、網域切換完成,原本的 WordPress 主機就功成身退了。我先撤掉給 AI 用的防火牆白名單,關掉主機對外的 80、443 通訊埠,觀察一陣子沒問題之後,再把主機整個關掉。

那張過期兩個月的憑證,也終於不用再管它了。

上線後的收尾:分享預覽圖、Search Console 與 GA

網站上線以後,還有幾件 WordPress 外掛以前默默幫我做掉的事,要自己補回來。

上線後分享文章到 Facebook 跟 LINE,結果預覽圖不是文章的封面,而是網站的 logo。【這是我自己發現的】

AI 檢查文章頁的 og:image 標籤,發現兩個問題:

  • 新文章的圖片網址錯了:封面圖放在文章自己的資料夾裡,但 PaperMod 佈景主題要多加一行 relative: true,才會在網址前面接上文章路徑。少了這行,網址就變成根本不存在的 https://hack543.com/cover.jpg,Facebook 跟 LINE 抓不到圖,只好拿頁面上的 logo
  • 首頁跟沒有封面的文章,沒有指定分享圖:平台就自己從頁面上挑一張,通常也是挑到 logo
cover:
  image: cover.jpg
  relative: true    # 封面在文章資料夾裡,分享預覽圖需要這行

AI 補上這行,也把它加進新文章的範本,以後寫新文章就不會再忘記。另外做了一張 1200×630 的預設分享圖,給首頁跟沒有封面的頁面用。

修好之後,平台還記著之前抓到的 logo,要請它們重新抓一次:

  • Facebook:用 Sharing Debugger 貼上文章網址,按「Scrape Again」
  • LINE:以前清除快取用的 Page Poker 已經停用了。我的做法是分享時在網址後面加上 ?v=2 這類參數,LINE 會把它當成新網址重新抓取。【我手動完成】

sitemap 與 Google Search Console

Hugo 會自動產生 sitemap.xml,robots.txt 也會指向它。但原本 WordPress 的 SEO 外掛 (Rank Math),產生的 sitemap 網址是 /sitemap_index.xml。以前提交給 Google 的是舊網址,搬家後就變成 404 了。

AI 在 _redirects 把舊的 sitemap 網址都轉到新的:

/sitemap_index.xml      /sitemap.xml   301
/post-sitemap.xml       /sitemap.xml   301
/page-sitemap.xml       /sitemap.xml   301
/category-sitemap.xml   /sitemap.xml   301

接著到 Google Search Console 重新提交 sitemap 時,Google 要求重新驗證網站擁有權。

驗證方式我選的是「HTML 標記」(meta 標籤)。PaperMod 有內建 Search Console 驗證碼的設定,寫在 hugo.yaml 就會輸出到每一頁的 <head>,不用自己改範本:

params:
  analytics:
    google:
      SiteVerificationTag: <Search Console 給的驗證碼>

上線後回到 Search Console 按「驗證」,再提交新的 sitemap.xml。【我手動完成】

Google Analytics (GA4)

以前的流量統計,是 WordPress 外掛幫我加的。外掛一拿掉,網站就完全沒有流量統計了。

我沿用原本 WordPress 時期的 GA4 評估 ID (G- 開頭),交給 AI 設定。用同一個評估 ID,新舊數據就會接在一起,不會因為搬家而斷掉。Hugo 本身就支援 GA,在 hugo.yaml 加上一段設定就好:

services:
  googleAnalytics:
    ID: <你的 GA4 評估 ID>

PaperMod 只會在正式環境載入 GA,自己在本機預覽時不會被算進去。

AI 上線後用 Playwright 確認瀏覽器有把資料送到 Google。不過我到 GA 後台的「即時」報表一看,卻沒有資料。原因很可能是 AI 用的是無頭瀏覽器 (Headless Chrome),GA 會把這種流量當成機器人過濾掉。我改用自己的瀏覽器打開網站,就在即時報表看到自己的瀏覽了。【我手動完成】

如果你也遇到 GA 沒有資料,先檢查瀏覽器的廣告阻擋工具,它們幾乎都會擋掉 GA。

盤點:省下的費用與工作

搬完家之後,我把前後的差別盤點了一下。

省下的費用

項目搬家前搬家後
主機VPS 月租費,每個月都要付不需要主機,Cloudflare Pages 免費方案
佈景主題WordPress 付費佈景主題開源的 PaperMod,免費
原始碼存放在 VPS 上GitHub 私人 repo,免費

網域的費用還是要付,這個跑不掉。

省下的工作

這部分才是我最有感的。以前每個禮拜的例行維護,現在幾乎都不用做了:

例行工作搬家前搬家後
升級 WordPress 版本每次改版都要升級,還要擔心外掛相容性沒有 WordPress
更新 WordPress 外掛 (Plugin)每個外掛都要追版本沒有外掛
更新佈景主題版本要跟著更新,還要確認版面沒壞掉不需要隨時跟進,想更新再更新
OS 套件安全性更新VPS 的作業系統跟套件要定期更新沒有主機
追資安漏洞新聞留意網站上每個安裝元件有沒有漏洞現在可以喘口氣了
SSL 憑證續期主機關機,續期就跟著停擺Cloudflare 自動處理

現在更新網站,就是寫好 Markdown,git push,一兩分鐘後網站就更新了。

前言說過,以前我有一半的力氣,都花在讓主機能安全地上線。現在這一半的力氣,終於可以拿回來寫文章了。

結論

這次搬家,我自己動手的部分其實不多,大部分的時間,我是在「做決定」跟「驗收」。

AI 做得又快又細,很多轉檔細節都是 AI 抓出來的。最讓我有感的是,AI 做了很多以前手動搬家時,自己做起來很累、很繁瑣的事。例如寫程式把文章轉檔、逐篇比對新舊網頁、重做 Hugo 佈景主題,讓新網站版面看起來清爽。這些事在以前沒有 AI 幫忙的時候,幾乎是辦不到的。

但也不是全部丟給 AI 就好。要不要開防火牆、開給誰、哪些資料不能外流、網站要長什麼樣子,這些還是得自己判斷。AI 的速度很快,人要負責踩煞車跟把關方向。

現在,Hack543 是一個沒有資料庫、沒有後台、沒有 PHP 的靜態網站了。如果你也有一個 WordPress 網站,也可以考慮這樣做。如果你喜歡我的文章,也請你分享給你的好朋友。