Deploy Hugo to Cloudflare Pages (Production Config)
Git-connected build, custom domain, cache headers, and preview branches. Faster than Vercel for static Hugo.
Tested on: Cloudflare Pages, Hugo 0.139.4-extended, PaperMod submodule Result: Global TTFB ~45ms (vs 180ms on previous host). Unlimited bandwidth.
Connect repository
Cloudflare Dashboard → Workers & Pages → Create → Connect Git → select repo.
Build settings:
| Field | Value |
|---|---|
| Framework preset | None |
| Build command | hugo --gc --minify |
| Build output directory | public |
| Root directory | (empty) |
Environment variables (Production + Preview):
HUGO_VERSION = 0.139.4-extended
HUGO_ENV = production
NODE_VERSION = 20
Submodule theme
Build command override:
git submodule update --init --recursive && hugo --gc --minify
First deploy failed without this — same as Vercel submodule issue.
baseURL per environment
hugo.toml:
baseURL = "https://myblog.com/"
Preview URLs differ. Cloudflare sets CF_PAGES_URL:
# Not native — use build script
scripts/build.sh:
#!/usr/bin/env bash
set -euo pipefail
git submodule update --init --recursive
if [ -n "${CF_PAGES_URL:-}" ]; then
export HUGO_BASEURL="$CF_PAGES_URL/"
fi
hugo --gc --minify
Build command: bash scripts/build.sh
Custom domain
Pages → Custom domains → Add myblog.com + www.
Cloudflare auto-provisions TLS. Set DNS CNAME to myblog.pages.dev.
Enable Always Use HTTPS.
Cache headers
static/_headers:
/assets/*
Cache-Control: public, max-age=31536000, immutable
/*.css
Cache-Control: public, max-age=604800
/
Cache-Control: public, max-age=3600
Cloudflare Pages respects _headers from output (public/_headers).
Wrangler optional deploy
npm i -D wrangler
npx wrangler pages deploy public --project-name=myblog
Errors encountered
Failed: build output directory public not found
Hugo failed earlier in log — scroll up for real error. Mine was missing extended.
404 on /blog/page/2/
Trailing slash config:
[permalinks]
blog = "/blog/:slug/"
[uglyURLs]
# leave false
Cloudflare serves directories with index.html — trailing slash matters.
Verify
curl -I https://myblog.com/
# cf-cache-status: HIT (second request)
curl -I https://myblog.com/assets/css/stylesheet.*.css
# cache-control: immutable
Run Lighthouse on production URL — compare TTFB.
Preview branches
PR #42 → https://abc123.myblog.pages.dev — share with editors before merge.
Why I switched from Vercel
- No bandwidth anxiety on viral post
_headerswithout paid plugin- Same account as DNS + WAF
Build time similar (~45s). Deploy hook integrates with GitHub Actions for status checks.