Mastodon

Creating responsive images in Hugo

Jul 11, 2021 by Kolappan N

If this is the first time you are hearing about responsive image I recommend you to check out this MDN article first. It is a great read. My website already uses Bootstrap so I will be taking advantages of Bootstrap’s classes in addition to the Hugo’s image processing. Another point to note is that all the images in my website are already in webp, if you have a different format modify the code accordingly.

Step 1: Creating a Shortcode

Hugo already has a built-in shortcode for images. But it doesn’t support multiple images of various sizes so we need to create our own.

<figure>
    <picture>
        {{ $ImgOg := resources.Get (.Get "webp") }}
        <source srcset="{{ $ImgOg.Permalink }}" type="image/webp">

        {{ $param := (print $ImgOg.Width "x jpg") }}
        {{ $resourceJpg := $ImgOg.Resize $param }}
        <img src="{{ $resourceJpg.Permalink }}" height="{{ $ImgOg.Height }}" width="{{ $ImgOg.Width }}"
            alt="{{ with .Get "alt" }} {{ . }}{{ else }}{{ .Get "caption" | markdownify | plainify }} {{ end }}"
            class="img-fluid rounded" loading="lazy" decoding="async" />
    </picture>
    <figcaption class="figure-caption ms-1">{{ .Get "caption" }}</figcaption>
</figure>

Here is how the shortcode is called from markdown,

{{ <nk-images webp="/path/image.webp" caption="Droid4X Home screen"> }}

It is a pretty simple shortcode which uses picture tag to have a webp and a jpg images. If there are caption and alt text it also adds them. I only have webp images and the jpg is automatically during the build time. This is done as some versions of Safari don’t support webp yet.

You can also see the img-fluid bootstrap class which also makes the images responsive. If you use some other framework or use your own CSS I suggest adding the corresponding CSS classes.

Step 2: Creating images of multiple sizes

In the above shortcode, while the images are responsive the same high quality is also served for mobile and desktop. I am using Hugo pipeline to generate various sizes of images and serve them depending on the screen width. Here is the full shortcode used in this website,

<!--
    Input Param:
        - webp
        - caption
        - alt (optional): caption will be used if not passed
-->

<figure>
    <picture>
        {{ $ImgOg := resources.Get (.Get "webp") }}

        {{ if gt $ImgOg.Width 1920 }}
        <source srcset="{{ $ImgOg.Permalink }}" type="image/webp" media="(min-width: 1920px)">
        {{ else if gt $ImgOg.Width 1600 }}
        <source srcset="{{ $ImgOg.Permalink }}" type="image/webp" media="(min-width: 1600px)">
        {{ else if gt $ImgOg.Width 1280 }}
        <source srcset="{{ $ImgOg.Permalink }}" type="image/webp" media="(min-width: 1280px)">
        {{ else if gt $ImgOg.Width 1024 }}
        <source srcset="{{ $ImgOg.Permalink }}" type="image/webp" media="(min-width: 1024px)">
        {{ else if gt $ImgOg.Width 720 }}
        <source srcset="{{ $ImgOg.Permalink }}" type="image/webp" media="(min-width: 720px)">
        {{ else if gt $ImgOg.Width 480 }}
        <source srcset="{{ $ImgOg.Permalink }}" type="image/webp" media="(min-width: 480px)">
        {{ else }}
        <source srcset="{{ $ImgOg.Permalink }}" type="image/webp">
        {{ end }}

        {{ if gt $ImgOg.Width 1920 }}
        {{ $ImgOg1920 := $ImgOg.Resize "1920x" }}
        <source srcset="{{ $ImgOg1920.Permalink }}" type="image/webp" media="(min-width: 1600px)">
        {{ end }}

        {{ if gt $ImgOg.Width 1600 }}
        {{ $ImgOg1600 := $ImgOg.Resize "1600x" }}
        <source srcset="{{ $ImgOg1600.Permalink }}" type="image/webp" media="(min-width: 1280px)">
        {{ end }}

        {{ if gt $ImgOg.Width 1280 }}
        {{ $ImgOg1280 := $ImgOg.Resize "1280x" }}
        <source srcset="{{ $ImgOg1280.Permalink }}" type="image/webp" media="(min-width: 1024px)">
        {{ end }}

        {{ if gt $ImgOg.Width 1024 }}
        {{ $ImgOg1024 := $ImgOg.Resize "1024x" }}
        <source srcset="{{ $ImgOg1024.Permalink }}" type="image/webp" media="(min-width: 720px)">
        {{ end }}

        {{ if gt $ImgOg.Width 720 }}
        {{ $ImgOg720 := $ImgOg.Resize "720x" }}
        <source srcset="{{ $ImgOg720.Permalink }}" type="image/webp" media="(min-width: 480px)">
        {{ end }}

        {{ if gt $ImgOg.Width 480 }}
        {{ $ImgOg480 := $ImgOg.Resize "480x" }}
        <source srcset="{{ $ImgOg480.Permalink }}" type="image/webp" media="(max-width: 480px)">
        {{ end }}

        {{ $param := (print $ImgOg.Width "x jpg") }}
        {{ $resourceJpg := $ImgOg.Resize $param }}
        <img src="{{ $resourceJpg.Permalink }}" height="{{ $ImgOg.Height }}" width="{{ $ImgOg.Width }}"
            alt="{{ with .Get " alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify| plainify }}{{ end }}"
            class="img-fluid rounded" loading="lazy" decoding="async" />
    </picture>
    <figcaption class="figure-caption ms-1">{{ .Get "caption" }}</figcaption>
</figure>

As you can see in the above shortcode, we only downscale the images and never upscale it. This is essential since, if you tell Hugo to resize an image to a extremely high resolution it will do it and it will not look good.

Also once you implement this, I recommend checking the images once as the automatic conversion and resizing can cause artifacts in already compressed images. Also set the quality of the image conversions to 100 in your websites config.yaml

imaging:
  quality: 100