Responsive images for Hugo
Tags: hugo, webdev, performance, go
Published: 2019-06-16 13:49:51

---
title: Responsive images for Hugo
published: true
description: Responsive, lazy-loading, LQIP, noscript image shortcode for Hugo
tags: [hugo, webdev, performance, go]
cover_image: https://thepracticaldev.s3.amazonaws.com/i/kns65ab4hk3wpr769f6m.jpg
series: Hugo theme
---

**UPD**: updated version is [here](https://stereobooster.com/posts/hugo-ideal-image/)

Demo is [here](https://stereobooster.netlify.com/posts/typography/). Source code is [here](https://github.com/stereobooster/hugo-pipes-parcel/tree/basic-theme).

## Resize depending on the viewport

We don't need much to build a basic responsive image.

```html
<img src="..." alt="..." >
```

```css
img {
  width: 100%;
  height: auto;
}
```

Images will take all given space and resize proportionally to the viewport,

## Dimensions

Without knowing dimensions (ratio) of the image browser will draw it with height 0 initially and then as soon as image loads it will redraw image with appropriate height, the page will unpleasantly "jump". To avoid this we need to provide width and height

```html
<img src="..." alt="..." width="{{ $img.Width }}" height="{{ $img.Height }}">
```

to do this in Hugo we need to use [`imageConfig`](https://kodify.net/hugo/functions/imageconfig-function/):

```
{{ $img := imageConfig (path to file) }}
```

## `srcset`

The current implementation is responsive in the sense that the size of the image changes with the size of the screen, but resizing is done by the browser. Instead, we can provide several sizes of the image so that clients with smaller screens (mobile devices most likely) can spend less traffic by downloading smaller images.

To do this we can use [`srcset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-srcset) img attribute and Hugo built-in functionality to resize images. For example like in [this article](https://laurakalbag.com/processing-responsive-images-with-hugo/):

```
{{ $src := .Page.Resources.GetMatch (printf "*%s*" (image path)) }}

{{ $tiny := $src.Resize $tinyw }}
{{ $small := $src.Resize $smallw }}
{{ $medium := $src.Resize $mediumw }}
{{ $large := $src.Resize $largew }}

<img
  srcset='
  {{ if ge $src.Width "500" }}
    {{ with $tiny.RelPermalink }}{{.}} 500w{{ end }}
  {{ end }}
  {{ if ge $src.Width "800" }}
    {{ with $small.RelPermalink }}, {{.}} 800w{{ end }}
  {{ end }}
  {{ if ge $src.Width "1200" }}
    {{ with $medium.RelPermalink }}, {{.}} 1200w{{ end }}
  {{ end }}
  {{ if ge $src.Width "1500" }}
    {{ with $large.RelPermalink }}, {{.}} 1500w {{ end }}
  {{ end }}'
  {{ if .Get $medium }}
    src="{{ $medium.RelPermalink }}"
  {{ else }}
    src="{{ $src.RelPermalink }}"
  {{ end }}
  ...
```

## lazy loading

We can save even more bandwidth by postponing images download unless they are in the viewport. We can use [`lazysizes`](https://github.com/aFarkas/lazysizes) to accomplish this.

```js
import "lazysizes";
```
 
change `src` to `data-src`, `srcset` to `data-srcset`, add `class="lazyload"`

```
<img
  class="lazyload"
  data-sizes="auto"
  data-srcset=...
```

This technique is called [lazy-loading](https://addyosmani.com/blog/lazy-loading/).

## LQIP

if we use lazy-loading and users network is slow or down user will see blank rectangles instead of images, which can be perceived as a broken site. Instead, we can provide low-quality image placeholders - blurry previews of actual content. To do this we can inline [base64 encoded small version of original image](https://mwellner.de/en/2018/10/30/image-handling-with-hugo/):

```
{{ $lqip := $src.Resize $lqipw }}

<div class="img" style="background: url(data:image/jpeg;base64,{{ $lqip.Content | base64Encode  }}); background-size: cover">
  <svg width="{{ $img.Width }}" height="{{ $img.Height }}" aria-hidden="true"></svg>
  <img
    class="lazyload"
    ...
```

and a bit of CSS

```css
.img svg,
.img img {
  margin: 0;
  width: 100%;
  height: auto;
}

.img {
  position: relative
}

.img img {
  position: absolute;
  top:0;
  left:0;
}
```

## noscript

Another downside of lazy-loading is that images don't work without JS. Let's fix this by providing default img in `<noscript>`

```
<noscript>
  <img
    loading="lazy"
    ...
```

and 

```css
.nojs .img .lazyload {
  display: none;
}
```

## final version

Let's create a shortcode

```
{{/* get file that matches the filename as specified as src="" in shortcode */}}
{{ $src := .Page.Resources.GetMatch (printf "*%s*" (.Get "src")) }}

{{/* set image sizes, these are hardcoded for now, x dictates that images are resized to this width */}}

{{ $lqipw := default "20x" }}
{{ $tinyw := default "500x" }}
{{ $smallw := default "800x" }}
{{ $mediumw := default "1200x" }}
{{ $largew := default "1500x" }}

{{/* resize the src image to the given sizes */}}

{{ $lqip := $src.Resize $lqipw }}
{{ $tiny := $src.Resize $tinyw }}
{{ $small := $src.Resize $smallw }}
{{ $medium := $src.Resize $mediumw }}
{{ $large := $src.Resize $largew }}

{{/* only use images smaller than or equal to the src (original) image size, as Hugo will upscale small images */}}
{{/* set the sizes attribute to (min-width: 35em) 1200px, 100vw unless overridden in shortcode */}}

{{ $img := imageConfig ($src.RelPermalink | printf "content/%s" ) }}

<div class="img" style="background: url(data:image/jpeg;base64,{{ $lqip.Content | base64Encode  }}); background-size: cover">
  <svg width="{{ $img.Width }}" height="{{ $img.Height }}" aria-hidden="true"></svg>
  <img
    class="lazyload"
    data-sizes="auto"
    data-srcset='
    {{ if ge $src.Width "500" }}
      {{ with $tiny.RelPermalink }}{{.}} 500w{{ end }}
    {{ end }}
    {{ if ge $src.Width "800" }}
      {{ with $small.RelPermalink }}, {{.}} 800w{{ end }}
    {{ end }}
    {{ if ge $src.Width "1200" }}
      {{ with $medium.RelPermalink }}, {{.}} 1200w{{ end }}
    {{ end }}
    {{ if ge $src.Width "1500" }}
      {{ with $large.RelPermalink }}, {{.}} 1500w {{ end }}
    {{ end }}'
    {{ if .Get $medium }}
      data-src="{{ $medium.RelPermalink }}"
    {{ else }}
      data-src="{{ $src.RelPermalink }}"
    {{ end }}
    width="{{ $img.Width }}" height="{{ $img.Height }}"
    {{ with .Get "alt" }}alt='{{.}}'{{ end }}>
  <noscript>
    <img
      loading="lazy"
      {{ with .Get "sizes" }}sizes='{{.}}'{{ else }}{{ end }}
      srcset='
      {{ if ge $src.Width "500" }}
        {{ with $tiny.RelPermalink }}{{.}} 500w{{ end }}
      {{ end }}
      {{ if ge $src.Width "800" }}
        {{ with $small.RelPermalink }}, {{.}} 800w{{ end }}
      {{ end }}
      {{ if ge $src.Width "1200" }}
        {{ with $medium.RelPermalink }}, {{.}} 1200w{{ end }}
      {{ end }}
      {{ if ge $src.Width "1500" }}
        {{ with $large.RelPermalink }}, {{.}} 1500w {{ end }}
      {{ end }}'
      {{ if .Get $medium }}
        src="{{ $medium.RelPermalink }}"
      {{ else }}
        src="{{ $src.RelPermalink }}"
      {{ end }}
      width="{{ $img.Width }}" height="{{ $img.Height }}"
      {{ with .Get "alt" }}alt='{{.}}'{{ end }}>
  </noscript>
</div>
```

Usage, instead of:

```md
![test](./image.jpg)
```

we need to write

```md
{{< img src="image.jpg" alt="test" >}}
```

## PS

before:

![](https://thepracticaldev.s3.amazonaws.com/i/yebs9vhpimw86he88bzu.png)

after:

![](https://thepracticaldev.s3.amazonaws.com/i/om6rvgokhp2z6937ba3l.png)


The sad part is that there is no markdown preprocessor which would allow me to use the traditional syntax for images instead of shortcode 😞.

[I can't use shortcodes inside theme files](https://discourse.gohugo.io/t/highlight-html-unexpected-in-command-built-site-for-language-en/4468), as the result I copy-pasted code from shortcode to theme files 😞.