Hugo ideal image

stereobooster - Oct 12 '23 - - Dev Community

I created image components a couple of times before:

But browsers keep improving - now almost all modern browsers support:

I checked existing solutions - they either don’t do what I want or are complicated (for my taste):

Features

What I expect from an “ideal” image:

Implementation

There are different ways to implement image components in Hugo:

I think the most pragmatic way would be to implement it as “partial” which accepts an image as a resource. This way it can be reused in render hooks and shortcodes, and the resolution of the image would be the responsibility of the caller.

It can look something like this:

{{ partial "picture.html" (dict "img" $img "alt" $.Text) }}

Enter fullscreen mode Exit fullscreen mode

Partial picture.html would be responsible for: detecting width and height, resizing the image, and rendering HTML. There are no special requirements for HTML or CSS so it would be a pretty portable solution (copy one file).

Responsiveness

Component would be responsible for resizing image and rendering srcset attribute. Basic code:

srcset="
{{- (.Resize "330x").RelPermalink }} 330w,
{{- (.Resize "660x").RelPermalink }} 660w,
{{- (.Resize "1024x").RelPermalink }} 1024w,
{{- (.Resize "1320x").RelPermalink }} 2x"
src="{{ (.Resize "660x").RelPermalink }}"

Enter fullscreen mode Exit fullscreen mode

But :

  • it suppose to resize only raster images (not SVG)
  • it should not upscale image

Additionally we can provide webp version of the image. Basic code:

<picture>
<source
srcset="
{{- (.Resize "330x webp").RelPermalink }} 330w,
{{- (.Resize "660x webp").RelPermalink }} 660w,
{{- (.Resize "1024x webp").RelPermalink }} 1024w,
{{- (.Resize "1320x webp").RelPermalink }} 2x"
src="{{ (.Resize "660x webp").RelPermalink }}"
/>
<img ... />
</picture>

Enter fullscreen mode Exit fullscreen mode

Component only responsible for HTML part. You probably would need to add some CSS, for example:

img {
 max-width: 100%;
 height: auto;
}

Enter fullscreen mode Exit fullscreen mode

Lazy-loading

In order to use lazy-load it is enough to add:

loading="lazy"

Enter fullscreen mode Exit fullscreen mode

and optionaly:

decoding="async"

Enter fullscreen mode Exit fullscreen mode

But :

  • it is important to set width and height of the image, to prevent reflow - browser would know ratio of the image upfront and will reserve space for it without downloading it

Which is pretty easy to do with Hugo

width="{{ .Width }}" height="{{ .Height }}"

Enter fullscreen mode Exit fullscreen mode

But :

  • it works only for raster images

For SVG we need to do something like this:

{{ if (eq .MediaType.SubType "svg") }}
{{ $width := ""}}
{{ $height := ""}}
{{ $svgContent := .Content }}
{{ range (findRESubmatch `<svg[^>]*width=["']([.0-9]*)["'a-zA-Z]` $svgContent 1) }}
{{ $width = index . 1 }}
{{ end }}
{{ range (findRESubmatch `<svg[^>]*height=["']([.0-9]*)["'a-zA-Z]` $svgContent 1) }}
{{ $height = index . 1 }}
{{ end }}
{{ if (eq "" $width $height) }}
{{ range (findRESubmatch `<svg[^>]*viewBox=["']?([.0-9]*) ([.0-9]*) ([.0-9]*) ([.0-9]*)` $svgContent 1) }}
{{ $width = index . 3 }}
{{ $height = index . 4 }}
{{ end }}
{{ end }}
{{ if (eq "" $width $height) }}
{{ warnf "Can't detect width and height for SVG %s" .RelPermalink }}
{{/* do not use lazy without dimensions */}}
{{ $lazy = false }}
{{ end }}
{{ end }}

Enter fullscreen mode Exit fullscreen mode

This code reads SVG as text and uses regular expression in order to match width, height, and viewBox. Important : it should not match values with %, because they are useless in this case. On the other hand - any absolute units can be matched, like px.

LQIP

There are many LQIP approaches. I like, so called, modern LQIP, which uses 20px, 20% quality webp image as base64.

<picture
{{ if $lqip }}
{{ $bg := (.Resize "20x webp q20").Content | base64Encode }}
style="background-image:url(data:image/webp;base64,{{ $bg }});background-size:cover"
{{ end }}>

Enter fullscreen mode Exit fullscreen mode

I already introduced picture in the markup, so I will reuse it as “holder” for LQIP.

But :

  • for transparent images (png, webp) LQIP may be problematic, so there suppose to be a way to disable it
  • I don’t want to generate LQIP for SVG, because I would need to rasterize SVG first, which is too much trouble

Additionaly you may want to add CSS in order to make picture of the same size as image, for example:

picture {
 display: block;
}

Enter fullscreen mode Exit fullscreen mode

To improve appearance it is recomended to add blur:

picture > img {
 -webkit-backdrop-filter: blur(10px);
 backdrop-filter: blur(10px);
}

Enter fullscreen mode Exit fullscreen mode

Image is on “top” of picture. While it is loading it will be transparent. So we can use it add blur to the background. Important : this trick doesn’t work in Firefox, see Bug 1556156.

Portable markdown links

This one is easy. Partial take resource rather than path. So it is responsibility of caller to resolve path.

Dark mode

I’m not sure yet how to implement “dark mode” for images. From an HTML point of view it is trivial to implement:

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="..." />
 <source media="(prefers-color-scheme: light)" srcset="..." />
 <img src="..." />
</picture>

Enter fullscreen mode Exit fullscreen mode

But how to do it from Markdown’s point of view? Some options are:

Other

I want to keep number of params minimal. Currently it supports only:

  • img - image as resource
  • alt - string, alternative text
  • class - string, for styling
  • lazy - boolean, whether it should use lazy loading or not
  • webp - boolean, whether it should generate webp version or not
  • lqip - boolean, whether it should generate LQIP or not

Source code

My current implementation is here:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .