Easy way to serve responsive images with Hugo

For my photography portfolio I wanted to serve images that are appropriately-sized. I spent most of my Saturday afternoon building this with Hugo and I would like to share my completed solution to anyone who is interested to save time in the future.

I must say Hugo’s support for Image processing is pretty good. You do not need any other dependencies as this works out-of-the-box.

My requirements for a solution are:

  • Configure available image resolutions in a single place
  • Portrait photos should be resized differently from landscape.
  • Use Page Bundle to organize images
  • Allow browsers to select best image resolution without JS.


# Highest width is 2500px for landscape and 1500px for portrait.
# This will give both orientations roughly the same resolution and size.
# Every landscape image will also be scaled down to 1500px and 1000px,
# and portrait photos to 1000px and 750px wide.
landscapePhotoWidths = [2500, 1500, 1000]
portraitPhotoWidths = [1500, 1000, 750]

quality = 70

My photos are organized in “content/photograpy/NewYork/photos/”. This partial simply inserts an <img> tag. You can pass in additional attributes such as CSS class="photo" or sizes="90vw" by supplying an additional dictionary as value to the attrs key.

{{ with .Resources.ByType "image" }}
  {{ range $image := . }}
     {{ partial "responsive-image" (dict "Site" $.Site 
                                         "image" $image 
                                         "attrs" (dict "class" "portfolio-photo" )) }}
  {{ end }}
{{ end }}

The actual image processing work is in this partial.

{{ $image := .image }}

<!-- variables used for img tag -->
{{ $imgSrc := "" }}
{{ $imgSrcSet := slice }}

<!-- uses settings from config.toml depending on orientation -->
{{ $widths := $.Site.Params.landscapePhotoWidths }}
{{ if gt $image.Height $image.Width }}
  {{ $widths = $.Site.Params.portraitPhotoWidths }}
{{ end }}

  Add URL for each width to $imgSrcSet variable
  format: "/path/img_1000.jpg 1000w,/path/img_500.jpg 500w"
  Note: the first URL is used as "fallback" src in $imgSrc.
{{ range $widths }}
  {{ $srcUrl := (printf "%dx" . | $image.Resize).RelPermalink }}
  {{ if eq $imgSrc "" }}{{ $imgSrc = $srcUrl }}{{ end }}
  {{ $imgSrcSet = $imgSrcSet | append (printf "%s %dw" $srcUrl .) }}
{{ end }}
{{ $imgSrcSet = (delimit $imgSrcSet ",") }}

<!-- Format additional HTML attributes -->
{{ $attributes := slice }}
{{ range $name, $value := .attrs }}
  {{ $attributes = $attributes | append (printf "%s=%q" $name $value) }}
{{ end }}
{{ $attributes = (delimit $attributes " ") }}

<img src="{{ $imgSrc }}" srcset="{{ $imgSrcSet }}" {{ print $attributes | safeHTMLAttr }}>

That’s it! Feel free to modify to suit your needs.

Final thoughts: I had most difficulty working around the quirks of Go’s html/template libraries. It’s not all that bad, but demands a lot of patient by trial and error. If you look for existing examples in the Hugo community, there aren’t a lot. And if search instead for help in the Go community, you quickly get the advice to write a normal helper function and call that from your template.

I wish Hugo’s templating would allow for such extensions to be made by myself.

Tip: I do not want to upload my original high-res photos to the web. So I’m running this command to delete the original images from the /public directory before uploading my site.

# Do not upload original photos. Images processed by Hugo 
# contain a hash (fingerprint) in the filename
find -E public/ -regex '.*DSC[0-9]+\.jpg' -delete

This looks really useful! Thanks for sharing it!

It would be nice to have an “asset” directory in page bundles for original images!
** typos corrected **

2020 update: To avoid the need to delete original images from /public before uploading, one can now add the following to config.toml:

  publishResources = false

See the documentation for ‘build options’.

1 Like

Sorry, that should have an underscore, like this:

  publishResources = false

(For some reason, Hugo (v0.71.0-DEV/extended) now always seems to remove original images from my page bundles, whether I spell this option correctly or not. I’m not sure what else has changed.)

(it’s backticks :wink: ```)

Great Tip! Much Appreciated!

I would like to add (if helpful for some) an extra step in your

{{ image := .Resources.ByType “image” }}
{{ with $image }}
{{ range $index, $image := . }}
{{ if eq index 0}} {{ partial "imghp" (dict "Site" .Site “image” $image) }}
{{ end }}
{{ end }}
{{ end }}