Skip to main content

Hugo ideal image

·

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) }}

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 }}"

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>

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

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

Lazy-loading #

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

loading="lazy"

and optionaly:

decoding="async"

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 }}"

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 }}

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 }}>

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;
}

To improve appearance it is recomended to add blur:

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

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.

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>

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:

Read more: Useful modern tools for static websites, What I miss in Markdown (and Hugo)