前言
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 驗證與 GA | AI |
| 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 重做首頁、側欄與文章頁,讓新網站版面看起來清爽。
搬家步驟
接下來是實際的搬家步驟。
*以下操作請在自己的主機上進行,並記得先備份!
讓 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 個程式碼區塊一個都沒少。
重做 Hugo 佈景主題
內容對了,接下來是版面。我請 AI 以開源的 PaperMod 為基礎,重新做了一套 Hugo 的佈景主題,讓新網站版面看起來清爽。
做好之後,AI 再用 Playwright 檢查每一頁,確認桌面版跟手機版的版面都沒有跑掉。
程式碼區塊的字型,則是我另外挑的。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 通訊埠,觀察一陣子沒問題之後,再把主機整個關掉。
那張過期兩個月的憑證,也終於不用再管它了。
上線後的收尾:分享預覽圖、Search Console 與 GA
網站上線以後,還有幾件 WordPress 外掛以前默默幫我做掉的事,要自己補回來。
分享到 Facebook、LINE 的預覽圖變成 logo
上線後分享文章到 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 網站,也可以考慮這樣做。如果你喜歡我的文章,也請你分享給你的好朋友。
