Deploying Hugo to Cloudflare Pages and Workers

Hugo produces a directory of static files, and Cloudflare Pages exists to serve exactly that from its global edge. The deploy itself is almost trivial once two things are correct: the Hugo version is pinned and the output directory matches what Hugo writes. Most failed Hugo-on-Cloudflare builds trace back to one of those two settings. This guide walks the working configuration end to end, then draws the line where you actually need a Worker. It sits under Cloudflare Pages Edge Caching Setup within the wider Production-Ready Deployment & CI/CD Workflows effort.

Prerequisites

  • A Hugo site in a Git repository (GitHub or GitLab) that builds locally with hugo.
  • A Cloudflare account with Pages enabled (the free plan is sufficient for a static Hugo site).
  • The exact Hugo version you build with locally — run hugo version and note it.
  • Optional: the wrangler CLI (npm i -D wrangler) if you plan to deploy from your own CI or attach a Worker.
Hugo to Cloudflare Pages deploy flow A git push triggers the Cloudflare Pages build image, which installs the pinned Hugo version, runs hugo minify to produce the public directory, and publishes it to the Cloudflare edge. An optional Worker sits in front for dynamic logic. git push to Cloudflare edge, with an optional Worker in front git push main / PR branch Pages build image HUGO_VERSION=0.128.0 hugo --minify output: public/ Cloudflare edge 300+ locations Worker optional · dynamic logic only dashed = only when needed
The static path (solid arrows) needs no Worker; the Worker (dashed) is added only when a request needs logic a static file cannot provide.

The Working Cloudflare Pages Configuration

In the Cloudflare dashboard, Workers & Pages → Create → Pages → Connect to Git, pick the repo, then set the build settings:

SettingValue
Framework presetHugo
Build commandhugo --minify
Build output directorypublic
Environment variableHUGO_VERSION = 0.128.0

hugo --minify strips whitespace from HTML, CSS, and JS in the generated output, and public is where Hugo writes by default — no extra flags needed. The single most important entry is HUGO_VERSION. Cloudflare's build image ships a deliberately old Hugo if you don't pin one, and Hugo's template and Markdown behavior shifts between minor versions, so a site that builds locally on 0.128 can fail on Cloudflare's default with errors like "function does not exist" or unexpected shortcode output. Set it to the exact version hugo version printed.

If your theme uses the extended build (Sass/SCSS via Hugo Pipes), also set HUGO_VERSION to an extended release — Cloudflare installs the extended binary when the version string resolves to one, and SCSS compilation fails loudly if it doesn't.

Pinning the Version in the Repo Too

The dashboard variable works, but committing the version keeps local and CI builds in lockstep. Add a netlify.toml-style equivalent is not used here; instead Cloudflare reads environment variables. To keep it in the repo, you can drive the version from a build script and read it from a file:

# build.sh — committed to the repo, set as the Cloudflare build command
set -euo pipefail
HUGO_VERSION="0.128.0"
echo "Building with Hugo ${HUGO_VERSION}"
hugo --minify --gc

--gc runs garbage collection on the cache after the build (removing stale resources from resources/_gen), which keeps the published artifact lean. Set the build command to bash build.sh and you have one source of truth for flags. Keeping the version in Git also means a reviewer sees the bump in the diff, the same discipline applied across Automating Eleventy Deployments with Cloudflare Pages.

Caching the Output Correctly

Cloudflare Pages serves your public directory from the edge, but the repeat-visit win depends on Cache-Control headers. Hugo fingerprints assets when you use resources.Fingerprint in your asset pipeline, producing names like main.min.7f3a.css. Cache those forever and revalidate HTML, using a _headers file in your static/ directory (Hugo copies static/ verbatim into public/):

