A new way to generate LQIPs in Hugo 0.104.0

An interesting addition to this SSG’s image-processing arsenal helps you accommodate users viewing your site under suboptimal conditions.


Update from the future: I later made considerable changes to the methods described in this post. So that you’ll have a fuller perspective, I suggest you read everything except the code here, and only then go to my post about why I made the changes, where superior code awaits.

With today’s release of version 0.104.0 of the Hugo static site generator (SSG), there’s a new and perhaps better way to generate low-quality image placeholders (LQIPs).

As you probably already know, using an LQIP as an image’s background allows those with slower connectivity and devices to get at least a representation of a full image while that image is still downloading.

My go-to approach for LQIP-building has been, for each image, to Base64-encode a tiny version of that image and then load it as the background. The tiny reproduction would obviously be extremely blurred as it was stretched out to the real image’s width and height, producing the familiar “blur-up” effect when the real image appeared (fading in via CSS).

Then, today, the release of Hugo 0.104.0 added a .Colors method to Hugo’s existing image-processing capabilities. As the documentation says (and remember that a slice is what Hugo, like the Go language on which it is based, calls what most other SSGs and languages call an array):

.Colors returns a slice of hex string with the dominant colors in the image[,] using a simple histogram method.

(Link added.)

For example, given this1 image:

Photo of a cat named Shakespeare sitting on a window sill

. . . here’s the slice you get from .Colors:


Those are the colors, in hex format, that .Colors picked out as the main ones in the image. The number of colors obviously varies from image to image, and it can be as low as one. Note that a one-item slice will be problematic if you try to code so that an image’s .Colors-derived LQIP has a background that’s a linear gradient, because the linear-gradient function requires at least two colors. Thus, you’ll want to add a test for a one-color return from .Colors so you can append to the slice an extra color — perhaps a safe one like black — if need be.

So, with Hugo 0.104.0 and up, you can try something like the following, in which $src represents the real image file:

{{- $LQIP_colors := $src.Colors -}}
{{- if (lt ($LQIP_colors | len) 2) -}}
	{{- $LQIP_colors = $LQIP_colors | append "#000000" -}}
{{- end -}}
{{- $LQIP_bkgd := delimit ($LQIP_colors) ", " -}}
{{- $style := print "background: linear-gradient(" $LQIP_bkgd "); background-size: cover; background-repeat: no-repeat;" -}}

After specifying that $LQIP_colors is the return from $src.Colors, we have the conditional for handling a one-color return: “if the length of $LQIP_colors is less than 2, append to $LQIP_colors the color black (#000000).” Right after that, we:

  • Create $LQIP_bkgd, which is a string containing the contents of $LQIP_colors, with each pair separated by a delimit-ing comma and space. That’s for . . .
  • . . . $style, the CSS we’ll provide to the eventual div in which the real image will be contained.2

Finally, be sure to check the $.Colors example page, and its source code, mentioned in the 0.104.0 release notes.

Later note: I subsequently found a 2018 article which suggested the result of this approach should be considered as not an LQIP but, rather, a GIP (gradient image placeholder). Fair enough — and please, folks, let’s avoid any .gif-style arguments about how to pronounce the G of GIP.

  1. If the site is currently on Hugo, the image is rendered using a shortcode that makes use of .Colors for demonstrative purposes. ↩︎

  2. If you have a strict Content Security Policy and need to work around the issue of inline styling, you might want to examine the admittedly more complicated original from which I extracted this example. ↩︎