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.
- The Preview build fails with
permalink ill-formed - 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: 1main (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.
| Deploy | Env var used | Hugo | Result |
|---|---|---|---|
| main | Production HUGO_VERSION | 0.164.0 (0.144 or later) | Success |
| other branches | Preview HUGO_VERSION | unset, or an old value such as 0.130.0 | permalink 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/ -> 404One 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
- Remove the manual CF_PAGES_URL setting from the Preview environment variables
- Use
$CF_PAGES_URLas-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
| Variable | Production | Preview | Notes |
|---|---|---|---|
| HUGO_VERSION | 0.164.0 (same as production) | 0.164.0 (same as production) | Required on both. Below 0.144, :contentbasename breaks |
| CF_PAGES_URL | Do not set manually | Do not set manually | Cloudflare 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 --unshallowfails when the clone is already full, and&&never reaches Hugo- Production
$CF_PAGES_URLis*.pages.dev, not the custom domain, so sitemap and hreflang would point atpages.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
enableGitInfolastmod) - main uses
hugo.toml’sbaseurl = 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
| Element | Role |
|---|---|
git fetch --unshallow | Cloudflare checks out with a shallow clone. Correct lastmod via enableGitInfo needs the full history |
hugo --gc --minify | The usual CI flags. --enableGitInfo is in the toml, so it does not need to be added |
-b $CF_PAGES_URL | Absolute URL for the English menu and hreflang on Preview. Do not use it on production |
Summary
| Item | Detail |
|---|---|
| Symptom 1 | Preview build fails with permalink ill-formed |
| Cause 1 | Preview HUGO_VERSION is older than Production, so it cannot interpret the :contentbasename token (Hugo 0.144~) |
| Fix 1 | Align Preview HUGO_VERSION with the Production value |
| Symptom 2 | The English link on Preview jumps to the production domain and 404s |
| Cause 2 | I had manually overwritten CF_PAGES_URL with the production domain |
| Fix 2 | Remove 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
- Use a specific Hugo version
- Build configuration / CF_PAGES_URL
- Set build commands per branch
- Hugo GitInfo and shallow clone
- Hugo permalinks / :contentbasename