Fixing Hugo Builds That Fail After a Version Upgrade
Hugo releases often — usually several minor versions a month — and it removes deprecated features on a schedule. That keeps Hugo lean, but it means a site that last upgraded a year ago will fail on the latest version with errors like these:
ERROR deprecated: site config key paginate was deprecated in Hugo v0.128.0 and subsequently removed. Use pagination.pagerSize instead.
ERROR render of "page" failed: "layouts/_default/single.html:14:13": execute of template failed:
template: _default/single.html:14:13: executing "main" at <.Site.Author>: can't evaluate field Author in type page.Site
Worse, the upgrade often is not deliberate. A CI workflow using peaceiris/actions-hugo with hugo-version: latest, a Docker image tagged latest, or a laptop that ran brew upgrade will pick up a new release without anyone changing the repository. This guide fixes the errors a company blog hit jumping from Hugo 0.111 to 0.139, explains how to read them, and pins the version so the next upgrade is a choice. It belongs to Hugo Build Times for Large Repositories.
What Happened
The blog's CI installed Hugo with hugo-version: 'latest'. It had been building on 0.111 for 14 months because the CI cache kept the old binary; when the cache key changed after a lockfile update, the workflow installed 0.139 and the build failed with eleven errors. Locally, two writers on 0.111 saw no errors; a third, on 0.139 from Homebrew, saw the same eleven.
Step 1: Reproduce on Both Versions
Install the exact versions side by side so you can compare output, not just errors. Hugo publishes standalone binaries, and running go run github.com/gohugoio/hugo@v0.111.3 also works for a quick check:
curl -sL https://github.com/gohugoio/hugo/releases/download/v0.111.3/hugo_extended_0.111.3_linux-amd64.tar.gz | tar xz -C /tmp/h111 hugo
/tmp/h111/hugo --gc -d /tmp/out-111
hugo version # 0.139.0
hugo --gc -d /tmp/out-139 2>&1 | grep -E 'ERROR|WARN' | sort | uniq -c
Step 2: Read the Deprecation History With Info Logging
Jumping 28 releases skips the warnings that explain each removal. Building on an intermediate version with info logging shows them in plain language:
/tmp/h125/hugo --logLevel info 2>&1 | grep -i deprecat | sort -u
On 0.125, each future error appeared as an INFO deprecated: line with the replacement named. That list became the work plan.
Step 3: Fix Each Error
The eleven errors came from six causes:
| Error | Cause | Fix |
|---|---|---|
paginate key removed | Old pagination config | [pagination] pagerSize = 10 |
.Site.Author can't evaluate | Removed site method | Move author to params.author; use .Site.Params.author |
.Site.Social can't evaluate | Removed site method | params.social.twitter |
.Site.IsServer removed | Moved to hugo | hugo.IsServer |
getJSON deprecated | Replaced by resources.GetRemote | resources.GetRemote + transform.Unmarshal |
.Paginate with options | Signature changed | Pass page size from config |
The getJSON replacement needed the most care, because resources.GetRemote caches differently and returns a resource, not data:
{{ with resources.GetRemote "https://api.example.com/status.json" }}
{{ with .Err }}{{ warnf "status API: %s" . }}{{ else }}
{{ $status := . | transform.Unmarshal }}
<p>API status: {{ $status.state }}</p>
{{ end }}
{{ end }}
.Err must be handled explicitly; an unhandled fetch error fails the build in recent versions, which is usually what you want for required data and not for decorative data like this status line.
Step 4: Compare the Output, Not Just the Build
Once the build passes on 0.139, compare against the 0.111 output:
diff -rq /tmp/out-111 /tmp/out-139 | wc -l
diff -r /tmp/out-111 /tmp/out-139 | grep '^[<>]' | sed 's/[0-9a-f]\{64\}/HASH/g' | sort | uniq -c | sort -rn | head
Three kinds of difference appeared. Fingerprinted asset names changed because the minifier's output changed slightly — expected. Image processing output differed in byte size by 1–3% because the default WebP encoder settings changed — acceptable after checking a few visually. And RSS feeds listed 20 items instead of 10, because the RSS limit had previously been inherited from the removed paginate setting; it was set explicitly with [services.rss] limit = 10.
Step 5: Pin Hugo Everywhere
The root cause was an unpinned version. Three changes pin it:
- CI installs an exact version:
hugo-version: '0.139.0'. - The site config declares the version range it supports, so a mismatched local Hugo fails fast with a clear message:
[module.hugoVersion]
min = "0.139.0"
max = "0.139.99"
extended = true
- Local development uses the same version through a
.hugo-versionfile read by a smallmaketarget, or throughgo run github.com/gohugoio/hugo@v0.139.0in the npm scripts, so nobody depends on whatever Homebrew installed.
Step 6: Upgrade on Purpose, Regularly
Pinning without upgrading only postpones the same problem. The blog now runs Renovate with a custom regex manager that watches Hugo releases and opens a pull request bumping all three pinned places at once. The CI build on that pull request runs with --logLevel info and fails on any deprecated message, which turns each future removal into a small fix made months before it becomes an error:
hugo --gc --logLevel info 2>&1 | tee build.log
! grep -q 'deprecated' build.log
Deprecations in Themes and Modules
Five of the eleven errors were in the third-party theme the blog used, not in its own templates. Two options exist when a theme uses a removed feature: override the affected template in the site's own layouts, or update the theme. The theme's maintainer had already fixed both in a release eight months earlier, so updating the theme module removed five errors at once — a reminder to check theme updates before fixing theme errors by hand. Where a theme is unmaintained, a site-level override is fine as a stopgap, but every override is a copy that must be maintained through the next Hugo upgrade too, so record each one in the README with the Hugo version that required it.
Measured Impact
| Measure | Before | After | Source |
|---|---|---|---|
| Hugo version in CI | latest (cached 0.111) | 0.139.0 pinned | Workflow |
| Errors on upgrade | 11 at once | 0–2 per Renovate PR | CI history |
| Time to fix the upgrade | ~1 day (incident) | ~15 min per PR | Commit timestamps |
| Writers on mismatched Hugo | 1 of 3 | 0 (config range enforced) | — |
| Build time, full site | 14.2 s (0.111) | 11.8 s (0.139) | hyperfine |
Pitfalls & Rollback
hugo-version: latestin CI. Pin an exact version.- Jumping many releases at once. Step through intermediate versions with info logging to see the deprecation messages.
- Fixing errors without diffing output. Defaults such as RSS limits can change silently.
- Ignoring
INFO deprecatedlines. They are next quarter's errors. - Rollback: pin CI back to the old version; the config's
maxrange must be relaxed in the same commit or the old version will refuse to build.
Conclusion
Hugo upgrade failures are almost always deprecations that were warned about for months, delivered all at once by an unpinned version. Reproducing on both versions, reading the deprecation history on an intermediate release, fixing each removed feature, diffing the output and then pinning Hugo in CI, config and local tooling turns the next upgrade into a routine pull request.
FAQ
Why did my Hugo build break when nothing in the repository changed?
Usually because the Hugo version changed underneath it. CI actions and container images that install the latest Hugo, or a developer's Homebrew upgrade, pick up a new release with deprecations turned into errors. Pinning the exact Hugo version in CI and declaring a minimum and maximum version in the site configuration prevents silent upgrades.
How does Hugo deprecate features?
In stages. A deprecated function or setting first logs an INFO message, then a WARN in a later release, and finally an ERROR that fails the build, typically over several minor versions. Running builds with log level info and treating deprecation warnings as failures in CI surfaces them long before they become errors.
What are the most common breaking changes in recent Hugo versions?
Moving from the deprecated paginate setting and .Paginator options to the pagination configuration block, removed or renamed template functions and site methods such as .Site.Social and .Site.Author, changes to resources and image processing defaults, and the switch to the new taxonomy and term page kinds. Each produces a clear error message naming the function or setting.
Should I upgrade Hugo one version at a time?
When the gap is large, yes, or at least in steps through releases that turned deprecations into errors. Upgrading from a version a year old directly to the latest gives you all the errors at once without the earlier warnings that explain them. Stepping through two or three intermediate versions makes each error easier to understand.
Related
- Parent: Hugo Build Times for Large Repositories — running Hugo at scale.
- Sharing a Theme Across Hugo Sites With Modules — the module config that declares supported versions.
- GitHub Actions for Hugo Deployments — the CI workflow to pin.
- Fixing Jekyll Builds That Break After a Ruby Upgrade — the same failure mode in Jekyll.
- Scoring SSG Long-Term Maintenance Risk — release cadence as a maintenance factor.