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.

Timeline of the Hugo version drift that broke the build Four events. Month 0, CI installs Hugo 0.111 with the version set to latest and caches the binary. Months 1 to 13, the cache keeps 0.111 while Hugo releases continue. Month 14, an unrelated lockfile change invalidates the cache and CI installs 0.139. The build fails with eleven errors on a commit that only changed a blog post. How the silent upgrade happened CI installs 0.111 Cache hides releases Cache key changes Build fails Month 0 version 'latest' Months 1–13 28 releases skipped Month 14 installs 0.139 Month 14 11 errors on a post edit
Nothing in the failing commit touched templates or config; the Hugo version was the only change.

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:

ErrorCauseFix
paginate key removedOld pagination config[pagination] pagerSize = 10
.Site.Author can't evaluateRemoved site methodMove author to params.author; use .Site.Params.author
.Site.Social can't evaluateRemoved site methodparams.social.twitter
.Site.IsServer removedMoved to hugohugo.IsServer
getJSON deprecatedReplaced by resources.GetRemoteresources.GetRemote + transform.Unmarshal
.Paginate with optionsSignature changedPass 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.

Number of changed files in the output by kind of difference Horizontal bars. Files that changed only because fingerprinted asset names changed 1,840. Processed images with slightly different byte sizes 612. RSS feeds with a different item count 34. Pages with genuinely different content 0 after the fixes. What differed in the output after the upgrade Asset fingerprints only 1840 Image bytes differ 1–3% 612 RSS item count 34 Content differences 0
diff of the 0.111 and 0.139 outputs after normalising hashes; only the RSS change needed a fix.

Step 5: Pin Hugo Everywhere

The root cause was an unpinned version. Three changes pin it:

  1. CI installs an exact version: hugo-version: '0.139.0'.
  2. 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
  1. Local development uses the same version through a .hugo-version file read by a small make target, or through go run github.com/gohugoio/hugo@v0.139.0 in 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
Process for regular, deliberate Hugo upgrades Five steps. Renovate opens a pull request when a Hugo release appears, bumping the CI version, the config range and the local version file together. CI builds with info logging. Any deprecation message fails the check. The writer or developer fixes the deprecation in the same pull request. The output diff against main is reviewed and the upgrade merges. Upgrading Hugo on purpose Renovate PR bumps all 3 pins Build with info logs on the PR Deprecation found? fail the check Fix in the PR usually minutes Diff and merge output reviewed
Small, frequent upgrades replace one large and surprising one.

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

MeasureBeforeAfterSource
Hugo version in CIlatest (cached 0.111)0.139.0 pinnedWorkflow
Errors on upgrade11 at once0–2 per Renovate PRCI history
Time to fix the upgrade~1 day (incident)~15 min per PRCommit timestamps
Writers on mismatched Hugo1 of 30 (config range enforced)—
Build time, full site14.2 s (0.111)11.8 s (0.139)hyperfine

Pitfalls & Rollback

  • hugo-version: latest in 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 deprecated lines. They are next quarter's errors.
  • Rollback: pin CI back to the old version; the config's max range 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.

Filed underFixesHugo