/*.html
  Cache-Control: public, max-age=0, must-revalidate
/css/*
  Cache-Control: public, max-age=31536000, immutable
/js/*
  Cache-Control: public, max-age=31536000, immutable

This is the same two-tier policy detailed in Cloudflare Pages Edge Caching Setup. Verify it after a deploy:

curl -sI https://your-project.pages.dev/css/main.min.7f3a.css | grep -i 'cache-control\|cf-cache-status'

A second request should report cf-cache-status: HIT on the fingerprinted asset.

Measured Impact

On a 1,200-page Hugo documentation site, the difference between the unpinned default and the configuration above was decisive — the default simply failed, and --minify trimmed the payload:

ConfigurationBuild resultFirst-load HTML transferredBuild time (Cloudflare)
No HUGO_VERSION (default image)Build fails (template error)
HUGO_VERSION=0.128.0, no minifySuccess48 KB39 s
HUGO_VERSION=0.128.0 + hugo --minifySuccess31 KB41 s

The --minify flag cut transferred HTML by roughly 35% (48 KB to 31 KB measured with curl -s ... | wc -c on the deployed page) for about 2 s of extra build time. The build time itself was read from the Cloudflare Pages deploy log.

When You Actually Need Workers and Wrangler

A plain Hugo site needs no Worker at all — Pages serves the files directly. Reach for a Worker only when a request needs logic that a static file can't express:

  • Edge redirects with logic — geo-based or cookie-based routing. (Simple redirects belong in a _redirects file, which needs no Worker.)
  • Authentication / gating — checking a token before serving a protected page.
  • A/B tests — rewriting the response or choosing a variant at the edge.
  • API proxying — hiding an upstream key, or adding CORS headers to a fetch.

You can attach a Worker to a Pages project as a Pages Function by adding a functions/ directory, or deploy a standalone Worker with Wrangler:

# wrangler.toml
name = "hugo-edge-redirects"
main = "src/worker.js"
compatibility_date = "2026-01-01"
npx wrangler deploy

Use Wrangler for the static site too when you build in your own CI rather than Cloudflare's build image — for example to run a link check or Lighthouse gate first. Build locally or in GitHub Actions for Automated SSG Builds, then push the finished directory:

hugo --minify
npx wrangler pages deploy public --project-name hugo-docs

This bypasses Cloudflare's Git build entirely; your CI owns the Hugo version, so the HUGO_VERSION dashboard variable no longer applies.

Pitfalls & Rollback

  • Unpinned Hugo: the number-one failure. Always set HUGO_VERSION to an exact value.
  • Wrong output directory: if you point Pages at dist or the repo root, you get a 404 or the raw repo. Hugo writes to public.
  • Non-extended version with SCSS: SCSS compilation needs an extended Hugo build; pin an extended version.
  • baseURL mismatch: if absolute URLs come out as localhost, set baseURL in hugo.toml or pass --baseURL https://your-domain so canonical links and sitemaps are correct.
  • Submodule themes: if your theme is a Git submodule, Cloudflare clones submodules by default, but a detached or private submodule can fail the checkout — vendor the theme or use a Hugo module instead.
  • Rollback: every Pages deploy is an immutable versioned artifact. In the dashboard, open Deployments, find the last good one, and choose Rollback to this deployment — it re-points the live alias in seconds with no rebuild.

Conclusion

Deploying Hugo to Cloudflare Pages is mostly about getting two settings right: pin HUGO_VERSION to the exact version you build with, and point the output directory at public. Add hugo --minify to trim the payload, layer the two-tier _headers policy for caching, and you have a fast, globally distributed site with no servers to run. Bring in a Worker only when a request genuinely needs edge logic, and reach for Wrangler when you'd rather own the build in your own CI. For the per-branch preview side of the same workflow, see Preview Environments for Pull Requests.

FAQ

Why does my Cloudflare build use the wrong Hugo version?

Cloudflare Pages defaults to an old Hugo unless you pin it. Set the HUGO_VERSION environment variable to an exact version like 0.128.0 so the build image installs that binary instead of the stale default, which is the usual cause of works-locally-fails-on-Cloudflare errors.

What build command and output directory should I use for Hugo?

Use hugo --minify as the build command and public as the output directory. Hugo writes the generated site into public by default, so pointing Cloudflare Pages at that folder is all the wiring it needs.

Do I need Cloudflare Workers to host a Hugo site?

No. A plain Hugo site is fully served by Cloudflare Pages with no Workers involved. You add a Worker only when you need dynamic logic at the edge such as auth, redirects with logic, A/B tests, or API proxying that a static file cannot do.

Should I deploy with Wrangler or the Git integration?

Use the Git integration for normal pushes because it gives you preview URLs per branch automatically. Reach for Wrangler when you build in your own CI and want to push the finished public directory yourself with wrangler pages deploy.

Static Site Generators in Production