NNicheStackTutorials
← All tutorials
Hugo

Deploy Hugo to Cloudflare Pages (Production Config)

Git-connected build, custom domain, cache headers, and preview branches. Faster than Vercel for static Hugo.

·10 min read·Hugo
Deploy Hugo to Cloudflare Pages (Production Config) cover

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:

FieldValue
Framework presetNone
Build commandhugo --gc --minify
Build output directorypublic
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
  • _headers without paid plugin
  • Same account as DNS + WAF

Build time similar (~45s). Deploy hook integrates with GitHub Actions for status checks.

Tags: HugoCloudflare PagesDeploymentCDN