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.
config.toml
# 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.
[params]
landscapePhotoWidths = [2500, 1500, 1000]
portraitPhotoWidths = [1500, 1000, 750]
[imaging]
quality = 70
layouts/photography/single.html
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 }}
layouts/partials/responsive-image.html
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.
hugo
# 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