Tim's blah blah blah

Responsive images & image grids for Hugo


I like bearblog for its simplicity and speed but its image support is not great. I wanted to add images with three requirements:

  1. Responsive images: the server only servers the resolution required by the client (i.e. high resolution screens get higher resolution images)
  2. Responsive image grids: depending on screen resolution, images are shown side-by-side or on top of each other.
  3. Modern serving: lazyloading, modern formats (WebP) with fallback, prevent content layout shift, highscore in WebPageTest and Google PageSpeed Insights

Responsive images

Read this excellent article on making responsive images using srcset and sizes. Also see Mozilla’s Responsive images guide on art direction / resolution switching / file format support and the difference between those. It’s not difficult but it’s complex. In my layout, which is a single-column content with max width of 720px, I have three scenarios for pictures:

  1. Picture is 100vw wide, with max of 720px (e.g. if I have a picture as wide as my content column)
  2. Picture is 50vw wide, with max of 360px (e.g. if I use floatleft or floatright with half-width)
  3. Picture is 33vw wide, with max of ~240px (e.g. if I use a 3-column responsive grid or use third-width)

This means I need the following sizes scenarios:

  1. For single-column pictures: sizes="(min-width: 720px) 720px, 100vw"
    • if the viewport is 720px or wider, give a 720px picture.
      • We need a 720 pixel picture (for 1x pixel ratio) and 1440 pixel picture (for 2x pixel ratio)
    • Else, use 100% of viewport (which will be <720px).
      • Here we need smaller pictures (we get for free from below scenarios)
  2. For two-column pictures: sizes="(min-width: 720px) 360px, 50vw"
    • Same as above, except everything half.
      • We need 360 pixel and 720 pixel pictures.
  3. For three-column picture: sizes="(min-width: 720px) 240px, 33vw"
    • Same as 1, except everything as third.
      • We need 240 pixel and 480 pixel pictures.

Combining this, we need source pictures in 360 pixels, 720 pixels, and 1440 pixels to cover almost all use cases. We only need more than 1440 pixels if we have >2x pixel ratio, which is rare and I’m OK not to support. On top of that, we need three flavors of sizes parameters depending on how we position the image (in single, double, or triple-column layout).

I spliced this and this and this to get a fig shortcode I wanted:

Example use

{{< fig src="images/diy-jaga-dbe-dbh/IMG_4664.jpg" caption="Marking holes" alt="Marking out ventilator holes on aluminium frame" >}}
{{< fig src="images/tim1837.jpg" link="images/tim1837.jpg" alt="Tim van Werkhoven by Cima" class="floatright half-width" sizes="(min-width: 720px) 360px, 50vw">}}


Get file that matches the filename as specified as src="" in shortcode.
Forgot why we use a wildcard here, maybe for case sensitivity. Doc:
{{ $src := resources.GetMatch (printf "*%s*" (.Get "src")) }}
<!-- Alternative non-wildcard version: {{ $src := resources.GetMatch (.Get "src") }} -->

