前言
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,逐篇比對內容、一對一比對 CSS | AI |
| 版面風格、底色、字型、標題字重等審美決定與驗收 | 我 |
| 這篇文章的初稿 | AI,我修改 |
| 封面圖片 | AI |
為什麼選 Hugo?
靜態網站產生器 (Static Site Generator) 有很多選擇,像是 Hugo、Astro、Jekyll 等。最後選了 Hugo,原因很簡單:
- 一個執行檔就搞定:Hugo 是 Go 寫的,下載一個執行檔就能用,不用裝 Node.js,也不用擔心 npm 套件的供應鏈攻擊。對資安人來說,相依套件越少,睡得越安穩。
- 建置速度超快:Hack543 幾十篇文章,建置不到一秒。
- 程式碼高亮內建:我的文章有一大堆指令跟程式碼,Hugo 在建置時就會幫程式碼上色,不需要再載入 JavaScript。
佈景主題用的是 PaperMod,再由 AI 客製化首頁、側欄與文章頁,維持跟原本 Hack543 一樣的樣子,讓老讀者不會覺得走錯地方。
搬家步驟
接下來是實際的搬家步驟。
*以下操作請在自己的主機上進行,並記得先備份!
讓 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 關機了快三個月。主機沒開,憑證自動續期的排程當然也不會跑,重新開機以後,憑證早就過期了。
主機關機,維護也跟著停擺。
匯出 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 一樣。
打包圖片 【我手動完成】
文章裡的圖片都放在 wp-content/uploads 資料夾裡,直接打包,連同 XML 檔一起交給 AI。
$ tar czf uploads.tar.gz -C /var/www/html/wp-content uploads
這裡有個資安小提醒:uploads 資料夾裡不是只有圖片!
AI 在解壓縮之前,先檢查了壓縮檔的內容,發現裡面還混了資安外掛的紀錄檔,內容有登入失敗紀錄、登入成功紀錄等資料。外掛的快取、表單上傳的檔案,也都放在這裡。
所以 AI 只取出「年份資料夾」裡的圖片,其他的一律不碰。這些東西如果一起推到 GitHub,萬一 repo 哪天設成公開,那就真的是自己把資料送給駭客了。
轉換成 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 很難用肉眼發現,坑二跟坑三,都是靠下一個步驟抓出來的。
用 Playwright 逐篇比對內容
38 篇文章、161 個程式碼區塊,一篇一篇用眼睛比對,眼睛會脫窗。
我請 AI 自己安裝 Playwright 來驗證。AI 在工作環境裡裝好 Playwright 跟 Chromium 瀏覽器,連瀏覽器缺少的系統函式庫、顯示中文需要的字型,都自己補齊了。
接著 AI 寫了一支比對程式,用 Playwright 同時打開舊的 WordPress 跟新的 Hugo 網站,一篇一篇比對:
- 標題與段落標題是否一致
- 程式碼區塊數量,以及每一塊的內容長度是否一致
- 舊網站的每一段文字,在新網站是否都找得到
- 圖片是否都正常顯示
- 手機版與桌面版是否有版面跑掉
第一輪比對就抓出了坑二跟坑三。修到最後,38 篇文章全部通過,161 個程式碼區塊一個都沒少。
用 Playwright 一對一比對 CSS
內容對了,接下來是版面。我希望新網站看起來跟舊網站一樣。
一開始 AI 是看截圖調整版面,但截圖看久了,總有一些「好像哪裡怪怪的」地方,例如首頁標題的字重跟文章頁不一樣,就是我自己看出來的。
所以我請 AI 換個方法,把 CSS 一對一比對清楚。AI 的做法是:
- 在舊網站跟新網站上,用同一段文字找到對應的元素,例如同一篇文章的標題、同一個「閱讀更多」按鈕、同一個側欄標籤
- 用 Playwright 讀出兩邊元素實際套用的樣式 (Computed Style)
- 逐一比對 24 種屬性,包括字型、字級、字重、行高、顏色、底色、內外距、邊框等
範圍涵蓋首頁、文章頁、分類頁、頁首、頁尾與側欄,一共 40 個比對點。
第一輪比對,有 39 個比對點不一致,大部分是當初看截圖估出來的數值不夠精準,例如藍色其實有兩種、摘要的字級其實是 20px、目前頁碼其實是橘色。修正後剩下 15 個,AI 逐項確認過,不是畫面上看不出差別的結構差異,就是刻意保留的設計。
唯一刻意不一樣的地方,是程式碼區塊的字型。AI 比較了幾款等寬字型給我看,我選了 JetBrains Mono,0 跟 O、1 跟 l 比較好分辨,看指令比較不會看錯。【這個決定是我做的】
保留原本的網址
搬家最怕的就是網址改變。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 通訊埠,觀察一陣子沒問題之後,再把主機整個關掉。
那張過期兩個月的憑證,也終於不用再管它了。
結論
這次搬家,我自己動手的部分其實不多,大部分的時間,我是在「做決定」跟「驗收」。
AI 做得又快又細,連我自己都沒注意到的過期憑證、混在 uploads 資料夾裡的登入紀錄、會讓一整行文字消失的 Markdown 語法,都是 AI 抓出來的。
但也不是全部丟給 AI 就好。要不要開防火牆、開給誰、哪些資料不能外流、網站要長什麼樣子,這些還是得自己判斷。AI 的速度很快,人要負責踩煞車跟把關方向。
回頭看前言說的那件事:AI 讓找漏洞變快了,也讓修東西、搬家變快了。與其用個人的力氣去追漏洞,不如用 AI 幫自己把攻擊面整個拿掉。
現在,Hack543 是一個沒有資料庫、沒有後台、沒有 PHP 的靜態網站了。如果你也有一個 WordPress 網站,也可以考慮這樣做。
