Fix Hugo Preview Build Failures and English 404s on Cloudflare Pages

Fixing a Hugo Version Mismatch and a Broken English Page on Cloudflare Pages Preview Deployments

* This page contains promotional content

After I published the English version under /en/ in Publishing an English Version of a Hugo Blog at a Separate /en/ URL , I tried to send non-main branches to Cloudflare Pages Preview and hit two problems in a row.
main (production) was succeeding without issue, so at first I had no idea where the cause was.

  1. The Preview build fails with permalink ill-formed
  2. After deploy, clicking “English” in the menu returns 404

The root of both was that Cloudflare Pages manages environment variables separately for Production and Preview. I worked through them one at a time.


1. Preview would not deploy

Symptom

When I pushed a branch other than main, the Cloudflare build failed with the following error.

Error: error building site: failed to create resource spec: error expanding "/:contentbasename/": permalink ill-formed
Failed: build command exited with code: 1

main (production) succeeds.

Cause

The permalink in hugo.toml is /:contentbasename/. This token was added in Hugo 0.144.0, and older Hugo treats it as unknown and raises permalink ill-formed.

DeployEnv var usedHugoResult
mainProduction HUGO_VERSION0.164.0 (0.144 or later)Success
other branchesPreview HUGO_VERSIONunset, or an old value such as 0.130.0permalink ill-formed

When I first moved this blog from Netlify, HUGO_VERSION was 0.130.0. Later a theme update replaced :filename with :contentbasename, and I had raised Hugo only on production. Preview had not followed and was still old.

Fix

In the Cloudflare dashboard → the Pages project → Settings → Environment variables, set the same HUGO_VERSION on Preview as on Production (0.144 or later. Matching production at 0.164.0 is the safe choice).
To confirm, look at Installing Hugo 0.xx.x at the top of the failed log. If it is below 0.144, this is the cause.

With this fix, the Preview build succeeded.


2. English did not switch after deploy

Symptom

The Preview site menu showed “English”. Clicking it returned 404. hugo server locally did not reproduce the problem.

Cause

The English pages themselves existed on Preview. The link destination was wrong.

The language switcher uses .Permalink (an absolute URL).

<a class="nav-link" href="https://scribble.washo3.com/en/" lang="en" hreflang="en">English</a>

The Japanese menu uses relLangURL, so it becomes a relative path such as /about and stays inside the Preview origin. English alone had become an absolute URL pointing at hugo.toml’s baseurl (https://scribble.washo3.com/).

The actual destination was https://scribble.washo3.com/en/. Production main did not have /en/ yet, so it 404’d.

https://<hash>.<project>.pages.dev/en/   -> 200
https://scribble.washo3.com/en/               -> 404

One step before the cause was that I had manually overwritten the environment variable CF_PAGES_URL with the production domain. Cloudflare injects the correct Preview URL on each deploy, but the dashboard value takes precedence. Even with hugo -b $CF_PAGES_URL in the build command, that overwrite becomes baseURL as-is.
The reason it did not reproduce locally was that hugo server sets baseURL to localhost.

Remedy

  1. Remove the manual CF_PAGES_URL setting from the Preview environment variables
  2. Use $CF_PAGES_URL as-is in the build (use the Cloudflare-injected value)

With this remedy, English pointed at /en/ inside Preview, and the 404 went away.
If I only want to look right now, I can also confirm by appending /en/ directly to the Preview URL.


3. Final build setup

Environment variables

VariableProductionPreviewNotes
HUGO_VERSION0.164.0 (same as production)0.164.0 (same as production)Required on both. Below 0.144, :contentbasename breaks
CF_PAGES_URLDo not set manuallyDo not set manuallyCloudflare injects the deploy URL automatically

Build command

The previous git fetch --unshallow && hugo --gc --minify -b $CF_PAGES_URL still made lastmod and Preview English switching work. Two points remained, though.

  • git fetch --unshallow fails when the clone is already full, and && never reaches Hugo
  • Production $CF_PAGES_URL is *.pages.dev, not the custom domain, so sitemap and hreflang would point at pages.dev

So I decided to put the following single line in the dashboard Build command field.

if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then git fetch --unshallow; fi && if [ "$CF_PAGES_BRANCH" = "main" ]; then hugo --gc --minify; else hugo --gc --minify -b "$CF_PAGES_URL"; fi
  • Unshallow only when the clone is shallow (for enableGitInfo lastmod)
  • main uses hugo.toml’s baseurl = https://scribble.washo3.com/ as-is
  • Other branches use Cloudflare-injected $CF_PAGES_URL

The Build command field is executed as a single shell command, so you cannot paste it as multiple lines. You can join with ;, or put build.sh in the repository and write bash build.sh in the field.

#!/bin/bash
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
  git fetch --unshallow
fi
if [ "$CF_PAGES_BRANCH" = "main" ]; then
  hugo --gc --minify
else
  hugo --gc --minify -b "$CF_PAGES_URL"
fi

./build.sh can fail for lack of the execute bit, so writing bash build.sh is apparently safer.

What the flags mean

ElementRole
git fetch --unshallowCloudflare checks out with a shallow clone. Correct lastmod via enableGitInfo needs the full history
hugo --gc --minifyThe usual CI flags. --enableGitInfo is in the toml, so it does not need to be added
-b $CF_PAGES_URLAbsolute URL for the English menu and hreflang on Preview. Do not use it on production

Summary

ItemDetail
Symptom 1Preview build fails with permalink ill-formed
Cause 1Preview HUGO_VERSION is older than Production, so it cannot interpret the :contentbasename token (Hugo 0.144~)
Fix 1Align Preview HUGO_VERSION with the Production value
Symptom 2The English link on Preview jumps to the production domain and 404s
Cause 2I had manually overwritten CF_PAGES_URL with the production domain
Fix 2Remove the manual CF_PAGES_URL setting and use the Cloudflare-injected value as-is

The cause of both was that I had forgotten Production and Preview environment variables are managed separately.
When using Cloudflare Pages for a multilingual Hugo site, it seems useful to suspect first whether the environment variables match on both environments, or conversely whether you have touched them too much.

References

See also