Cache busting CSS in Hugo with fingerprinting
Back in 2014 I wrote about forcing automatic reload of cached resources by appending a build timestamp as a query parameter to stylesheet URLs. The problem hasn’t gone away - browsers still aggressively cache CSS and JS - but Hugo has a built-in solution that’s cleaner than managing timestamps by hand.
I found out about this problem (again) as I migrated this statically-generated site from Jekyll to Hugo .
The problem
When you deploy a CSS change, visitors who already have the old file cached may not see it for hours or days,
depending on the Cache-Control / Expires headers your server sends. A hard refresh fixes it for you locally,
but you can’t ask every visitor to do that.
The classic symptom: your layout looks broken after a deploy, but only for some users.
The naive approach and why it fails
If you put your stylesheet in Hugo’s static/ directory and link it directly:
<link rel="stylesheet" href="/css/custom.css" />
There is no cache busting at all. The filename never changes, so browsers keep serving the cached version.
You could append Hugo’s .Site.Params build time or a version number manually, but that requires discipline
to bump on every deploy and is easy to forget.
Hugo’s asset fingerprinting
Hugo’s asset pipeline solves this properly. It hashes the file contents and embeds that hash in the filename, so the URL changes automatically whenever the file changes — and stays the same when it doesn’t.
Two things are required:
- The CSS file must live in
assets/(notstatic/), so Hugo can process it. - Use
resources.Getandresources.Fingerprintin your template.
The template partial looks like this:
{{ $css := resources.Get "/css/custom.css" }}
{{ $style := $css | resources.Fingerprint "sha256" }}
<link rel="stylesheet" href="{{ $style.RelPermalink }}" />
Hugo will output something like:
<link rel="stylesheet" href="/css/custom.abc123def456.css" />
The hash is derived from the file contents. Change a single character in the CSS and the hash - and therefore the URL - changes on the next build. The old URL is simply never requested again.
The gotcha: assets/ vs static/
The most common mistake (and the one I ran into when I was trying this approach) is leaving the CSS file in static/css/ while trying to
use resources.Get. Hugo’s asset pipeline only processes files under assets/. If the file is in static/,
resources.Get returns nil and the build silently produces a broken stylesheet link.
Move the file:
static/ ← files here are copied as-is, move them to:
assets/
css/
custom.css ← Hugo's asset pipeline can fingerprint this
After moving custom.css to assets/css/, the template above works and every hugo build and deploy with a CSS change
gets a new URL automatically.
You can check out the source of this page and find a similar custom.abc123def456.css stylesheed linked in <head>.