Using responsive images effortlessly with Hugo

how to Hugo web development

There’s an inherent problem with the way majority of the people, including me, normally use the <img> tag to use images on their websites.

<img alt="An example image" src="example.jpg">

If you’re expecting it to simply show you an image, then you’d think what’s the problem over here, as it works perfectly. Well, the problem is not with the <img> tag, rather it is with our way of sourcing the image.

In case of static websites, if we use the above mentioned method, the same image will be sourced for clients of different viewports. What this means is that if our markup sourced a 4K image, the same 4K image would also be loaded on clients with small viewports like mobiles. This is an inefficiency which drastically affects website loading times as images contribute to a major chunk of bandwidth consumption.

So how should we tackle this? #

Obviously, this is not something which the people behind HTML were oblivious to. Which is why they gave us an option to define a set of source images for a single image. This allows the browser to choose an appropriate image from the set, allowing the same HTML markup to cater to different viewports statically.

Chris Coyier covers this topic really well in his Guide to the Responsive Images Syntax in HTML. He mentions various approaches, but the one which I will go for is using the <picture> tag.

The syntax #

The syntax is pretty straightforward. Inside the <picture> tag, we define <source> images, usually with a media rule through the media attribute. The browser then evaluates which image it should load based on the media rules specified.

<picture>
  <!-- Serve the original image for large screens -->
  <source
    srcset="example.jpg"
    media="(min-width: 1000px)"
  />
  <!-- Serve the 1000px version for viewports upto 1000px -->
  <source
    srcset="example-1000px.jpg"
    media="(min-width: 600px)"
  />
  <!-- Serve the 600px version for viewports upto 600px -->
  <img
    src="example-600px.jpg"
    alt="An example image"
  />
</picture>

The media rules above have been written in a “mobile first” convention.

That’s cool, but tedious #

The above approach is great, but the main drawback it has is just it’s sheer hugeness. Imagine having to write that every time we had to insert an image.

Apart from that, we also have to ensure the generation of “example-600px.jpg” and “example-1000px” which we had conveniently assumed to be present in the previous case.

Enter, Hugo partials and image processing #

A really simple solution to this problem is to use Hugo partials. They are great! They take in a set of parameters, and then spit out the respective markup during builds, all behind the scenes.

Using Hugo partials, we can simply write the following as an equivalent to the <picture> markup.

{{ with resources.Get "example.jpg" }}
  {{ partial "img" (dict "img" . "alt" "An example image") }}
{{ end }}

We can also use Hugo’s native image processing functions inside the partial to automatically generate the resized versions of “example.jpg” without having to bother with it ourselves.

Behold, the partial #

The img partial I ended up with takes the following parameters:

Argument Type Default value What it expects
img Image resource nil The image resource
alt String "" The alt text for the image
class String "" HTML classes to apply to the image
loading String "" Value for the HTML loading attribute
webpHint String “photo” Hint to be used for Hugo’s WebP conversion.
breakpoints Slice<int> [768, 992, 1200] Viewport breakpoints, in pixels
widths Slice<int> [768, 992, 1200, -1*] Widths to be used for each breakpoint, in pixels. width[i] will be used for the image when viewport width is in between breakpoints[i-1] and breakpoints[i+1].
quality int 75 Quality value to be used for Hugo’s image processing

*-1 implies that the width of the input image should be used. Additionally, -2 can be used to indicate that no image should be produced for that specific breakpoint.

I broke the partial into three parts, for simplicity.

layouts/partials/img.html #

This is the partial which we call directly. It’s a simple wrapper and sets some defaults for our arguments.

{{/* Set defaults */}}
{{ $opts := . | merge (dict
    "alt" ""
    "class" ""
    "loading" ""
    "webpHint" "photo"
    "breakpoints" (slice 768 992 1200)
    "widths" (slice 768 992 1200 -1)
    "quality" 75)
}}

{{/*
    Hugo doesn't seem to distinguish between image resources well.
    Therefore, pass the image path as another variant.
*/}}
{{ partialCached "_img" $opts . .img.Key }}

layouts/partials/_img.html #

This partial is responsible for generating the <picture> tag markup based on the passed breakpoints and widths.

{{ with .img }}
    <picture>
        {{ $opts := dict
            "img" $.img
            "quality" $.quality
            "webpHint" $.webpHint
        }}

        {{/* For each breakpoint, convert image to WebP and resize according to `$widths` */}}
        {{ range $i, $v := $.breakpoints }}
            {{ $width := index $.widths (add $i 1) | default -1 }}
            {{ $opts = dict "breakpoint" $v "width" $width | merge $opts }}
            {{ partialCached "__img" $opts $.img.Key $opts }}
        {{ end }}

        {{/* Handle the smallest breakpoint */}}
        {{ $width := index $.widths 0 }}
        {{ $width = cond (lt $.img.Width $width) -1 $width }}
        {{ $opts = dict "breakpoint" nil "width" $width | merge $opts }}
        {{ partialCached "__img" $opts $.img.Key $opts }}

        {{/* Original, unresized image as fallback */}}
        {{ $attributes := "" }}
        {{ range $i, $v := slice "class" "alt" "loading" }}
            {{ with index $ . }}
                {{ $attributes = printf `%s %s="%s"` $attributes $v . }}
            {{ end }}
        {{ end }}
        <img src="{{.RelPermalink}}" height="{{.Height}}" width="{{.Width}}"
            type="{{.MediaType}}" {{$attributes|safeHTMLAttr}} />
    </picture>
{{ end }}

layouts/partials/__img.html #

This partial resizes image based on the passed width. It also fills in the media rules based on the breakpoint argument.

{{/*
    Only include image if image width exceeds breakpoint width, and if breakpoint
    shouldn't be skipped.
*/}}
{{ $width := cond (eq .width -1) .img.Width .width }}
{{ if and (lt $width .img.Width) (ne $width -2) }}
    {{ with .img.Resize (printf "%dx webp q%d %s" $width .quality .webpHint) }}
        {{ $breakpoint := "" }}
        {{ with $.breakpoint }}
            {{ $breakpoint = printf "media=(min-width:%dpx)" . }}
        {{ end }}
        <source {{$breakpoint|safeHTMLAttr}} srcset="{{.RelPermalink}}" type="image/webp" />
    {{ end }}
{{ end }}

I’ve also added WebP conversion to the mix as a bonus.

Reaping the benefits of our hard work #

I admit that the partial does look a bit intimidating. But the complexity is simply a testament of the amazing returns we can hope out of it.

All we have to do now is use {{ partial "img" (dict "img" $img "alt" "Some alt text") }} instead of the <img> invocation, and we can expect Hugo to automatically generate an efficient, responsive set of images for our website.

Final thoughts #

The partial I’ve written works fairly well considering the requirements at the time of writing, but I am pretty sure there are places I can improve it. Therefore, I have decided to maintain this partial as a Hugo module on GitHub: UtkarshVerma/hugo-modules/responsive-images

Also, this website uses the said partial as a Hugo module, so if you’re curious about the end result, just inspect some of the images. If you have any doubts or suggestions, feel free to comment them down below.


Related



Comments

Nothing yet.

Leave a reply