這篇記錄這個站台是怎麼架起來的。重點不在「照著打指令」,而在幾個中文使用者特別容易踩到、但英文教學不會提的地方。

為什麼不用 WordPress

理由其實很單純:

面向WordPressHugo
執行層PHP + MySQL,隨時可能有漏洞純靜態 HTML,沒有可攻擊的執行層
維護核心、外掛、主題都要持續更新想更新才更新,不更新也不會被打
備份檔案 + 資料庫 dump複製一個資料夾
速度需要快取外掛才勉強夠快天生就是最快的那種
寫作網頁後台編輯器本機 Markdown,用慣的編輯器

代價是沒有現成的後台,所有事情都得從指令列來。對習慣 terminal 的人來說這反而是優點。

一、安裝 Hugo Extended

不要用 apt install hugo Ubuntu 24.04 套件庫裡是 0.123.7,而現在多數主題(包含 PaperMod)都要求 0.146 以上,裝下去第一步建置就會失敗。

直接抓官方的 .deb

1
2
3
4
cd /tmp
curl -fsSLO https://github.com/gohugoio/hugo/releases/download/v0.165.0/hugo_extended_0.165.0_linux-amd64.deb
sudo dpkg -i hugo_extended_0.165.0_linux-amd64.deb
hugo version

輸出必須同時包含版本號和 extended

1
hugo v0.165.0+extended linux/amd64

extended 這個字很重要 —— 它代表這份執行檔內含 Dart Sass,大部分現代主題的 CSS 都靠它編譯。裝到非 extended 版本,主題會噴 this feature is not available in your current Hugo version

二、建站與主題

1
2
3
hugo new site /var/www/geek --force
git clone --depth=1 https://github.com/adityatelange/hugo-PaperMod.git \
    /var/www/geek/themes/PaperMod

git clone 而不是 Hugo Modules,是因為 Modules 需要裝 Go。單機自架的情境下多一個依賴沒有好處。

三、繁體中文的三個關鍵設定

這一節是整篇的重點。

1. hasCJKLanguage = true

1
2
3
hasCJKLanguage = true
languageCode = 'zh-tw'
defaultContentLanguage = 'zh-tw'

沒開這個設定,中文文章的字數統計、閱讀時間、自動摘要會全部壞掉。

原因是 Hugo 預設用「空白」切詞。中文句子沒有空白,所以一整篇兩千字的文章會被算成 1 個字、閱讀時間 0 分鐘,摘要則可能整段吐出來或整個空白。

開啟後 Hugo 會改用 CJK 字元計數,一切就正常了。判斷有沒有生效很簡單:看文章列表的摘要是不是正常截斷。

2. 網址不要放中文

1
2
[permalinks]
  posts = '/posts/:year/:month/:slug/'

Hugo 預設拿檔名當網址。如果檔名取成 我的第一篇文章.md,網址就會變成:

1
/posts/%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0/

這種網址貼到 Slack、LINE 或 Markdown 連結裡常常會斷掉,搜尋引擎收錄也不漂亮。

做法:檔名一律用英文,中文標題放在 front matter 的 title。讀者看到的是中文標題,網址則保持乾淨。

1
2
3
---
title: "用 Hugo 架一個繁體中文部落格"   # 顯示用,中文
---

檔名 hugo-nginx-letsencrypt.md → 網址 /posts/2026/09/hugo-nginx-letsencrypt/

3. 搜尋要為中文調參數

PaperMod 的搜尋用 Fuse.js,而它的預設值是為英文設計的:

1
2
3
4
[params.fuseOpts]
  minMatchCharLength = 1
  ignoreLocation     = true
  threshold          = 0.4

兩個關鍵:

  • minMatchCharLength = 1 —— 英文預設要求至少 3 個字元才算命中,但中文一個字就有完整語意(「網」、「站」)。維持預設會讓短詞完全搜不到。
  • ignoreLocation = true —— Fuse.js 預設「關鍵字越靠近開頭分數越高」,這是英文標題的假設。中文長句裡的關鍵詞常出現在中段,不關掉的話會被評為不相關而濾掉。

四、Nginx 設定

站台原始碼放 /var/www/geek,但 Nginx 的 root 要指向 public/ 子目錄 —— 這樣 content/themes/ 這些原始檔就不會被對外提供。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
server {
    listen 80;
    server_name zeronode-uav.duckdns.org;

    root /var/www/geek/public;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # 擋掉 .git 之類的隱藏檔
    location ~ /\. {
        deny all;
    }
}

先只開 80,讓 certbot 有辦法做 HTTP-01 驗證。

五、Let’s Encrypt

1
sudo certbot --nginx -d zeronode-uav.duckdns.org --agree-tos -m you@example.com --redirect

certbot 的 nginx 外掛會自動改寫設定檔,補上 443 區塊、憑證路徑和 80 → 443 轉址。

申請前務必先測 80 埠

這是最常見的失敗點。Let’s Encrypt 對失敗次數有上限,連續失敗會被鎖一小時,所以要先確認再送出。

放一個測試檔:

1
2
mkdir -p /var/www/geek/public/.well-known/acme-challenge
echo ok > /var/www/geek/public/.well-known/acme-challenge/ping

然後從外部網路http://你的網域/.well-known/acme-challenge/ping

用手機關掉 Wi-Fi、改行動網路測是最快的方法。從伺服器自己 curl 自己的公網 IP 不算數 —— 多數家用路由器不支援 hairpin NAT,就算轉發設定正確也會連不上,測了只會誤導自己。

如果外部確實連不通,很可能是 ISP 封鎖對外 80 埠(台灣家用線路頗常見)。這種情況要改走 DNS-01:用 DuckDNS 的 API token 寫 TXT 記錄來驗證,繞過 80 埠。

續期

Ubuntu 的 certbot 套件會自帶 systemd timer,不需要自己寫 cron:

1
2
systemctl list-timers | grep certbot
sudo certbot renew --dry-run

--dry-run 會完整跑一次模擬續期,通過就代表三個月後會自動更新。

六、日常寫作流程

1
2
3
hugo new content posts/my-post.md   # 從 archetype 建立草稿
hugo server -D                       # 本機預覽,含草稿,存檔即時更新
hugo --gc --minify --cleanDestinationDir   # 發佈

--cleanDestinationDir 會清掉已刪除文章留在 public/ 的殘骸。沒加這個參數,刪掉的文章網址還是會活著。

小結

真正花時間的不是安裝,是那三個中文設定。hasCJKLanguage 沒開,站台看起來會「能動但哪裡怪怪的」——字數是 0、摘要是空的,而且錯誤訊息不會告訴你原因。這種坑值得先知道。