Responsive Images in Hugo with Leaf Bundles and Build-Time Processing
Hugo leaf page bundles keep images beside their Markdown files. Combined with build-time image processing, they let you ship responsive srcset images without an external service.
11 Oct 2026, 07:50 UTC

The problem: image pipelines outside Hugo
When you add an image to a Hugo post, the default path is often unsatisfying. You either commit a single full-size file and let the browser scale it, or you hand off resizing to a CDN or external image service. Both approaches move work away from the build step. If you want a static site with no runtime image dependency, you need a way to generate derivatives during hugo execution.
Leaf bundles keep assets next to their content
Hugo's page bundles solve the organizational half of this problem. A leaf bundle is a folder that contains exactly one Markdown file named index.md and any asset files you want to associate with it. For example:
content/posts/my-post/index.md
content/posts/my-post/cover.jpg
Any file living beside index.md becomes a page resource. In templates, you access it through .Resources:
{{ $cover := .Resources.GetMatch "cover.*" }}
This keeps each article self-contained. You can move the folder, rename it, or copy it to another Hugo project without breaking image paths.
Build-time processing produces srcset-ready files
Once you have a resource, Hugo can transform it during the build. Methods such as .Resize, .Fit, .Fill, and .Crop return a new derived resource with its own Width, Height, and Permalink. Hugo writes the processed file to the output directory only when that permalink is referenced in a template.
A common pattern is to generate several widths so the browser can choose via the srcset attribute. The exact option string format—such as "400x webp q80"—can vary between Hugo releases, so confirm the syntax for your target version before relying on it.
A worked srcset example
The following template snippet demonstrates the pattern. It assumes a leaf bundle with a file matching cover.* and generates three WebP widths.
{{ $cover := .Resources.GetMatch "cover.*" }}
{{ if $cover }}
{{ $srcset := slice }}
{{ range slice 400 800 1200 }}
{{ $img := $cover.Resize (printf "%dx webp q80" .) }}
{{ $srcset = $srcset | append (printf "%s %dw" $img.Permalink $img.Width) }}
{{ end }}
<img
src="{{ $cover.Permalink }}"
srcset="{{ delimit $srcset ", " }}"
sizes="(max-width: 600px) 400px, 800px"
alt="Cover image"
>
{{ end }}
Each .Resize call produces a distinct file under public/. Because the transformation happens at build time, the output is deterministic and requires no server-side image processor.
The git trade-off: originals versus cache
The central engineering decision is what you commit to version control. Hugo caches processed images, typically in a resources/ directory or a configured cache location. Committing only the original images keeps the repository lean, but every build—including CI—must regenerate derivatives. Committing the cache speeds up builds, yet it bloats repository history and can introduce stale artifacts if source images change.
Neither choice is universally correct. A lean repository with a clean CI rebuild is usually easier to reason about, provided your build time budget tolerates the regeneration cost.
Limitations and practical verification
This approach has boundaries. WebP and AVIF output depend on the Hugo version and its underlying imaging library; verify support before promising modern formats. Large original images still consume repository space even when derivatives are cached, so downsize sources before committing. And template syntax for resize options has changed across Hugo releases.
To check the behavior on your own project, run these commands from the project root:
hugo version— confirm the release you are targeting.hugo server— start the development server and inspect the emittedimgandsrcsetpaths underpublic/to confirm derivatives are generated. No special permissions are required.rm -rf resources/— clear the cache and rebuild to observe regeneration cost. The risk is a slower next build; the check is that files reappear correctly.
For CI, build in a clean container without a pre-warmed cache to confirm the pipeline works from scratch.
Actionable takeaway
If your Hugo site currently ships full-size images or depends on an external image service, convert one post into a leaf bundle and add a srcset loop. Verify the generated paths locally, measure the build-time cost, and then decide whether your repository should carry the original images only or also the processed cache. That decision depends on your CI budget and repository size tolerance, not on Hugo's capabilities.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.