Set responsive image sizes, these are hardcoded throughout this
- 'x' dictates that images are resized to this width
- q50 is for 50% quality. 
- Box filtering because we're downsampling anyway (anyone can do that),
  and other filters (like Lanczos - https://github.com/disintegration/imaging)
  increase PNG filesize enormously. Doc:
- we also generate the same set in webp format for advanced browsers

TODO: check if source image is not already in WebP format (rare case)

See also: https://gohugo.io/content-management/image-processing/
{{ $tinyw := default "360x q50 Box" }}
{{ $smallw := default "720x q50 Box" }}
{{ $largew := default "1440x q50 Box" }}

{{ $tinywwebp := default "360x webp q50 Box" }}
{{ $smallwwebp := default "720x webp q50 Box" }}
{{ $largewwebp := default "1440x webp q50 Box" }}

Resize the source image to the given responsive sizes. Be sure to set 
variable scope correctly. 

{{ $.Scratch.Set "tiny" false }}{{ if gt $src.Width "360" }}{{ $.Scratch.Set "tiny" ($src.Resize $tinyw) }}{{ end }}
{{ $.Scratch.Set "small" false }}{{ if gt $src.Width "720" }}{{ $.Scratch.Set "small" ($src.Resize $smallw) }}{{ end }}
{{ $.Scratch.Set "large" false }}{{ if gt $src.Width "1440" }}{{ $.Scratch.Set "large" ($src.Resize $largew) }}{{ end }}

{{ $.Scratch.Set "tinywebp" false }}{{ if ge $src.Width "360" }}{{ $.Scratch.Set "tinywebp" ($src.Resize $tinywwebp) }}{{ end }}
{{ $.Scratch.Set "smallwebp" false }}{{ if ge $src.Width "720" }}{{ $.Scratch.Set "smallwebp" ($src.Resize $smallwwebp) }}{{ end }}
{{ $.Scratch.Set "largewebp" false }}{{ if ge $src.Width "1440" }}{{ $.Scratch.Set "largewebp" ($src.Resize $largewwebp) }}{{ end }}

Figure block begins, based on Hugo's default and Mozilla's example

  <source type="image/jpeg" srcset="pyramid.svg" sizes="">
  <source type="image/webp" srcset="pyramid.webp" sizes="">
  <img src="pyramid.png" alt="regular pyramid built from four equilateral triangles" width="640" height="480" >
<figure{{ with .Get "class" }} class="{{ . }}"{{ end }}>
    {{- if .Get "link" -}}
        <a href="{{ .Get "link" }}"{{ with .Get "target" }} target="{{ . }}"{{ end }}{{ with .Get "rel" }} rel="{{ . }}"{{ end }}>
    {{ else }}
        <a href="{{ with $src }}{{.RelPermalink}}{{ end }}">
    {{- end }}
    Show responsive images here, first in one source tag (for webp), second 
    in img as fallback / jpeg. Note we can't include a second source tag
    with 'image/jpeg' as mime-type because we're not sure if the original is
    jpeg (e.g. could be png as well).
        <source type="image/webp"
          sizes={{ with .Get "sizes" }}'{{.}}'{{ else }}"(min-width: 720px) 720px, 100vw"{{ end }}
    {{ if ($.Scratch.Get "tinywebp") }}      {{($.Scratch.Get "tinywebp").RelPermalink}} 360w,
    {{ end }}{{ if ($.Scratch.Get "smallwebp") }}      {{($.Scratch.Get "smallwebp").RelPermalink}} 720w,
    {{ end }}{{ if ($.Scratch.Get "largewebp") }}      {{($.Scratch.Get "largewebp").RelPermalink}} 1440w{{ end }}'
          sizes={{ with .Get "sizes" }}'{{.}}'{{ else }}"(min-width: 720px) 720px, 100vw"{{ end }}
    {{ if ($.Scratch.Get "tiny") }}      {{($.Scratch.Get "tiny").RelPermalink}} 360w,
    {{ end }}{{ if ($.Scratch.Get "small") }}      {{($.Scratch.Get "small").RelPermalink}} 720w,
    {{ end }}{{ if ($.Scratch.Get "large") }}      {{($.Scratch.Get "large").RelPermalink}} 1440w,
    {{ end }}{{ with $src }}      {{.RelPermalink}} {{.Width}}w{{ end }}'
          {{ if ($.Scratch.Get "small") }}src="{{ ($.Scratch.Get "small").RelPermalink }}" width="{{ ($.Scratch.Get "small").Width }}" height="{{ ($.Scratch.Get "small").Height }}"
          {{ else }}src="{{ $src.RelPermalink }}" width="{{ $src.Width }}" height="{{ $src.Height }}"{{ end }}
          {{- if or (.Get "alt") (.Get "caption") }}
          alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify| plainify }}{{ end }}"
          {{- end -}}
        /> <!-- Closing responsive img tag -->
    {{- if .Get "link" }}</a>{{ else }}</a>{{ end -}}
    {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}}
            {{ with (.Get "title") -}}
                <h4>{{ . }}</h4>
            {{- end -}}
            {{- if or (.Get "caption") (.Get "attr") -}}<p>
                {{- .Get "caption" | markdownify -}}
                {{- with .Get "attrlink" }}
                    <a href="{{ . }}">
                {{- end -}}
                {{- .Get "attr" | markdownify -}}
                {{- if .Get "attrlink" }}</a>{{ end }}</p>
            {{- end }}
    {{- end }}

Responsive image grids

I adopted the w3schools responsive grid, and added the below css. Not used, but looked promising: w3schools csss image side by side, hugo shortcode gallery and hugo easy gallery

I tweaked a few things:

Note that if you’re using above fig shortcode you need to manually set the sizes parameter, as follows:

This gives: sizes="(min-width: 720px) 240px, (min-width: 480px) 360px, 100vw"

Example use

{{< rawhtml >}}
<div class="row">
  <div class="col3">
    {{< fig src="images/diy-jaga-dbe-dbh/IMG_4664.jpg" sizes="(min-width: 720px) 240px, (min-width: 480px) 360px, 100vw" caption="Marking holes" alt="Marking out ventilator holes on aluminium frame" >}} 
  <div class="col3">
    {{< fig src="images/diy-jaga-dbe-dbh/IMG_4670.jpg" sizes="(min-width: 720px) 240px, (min-width: 480px) 360px, 100vw" caption="Drilling out holes & sawing out fan blade area" alt="Drilling out ventilator holes & sawing out fan blade area for improved airflow" >}}
  <div class="col3">
    {{< fig src="images/diy-jaga-dbe-dbh/IMG_4673.jpg" sizes="(min-width: 720px) 240px, (min-width: 480px) 360px, 100vw" caption="End result of two aluminium frames" alt="End result of two alumiunum frame with sawed out fan blade area" >}}
{{< /rawhtml >}}


figure {
  margin: 0px;
  padding: 0 2px;
figure p {
  margin: 0px;
  font-style: italic;
  font-size: small;
  text-align: center;
.row {
  display: flex;
  flex-wrap: wrap;
/* Default:  Create three equal columns that sits next to each other */
.col3 {
  -ms-flex: 33%;
  flex: 33%;
  max-width: 33%;
.col3 img {
  margin-top: 8px;
  vertical-align: middle;
  width: 100%;

/* Responsive layout - makes a two column-layout instead of three columns to ensure the picture is never *narrower* than 240 pixels, 720 = 3*240 */
@media screen and (max-width: 720px) {
  .col3 {
    -ms-flex: 50%;
    flex: 50%;
    max-width: 50%;
/* Responsive layout - makes the two columns stack on top of each other instead of next to each other 480 = 2*240 */
@media screen and (max-width: 480px) {
  .col3 {
    -ms-flex: 100%;
    flex: 100%;
    max-width: 100%;

#html #hugo #css