Hugo Pipes Image Processing: Build Responsive Images Without External Tools
Hugo's extended binary includes libvips, letting you resize, crop, convert, and filter images at build time via Hugo Pipes — no external tooling required. This post walks through page bundles, transformation chaining, deterministic caching, a five-minute verification, and practical limits at scale.
09 May 2026, 13:24 UTC

The problem: responsive images without a separate pipeline
Static sites often need multiple image variants — thumbnails, WebP fallbacks, high-DPI versions — but adding a Node-based build step or an external CI job adds complexity and maintenance. Hugo's extended binary ships with libvips, so the same tool that renders your Markdown can also transform images at build time.
Thesis: Hugo Pipes makes image derivatives a first-class part of the site build
By declaring images as Page Resources (files that live next to their content file in a page bundle) or as global assets in assets/, you can call methods like .Resize, .Fill, .Fit, .Crop, and .Filter directly in templates. The processed file is written to resources/_gen/ and referenced through the returned Resource object's RelPermalink. No external tooling, no runtime image server.
Where images live and how to fetch them
- Page bundles – leaf or branch bundles. Access with
.Resources.GetMatch "hero*"or.Resources.ByType "image". - Global assets – any file under
assets/. Access withresources.Get "images/logo.png".
Both sources support identical processing chains, so you can reuse the same template logic for content images and site-wide graphics.
Transformation methods and chaining
Typical chain in a template:
{{ $src := .Resources.GetMatch "hero*" }}
{{ $webp := $src.Resize "800x webp" }}
{{ $thumb := $src.Fill "200x200 Center" | images.Filter "grayscale" }}
<img src="{{ $webp.RelPermalink }}" width="{{ $webp.Width }}" height="{{ $webp.Height }}" alt="Hero">
Resize "800x webp"– width 800 px, output WebP.Fill "200x200 Center"– crop to exact 200x200, centered.Filter "grayscale"– apply a grayscale filter.- Quality can be set globally (
images.quality = 75inhugo.toml) or per call (images.quality=60in the chain, since v0.121).
Deterministic caching and incremental builds
Each derivative's cache key includes the source file hash, the full transformation chain, and the Hugo version. Changing any parameter (size, format, quality) produces a new hash; unchanged derivatives are reused. This makes incremental builds fast — only the altered images are regenerated.
Practical tip: Commit the resources/_gen/ directory or, more commonly, cache it in CI (e.g., GitHub Actions actions/cache) so deploys don't reprocess the whole library.
Worked example: verify the pipeline in five minutes
- Create a test site (run in any directory you own):
hugo new site testimg && cd testimg && git init # add a sample post hugo new posts/demo.md - Add an image to the page bundle (replace the path with a real JPEG you have locally):
mkdir -p content/posts/demo cp /path/to/your/photo.jpg content/posts/demo/hero.jpg - Edit
layouts/_default/single.html(create the file if it doesn't exist) and paste:{{ $img := .Resources.GetMatch "hero*" }} {{ $r := $img.Resize "800x webp" }} <img src="{{ $r.RelPermalink }}" width="{{ $r.Width }}" height="{{ $r.Height }}" alt="Demo"> - Start the dev server (requires the extended Hugo binary; verify with
hugo versionshowing "extended"):hugo server --buildDrafts - Open
http://localhost:1317/posts/demo/. Inspect the <img> src — it should point to a file under/resources/_gen/images/…webp. Check the file exists on disk and note its size.ls -lh resources/_gen/images/ - Change the resize to
"400x jpg q50", rebuild (hugo --buildDrafts), and confirm a new derivative appears with a different hash and smaller file size.
Expected checks: the RelPermalink changes, the new file is present in resources/_gen/, and the browser loads a JPEG at 400 px width. Risk: if you run the standard (non-extended) Hugo binary, the .Resize call will panic — always confirm hugo version includes "extended".
Trade-offs and limitations
- Build time & memory – processing thousands of high-resolution images sequentially can slow the build. Hugo does not expose a worker-pool knob; parallelism is limited to concurrent template executions.
- No automatic
srcsetor<picture>– you must write the responsive markup yourself (e.g., generate 400w, 800w, 1200w derivatives and emit a<picture>with<source srcset>). - Animated formats – only the first frame of GIF/WebP/AVIF is transformed. Pre-process animations externally if you need multi-frame output.
- Color space – CMYK sources are not auto-converted to sRGB; convert beforehand for predictable results.
- Cache persistence –
resources/_gen/is not committed by default. Forgetting to cache it in CI forces a full reprocess on every deploy.
Actionable next step
Add one responsive hero image to a page bundle today. Use the Resize chain to emit WebP at two widths, then hand-write a <picture> block in your single template. Verify the output in the browser and confirm the cache directory is preserved in your CI configuration. That single change gives you modern image delivery without leaving the Hugo ecosystem.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.