在 Cloudflare 部署 Hugo 網站
講講 Hugo 部署到 Cloudflare 的相關內容,基本上和 Hugo 文檔講的一樣,本文內容主要是自己的發現和補充。
Pages 和 Workers
Pages 是 Cloudflare 過去專門用於部署靜態網站的服務,但是由於 Workers 服務逐漸強大,因此 Pages 已經不再被建議使用,建議直接用 Workers 部署網站,即使他是靜態的也一樣。
那現在 (2026) 他們具體上有什麼差異呢,就是 Pages 設定更簡單,Workers 則一定要使用 wrangler,這是唯一的差別。
實際親測 Pages 和 Workers 部署,心得是用 Workers 就好了:簡單的網站設定沒多幾行,用 wrangler 也沒變的多複雜;複雜的網站本身就更適合以 Workers 部署。
概念
將源碼推送到 Github/Gitlab 倉庫,Cloudflare 設定和倉庫連結,偵測到推送就自動執行構建和部署流程。
關鍵設定
如何設定看 Hugo 文檔就好了,這邊簡單補充 + 優化文檔:
- Build command (dashboard): 留空
- Deploy command (dashboard): pnpm dlx wrangler@4.122.0 deploy
- Version command (dashboard): pnpm dlx wrangler@4.122.0 versions upload
- Root directory (dashboard): /
- Variables and secrets (dashboard):
- SKIP_DEPENDENCY_INSTALL=1 用於避免 worker 自動安裝 pnpm 套件
- wrangler.toml 設定靜態網站部署指令
- build.sh 設定工具依賴
- HUGO_VERSION: 由環境變數控制
- NODE_VERSION: 不由環境變數控制,由 .npmrc 控制,直接交給 worker 自動偵測版本
- HUGO_CACHEDIR: 快取 Hugo 構建結果,但是由於沒有圖片,因此目前用不到
- CUSTOM_CACHE_DIR: 快取相關工具套件,如 hugo 本身的執行檔
使用 pnpm 因為速度更快;固定 wrangler 版本1避免每次構建都要解析版本;SKIP_DEPENDENCY_INSTALL 避免 Workers 自動安裝過程安裝到 devDependencies 2;不設定 NODE_VERSION 由 workers 環境自行偵測 .nvmrc 方式更原生且整合工具鏈;wrangler.toml 因為是靜態網站所以不需要設定 main,並且新增 assets.directory 和 assets.not_found_handling 用於靜態網站的輸出目錄和 404 處理。
具體範例
包含我個人部落格的以及 hugo-yore 使用的複雜版本,包含 wrangler.toml 以及 build.sh。
個人網站版
我的個人網站需要用到 js.Build 整合 node_modules 套件,其他的都不需要,因此只包含 Hugo 和 pnpm 安裝,其餘全部移除。
wrangler.toml
name = "blog"
compatibility_date = "2026-08-13"
[build]
command = "chmod a+x build.sh && ./build.sh"
[assets]
directory = "./public"
not_found_handling = "404-page"
build.sh
#!/usr/bin/env bash
set -euo pipefail
HUGO_VERSION=0.164.0
# NODE_VERSION is defined in .nvmrc
export TZ=Asia/Taipei
export HUGO_CACHEDIR="${PWD}/.cache/hugo"
CUSTOM_CACHE_DIR="${HUGO_CACHEDIR}/__custom__"
cleanup() {
if [[ -n "${build_temp_dir:-}" && -d "${build_temp_dir}" ]]; then
rm -rf "${build_temp_dir}"
fi
}
trap cleanup EXIT SIGINT SIGTERM
main() {
# Install Hugo
# Cached install. See full version in https://github.com/ZhenShuo2021/hugo-yore/tree/v2.8.0
if [[ -x "${CUSTOM_CACHE_DIR}/hugo-${HUGO_VERSION}/hugo" ]]; then
echo "Using cached Hugo ${HUGO_VERSION}..."
else
echo "Installing Hugo ${HUGO_VERSION}..."
build_temp_dir=$(mktemp -d)
pushd "${build_temp_dir}" > /dev/null
curl -sLJO "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz"
mkdir -p "${CUSTOM_CACHE_DIR}/hugo-${HUGO_VERSION}"
tar -C "${CUSTOM_CACHE_DIR}/hugo-${HUGO_VERSION}" -xf "hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz"
popd > /dev/null
rm -rf "${build_temp_dir}"
build_temp_dir=""
fi
export PATH="${CUSTOM_CACHE_DIR}/hugo-${HUGO_VERSION}:${PATH}"
# Log tool versions
echo "Logging tool versions..."
command -v hugo &> /dev/null && echo "Hugo: $(hugo version)" || echo "Hugo: not installed"
command -v node &> /dev/null && echo "Node.js: $(node --version)" || echo "Node.js: not installed"
# Configure Git
echo "Configuring Git..."
git config --global core.quotepath false
# Fetch full Git history
# if [[ $(git rev-parse --is-shallow-repository) == true ]]; then
# echo "Fetching full Git history..."
# git fetch --unshallow
# fi
# Initialize Git submodules
if [[ -f .gitmodules ]]; then
echo "Initializing Git submodules..."
git submodule update --init --recursive
fi
# Install Node.js dependencies
# should set SKIP_DEPENDENCY_INSTALL=1 in cloudflare "Variables and secrets"
pnpm install --prod --frozen-lockfile --ignore-scripts
# Build the project
echo "Building the project..."
hugo build --gc --minify
}
main "$@"
基本上就是官方版本移除掉不需要的東西,比較特別的是 Hugo 安裝也納入快取版本,而 pnpm 設定 --prod 只安裝開發套件,--frozen-lockfile 不更新 lockfile,--ignore-scripts 關閉自訂的 pnpm 的 pre/post 腳本執行。
我想大家比較感興趣的是和 Pages 的速度比較,我把 log 丟給 Claude 後整理出的表格如下:
| 階段 | Pages | Worker | 差異 |
|---|---|---|---|
| 環境初始化 | — | 1.70s | Worker 多此步驟 |
| Clone repository | 1.82s | 1.72s | 打平 |
| 還原 dependencies cache | 0.39s | 1.46s | Pages 快 |
| Cache 還原確認 / 偵測工具 | 2.34s + 1.47s + 1.30s = 5.11s | 0.70s + 0.10s = 0.80s | Worker 快 4.31s |
| 安裝/更新 Node.js | 3.57s(重新下載安裝) | 0s(已在環境中) | Worker 快 3.57s |
| pnpm 啟用 | 3.02s | 0s(環境已內建) | Worker 快 3.02s |
| 安裝/更新 Hugo | 0.17s + 1.14s = 1.31s(重新下載安裝) | 0.08s(用快取) | Worker 快 1.23s |
| 跳過依賴安裝確認 | — | 1.08s | Worker 多此步驟 |
| 準備執行 deploy 指令 | — | 0.15s | Worker 多此步驟 |
pnpm install | 1.86s + 0.59s = 2.45s(含 devDependencies) | 3.61s + 0.46s + 0.16s = 4.23s(僅 dependencies,含下載 35 套件) | Pages 快 1.78s |
hugo build | 1.12s | 0.43s | Worker 快 0.69s |
| Build 完成後檢查 | 0.50s | 0.56s | 打平 |
| 部署啟動 | 1.31s | 0.10s | Worker 快 |
| 資產上傳/檢查 | 2.27s | 1.35s | Worker 快 |
| 上傳完成確認 | 3.32s | 1.12s + 0.54s = 1.66s | Worker 快 1.66s |
| 收尾(cache 上傳/完成) | 1.56s | 0.11s + 0.89s + 0.13s = 1.13s | Worker 快 |
| 總計 | 28.46s | 17.20s | Worker 快 11.26s(約 40%) |
兩次 Pages build(35.09s / 28.46s)都比 Worker(17.20s)慢至少 11 秒以上,差距的九成以上集中在同一塊:Node.js、Hugo、pnpm 的安裝與啟用。
兩邊用的是同一套工具版本(Node 24.18.0、Hugo 0.164.0、pnpm 10.33.4),但 Pages 的 CI 每次都重新走一遍「偵測版本 → 下載 tarball → 解壓安裝 → reshim」的完整流程;Worker 的 CI 環境則是工具鏈已經預裝/快取在容器裡,直接偵測到版本就跳過安裝,等於省掉了整個下載安裝的步驟。
Hugo Yore 複雜版本
Yore 版本就包含更多變化:wrangler 設定多環境以區分主分支部署、次要分支預覽,build.sh 也有相應改變,dashboard 也同樣要更改:Deploy command 要加上 -e "" 明確指定使用 wrangler 的最上層環境(生產環境),Version command 則對應加上 -e dev。
除此之外,最大的差異還有 exampleSite 的存在,yore 的網站本身放在子目錄中,和一般個人網站不同,不是直接放在根目錄。此外也依賴 Hugo modules、pagefind indexing,因此也包含 Go 安裝和索引指令。
其餘 shell 選項部分則是用於 CI testing,其實這些應該要拆檔案的,但是目前懶惰還沒改。
同樣的 wrangler 設定我用 hextra template 測試就沒辦法啟用 branch preview,但是 Hugo Yore 即使開新 worker 重複設定一次都可以啟用 branch preview,我不知道到底哪裡出問題。
wrangler.toml
name = "hugo-yore"
compatibility_date = "2026-08-13"
[build]
command = "chmod a+x build.sh && ./build.sh"
[assets]
directory = "./public"
not_found_handling = "404-page"
[env.dev]
name = "hugo-yore"
[env.dev.build]
command = "chmod a+x build.sh && HUGO_DEPLOY_ENV=staging ./build.sh --base-url https://dev-hugo-yore.workerdomain.workers.dev"
[env.dev.assets]
directory = "./public"
not_found_handling = "404-page"
build.sh
#!/usr/bin/env bash
set -euo pipefail
build_temp_dir=""
cleanup() {
if [[ -n "${build_temp_dir}" && -d "${build_temp_dir}" ]]; then
rm -rf "${build_temp_dir}"
fi
}
trap cleanup EXIT SIGINT SIGTERM
HUGO_DEPLOY_ENV="${HUGO_DEPLOY_ENV:-staging}"
HUGO_CACHEDIR="${PWD}/.cache/hugo"
HUGO_VERSION=0.164.0
GO_VERSION=1.25.5
MINIMAL_CONFIG=false
SKIP_PAGEFIND=false
BASE_URL=""
CUSTOM_CACHE_DIR="${HUGO_CACHEDIR}/__custom__"
export HUGO_CACHEDIR
export GOPATH="${CUSTOM_CACHE_DIR}/go-pkg"
export GOMODCACHE="${GOPATH}/pkg/mod"
export TZ=Asia/Taipei
export HUGO_DEPLOY_ENV
echo "Deploy environment: ${HUGO_DEPLOY_ENV}"
main() {
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--base-url)
if [[ -z "${2:-}" ]]; then
echo "--base-url requires a value" >&2
exit 1
fi
BASE_URL="$2"
shift 2
;;
--minimal-config)
MINIMAL_CONFIG=true
shift
;;
--skip-pagefind)
SKIP_PAGEFIND=true
shift
;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
done
mkdir -p "${CUSTOM_CACHE_DIR}"
# Install Go
if [[ -x "${CUSTOM_CACHE_DIR}/go-${GO_VERSION}/go/bin/go" ]]; then
echo "Using cached Go ${GO_VERSION}..."
else
echo "Installing Go ${GO_VERSION}..."
build_temp_dir=$(mktemp -d)
pushd "${build_temp_dir}" > /dev/null
curl -sLJO "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz"
mkdir -p "${CUSTOM_CACHE_DIR}/go-${GO_VERSION}"
tar -C "${CUSTOM_CACHE_DIR}/go-${GO_VERSION}" -xf "go${GO_VERSION}.linux-amd64.tar.gz"
popd > /dev/null
rm -rf "${build_temp_dir}"
build_temp_dir=""
fi
export PATH="${CUSTOM_CACHE_DIR}/go-${GO_VERSION}/go/bin:${PATH}"
# Install Hugo
if [[ -x "${CUSTOM_CACHE_DIR}/hugo-${HUGO_VERSION}/hugo" ]]; then
echo "Using cached Hugo ${HUGO_VERSION}..."
else
echo "Installing Hugo ${HUGO_VERSION}..."
build_temp_dir=$(mktemp -d)
pushd "${build_temp_dir}" > /dev/null
curl -sLJO "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz"
mkdir -p "${CUSTOM_CACHE_DIR}/hugo-${HUGO_VERSION}"
tar -C "${CUSTOM_CACHE_DIR}/hugo-${HUGO_VERSION}" -xf "hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz"
popd > /dev/null
rm -rf "${build_temp_dir}"
build_temp_dir=""
fi
export PATH="${CUSTOM_CACHE_DIR}/hugo-${HUGO_VERSION}:${PATH}"
# Install Node.js dependencies
# should set SKIP_DEPENDENCY_INSTALL=1 in cloudflare "Variables and secrets"
pnpm install --prod --frozen-lockfile --ignore-scripts
# Verify installations
echo "Verifying installations..."
echo Go: "$(go version)"
echo Hugo: "$(hugo version)"
# Configure Git
echo "Configuring Git..."
git config core.quotepath false
# not using .GitInfo anymore
# if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
# git fetch --unshallow
# fi
# Apply minimal config if requested
if [[ "${MINIMAL_CONFIG}" == "true" ]]; then
echo "Applying minimal config for build testing..."
cat > exampleSite/hugo.yaml << 'EOF'
baseURL: https://example.org/
title: My New Hugo Project
module:
workspace: hugo-yore-doc.work
imports:
- path: github.com/ZhenShuo2021/hugo-yore/v2
- path: github.com/ZhenShuo2021/hugo-knowledge-graph
# Below are used to suppress unnecessary warnings
ignoreLogs: ["warning-goldmark-raw-html", "fetch-fail"]
languages:
en:
contentDir: content/en
locale: en
zh-cn:
contentDir: content/zh-cn
locale: zh-cn
EOF
fi
# Build the site
# MUST use `-d` for the cache to work
echo "Building the site..."
cd exampleSite
hugo mod get
if [[ -n "${BASE_URL}" ]]; then
hugo -d ../public --gc --minify --baseURL "${BASE_URL}"
else
hugo -d ../public --gc --minify
fi
cd ..
# Run Pagefind indexing
if [[ "${SKIP_PAGEFIND}" == "true" ]]; then
echo "Skipping Pagefind indexing (--skip-pagefind set)."
else
echo "Running Pagefind indexing..."
pnpm pagefind --site public
echo "Pagefind indexing complete."
fi
}
main "$@"
對照以 Hugo 官方提供的安裝方式(npx wrangler deploy 且 Go/Hugo/Modules 沒有快取),速度從 75-90 秒降低到 40-70 秒,也就是說最好和最差相比最高可以快到兩倍。
HUGO_CACHEDIR
這就是我講 Hugo 可悲的地方了。直接被 Cloudflare 歸類到不遵守 semantic versioning 的那類,Workers 快取系統也看 Hugo 沒有,完全不支援 Hugo,因此才需要 HUGO_CACHEDIR 設定快取目錄 .cache,這其實是偽裝成 Eleventy。
這就要說我遇到的坑了,Hugo Yore 最初設定總是無法套用快取,把所有東西都試過一輪才發現除了 .cache 要設定以外,構建目錄也要放到根目錄 ./public 而不是 ./exampleSite/public,才能成功偽裝正確套用快取。
小結
Hugo 真的很快,hugo-yore 目前有 500 個頁面構建在 CI 上只需要 4 秒,包含影像處理則 7 秒,CI 整體時間拖到 100 秒都是在設定環境,透過快取優化最高可以降低到只有 40 秒。
現在的設定基本上也已經是最快的速度了,能快取的全部快取,剩下都是硬核 CPU 工作,理由是測試過程有一次不小心移除 deploy 指令連帶 build.sh 也完全沒跑,耗時不記得具體數字了,但是這種情況下也不是瞬間完成,環境初始化就是要那麼多時間是逃不掉的。
其他部署方式?
Cloudflare 是全球最大網路服務商,自身就整合 headers/redirects/domain/host/ci/waf 等各種數不勝數的功能,如果你的地區可以正常訪問 Cloudflare,我真的覺得用他就好了。
Github Pages 非常純粹沒有任何額外功能,沒有 brotli 只有 gzip,這也就算了,最大的問題是快取只有 10 分鐘:一個 20KB 的 CSS 檔案 10 分鐘後就需要重新打網路,還沒辦法改。
其他的部署方式我就沒試過了,最知名的還有 Netlify 和 Vercel,如果不想要大公司的話可以試試看這兩間公司,以下是叫 Claude 整理的基礎資訊:
Vercel 是前端雲端平台,專注 Next.js 部署,提供自動化 CI/CD、全球 CDN、Serverless/Edge Functions,讓開發者快速上線並自動擴展 Web 應用。
Netlify 是靜態網站與 JAMstack 雲端平台,提供自動化建置部署、CDN、Serverless Functions、表單處理與身分驗證,支援快速上線與持續整合。
搜尋一下兩家公司的最新規模資訊。估值/融資: Vercel 於 2025 年 9 月完成 $300M Series F,估值達 $9.3B;累計融資約 $863M。
Netlify 融資規模小很多,累計約 $313M(截至 2024 年初),2021 年估值 $2.5B,近年未再公開大額融資。
營收: Vercel 2025 年營收達 約 $200M,2026 年 ARR 已達 $340M。Netlify 未公開 ARR,規模明顯落後。
結論: Vercel 目前在估值與營收規模上都明顯領先 Netlify,尤其受惠於 Next.js 生態與 AI(v0)帶動的成長。
讓我確認正確的 Cloudflare 規模。不同量級,Cloudflare 完全不是同一個等級的對手:
規模對比:
| Vercel | Netlify | Cloudflare | |
|---|---|---|---|
| 性質 | 私募新創 | 私募新創 | 上市公司 (NYSE: NET) |
| 估值/市值 | $9.3B(2025 估值) | ~$2.5B(2021 估值,未更新) | 約 $109B(2026/8 市值) |
| 營收 | ~$340M ARR(2026/2) | 未公開,估計遠低於 Vercel | 2026 全年預估 $2.86B–2.87B |
| 員工數 | 數百人規模 | 更小 | 約 5,156 人 |
結論: Cloudflare 是市值破千億美元的上市巨頭,營收規模是 Vercel 的近 10 倍。Vercel/Netlify 屬於專注前端部署的新創,Cloudflare Pages 只是其龐大網路基礎設施(CDN、安全、邊緣運算)業務中的一小塊產品線,不能直接類比公司規模。