Hugo's Image Processing Pipeline — Worth It?

Mar 22, 2026

Hugo image processing

While building out this site I looked at switching the post images over to Hugo’s built-in image processing pipeline. Here’s what that means, what the trade-offs are, and why we kept things simple.


What Is the Image Pipeline?

Hugo has an extended build mode that can process images at build time. Instead of serving images as dumb static files, Hugo can:

The result is that a 6MB iPhone photo becomes a 150KB WebP for mobile and a 300KB JPEG fallback for older browsers — automatically, every build.

The gallery on this site already uses this. Each photo is processed into a 300×300 thumbnail and a 1600px large version:

{{ range resources.Match "images/gallery/*" }}
  {{ $thumb := .Fill "300x300 q75" }}
  {{ $large := .Fit "1600x1600 q85" }}
{{ end }}

It works well there because you’re loading 20+ images at once and the size savings add up quickly.


What We’d Need to Change for Posts

Post images currently live in static/images/ and are referenced in markdown like this:

![A snowy mountain](/images/chamonix-town-evening.jpeg)

Hugo serves these verbatim — no processing, no resizing, no WebP. To use the pipeline, images would need to move to assets/images/posts/ and be referenced via resources.Get in a template, not directly in markdown.

The rendered output with the pipeline would look like this:

<picture>
  <source type="image/webp"
    srcset="/images/posts/photo_400.webp 400w,
            /images/posts/photo_800.webp 800w"
    sizes="(max-width: 30em) 100vw, 70vw">
  <source
    srcset="/images/posts/photo_400.jpg 400w,
            /images/posts/photo_800.jpg 800w"
    sizes="(max-width: 30em) 100vw, 70vw">
  <img src="/images/posts/photo_800.jpg"
       width="800" height="600"
       loading="lazy">
</picture>

Browser gets WebP if supported, appropriately sized for screen width, with lazy loading. Genuinely good.


The Catch

The simple markdown syntax for images would stop working for processed images. You’d need a custom shortcode instead:

<!-- Before (works anywhere, any editor) -->
![caption](/images/photo.jpeg)

<!-- After (Hugo-specific shortcode) -->
{{< img "images/posts/photo.jpeg" "caption" >}}

That shortcode would then call resources.Get, resize, generate WebP variants, and output the full <picture> block.

This has two real downsides:

  1. It’s Hugo-specific — the content is no longer portable plain markdown. Moving to a different static site generator later means rewriting every image reference.

  2. It changes the mental model for adding content — images go in a different folder, use a different syntax, and if you get the path wrong (e.g. leading slash vs no leading slash) nothing breaks visibly, it just silently falls back to no image.


Why We Didn’t Switch

For a personal blog at this scale, the main wins are already covered:

The pipeline is genuinely useful for high-traffic sites or image-heavy pages. For this site, keeping the authoring workflow straightforward is worth more than the marginal performance gain on post images.


If You Did Want to Add It

The path of least resistance would be to create a shortcode at layouts/shortcodes/img.html:

{{ $path := .Get 0 }}
{{ $alt  := .Get 1 | default "" }}
{{ $img  := resources.Get $path }}
{{ if $img }}
  {{ $sm := $img.Resize "600x q80" }}
  {{ $lg := $img.Resize "1200x q85" }}
  {{ $sm_webp := $img.Resize "600x q80 webp" }}
  {{ $lg_webp := $img.Resize "1200x q85 webp" }}
  <picture>
    <source type="image/webp"
      srcset="{{ $sm_webp.RelPermalink }} 600w, {{ $lg_webp.RelPermalink }} 1200w"
      sizes="(max-width: 60em) 100vw, 70vw">
    <source
      srcset="{{ $sm.RelPermalink }} 600w, {{ $lg.RelPermalink }} 1200w"
      sizes="(max-width: 60em) 100vw, 70vw">
    <img src="{{ $lg.RelPermalink }}"
         width="{{ $lg.Width }}"
         height="{{ $lg.Height }}"
         alt="{{ $alt }}"
         loading="lazy">
  </picture>
{{ else }}
  <img src="{{ $path }}" alt="{{ $alt }}" loading="lazy">
{{ end }}

Put images in assets/images/posts/, then in markdown:

{{< img "images/posts/photo.jpeg" "A snowy mountain" >}}

The {{ else }} fallback means if resources.Get can’t find the image it degrades gracefully rather than breaking the page. If you ever decide the trade-off is worth it, that’s the starting point.

#hugo #images #performance #meta