Responsive images & image grids for Hugo
I like bearblog (github.com) for its simplicity and speed but its image support is not great. I wanted to add images with three requirements:
- Responsive images: the server only servers the resolution required by the client (i.e. high resolution screens get higher resolution images)
- Responsive image grids: depending on screen resolution, images are shown side-by-side or on top of each other.
- Modern serving: lazyloading, modern formats (WebP) with fallback, prevent content layout shift, highscore in WebPageTest (webpagetest.org) and Google PageSpeed Insights (google.com)
Contents
Responsive images ¶
Read this excellent article (ericportis.com)
on making responsive images using srcset
and sizes
. Also see Mozilla’s Responsive images guide (mozilla.org) 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:
- Picture is
100vw
wide, with max of 720px (e.g. if I have a picture as wide as my content column) - Picture is
50vw
wide, with max of 360px (e.g. if I usefloatleft
orfloatright
withhalf-width
) - Picture is
33vw
wide, with max of ~240px (e.g. if I use a 3-column responsive grid or usethird-width
)
This means I need the following sizes
scenarios:
- 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)
- if the viewport is 720px or wider, give a 720px picture.
- For two-column pictures:
sizes="(min-width: 720px) 360px, 50vw"
- Same as above, except everything half.
- We need 360 pixel and 720 pixel pictures.
- Same as above, except everything half.
- 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.
- Same as 1, except everything as third.
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 (dev.to) and
this (alexlakatos.com) and
this (github.com) to get a fig
shortcode I wanted:
- Prepares responsive images in max 4 resolutions
- Prepares image in original format (e.g. jpg/png) and WebP (N.B. you need Hugo 0.83 extended version (gohugo.io) for this)
- Never upscale images
- Use
box
filter because we’re downsampling only (anyone can do that), and other filters (github.com) (like Lanczos) increase PNG filesize enormously. - Serve 720px wide image to non-responsive browsers (we assume these are older browsers which don’t need high res images)
- Use
caption
asalt
if it’s the only attribute defined - Specify precise source image size (width/height tags) to prevent content layout shift
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">}}
Source ¶
{{/*
Get file that matches the filename as specified as src="" in shortcode.
Forgot why we use a wildcard here, maybe for case sensitivity. Doc:
https://gohugo.io/content-management/page-resources/
*/}}
{{ $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
shortcode,
- '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.
Doc:
https://code.luasoftware.com/tutorials/hugo/hugo-scope-variable-in-template/
*/}}
{{ $.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
<picture>
<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" >
</picture>
*/}}
<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).
*/}}
<picture>
<source type="image/webp"
sizes={{ with .Get "sizes" }}'{{.}}'{{ else }}"(min-width: 720px) 720px, 100vw"{{ end }}
srcset='
{{ 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 }}'
/>
<img
loading="lazy"
sizes={{ with .Get "sizes" }}'{{.}}'{{ else }}"(min-width: 720px) 720px, 100vw"{{ end }}
srcset='
{{ 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 -->
</picture>
{{- if .Get "link" }}</a>{{ else }}</a>{{ end -}}
{{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}}
<figcaption>
{{ 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 }}
</figcaption>
{{- end }}
</figure>
Responsive image grids ¶
I adopted the w3schools responsive grid (w3schools.com), and added the below css. Not used, but looked promising: w3schools csss image side by side (w3schools.com), hugo shortcode gallery (github.com) and hugo easy gallery (github.com)
I tweaked a few things:
- Default 3 columns of images (instead of 4)
- Smaller figure caption with less margin
Note that if you’re using above fig
shortcode you need to manually set the sizes
parameter, as follows:
- at >720px, there are three columns, i.e. 240px images –> the 360px image will be served
- at >480px, there are two columns, i.e. 240px-360px images –> the 360px image will be served
- at <=480px, there is one column, i.e. <480px images –> the 720px image will be served, not ideal but bearable.
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>
<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>
<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" >}}
</div>
</div>
{{< /rawhtml >}}
Source ¶
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%;
}
}