hugo

Image optimization in Hugo

Go through the easy process of optimizing images in Hugo to create <img> elements that represent good practice. It will cover recommended attributes, image compression, the automatic generation of alternative formats, and cache busting using fingerprint.

Reading time of 1442 words
7 minutes
Reading time of 1442 words ~ 7 minutes


Did you find this article helpful?
Please consider tipping me a coffee as a thank you.
Ko-fi Buy Me a Coffee
Did you find this article helpful? Please consider tipping me a coffee or three as a thank you.
Tip using Ko-fi or Buy Me a Coffee

I will go through the easy process of optimizing images in Hugo to create <img> elements that represent good practice. It will cover recommended attributes, image compression, the automatic generation of alternative formats, and cache busting using fingerprint.

The basics

Suppose I want to insert a new image header. In this case, the most straightforward approach is to copy an image to the static directory and directly reference it in a partial or default template.

For example, a header partial template at: themes/mytheme/layouts/partials/header.html.

While the banner.jpg image would be located at: static/banner.jpg.

# an example command to copy banner.jpg to the static directory
cp banner.jpg /mysite/static
1<header>
2  <img src="/banner.jpg">
3</header>

Including the leading / forward slash is essential to tell the browser that the image is accessible from the server’s root.

While this is the most straightforward approach does not allow Hugo to interact with the image. So I personally never recommend using the static directories for any custom content. Instead, use the theme assets directory to hold any site images unrelated to individual pages and posts.

# an example command to move banner.jpg into the assets directory
cd mysite
mv static/banner.jpg themes/mytheme/assets/images/banner.jpg

We can use the Hugo resources.Get function to retrieve the image and store it as a variable.

1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="/banner.jpg">

Update the src attribute to use the $banner variable as a relative path to the image.

1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $$banner.RelPermalink }}">

It will generate the following HTML.

<img src="/images/banner.jpg">

Or use Permalink for an absolute path to the image.

1<img src="{{ $banner.Permalink }}">
<img src="http://localhost:1313/images/banner.jpg">

Alt attributes

It’s essential for accessibility to always include descriptive image texts with the alt attribute.

1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $banner.RelPermalink }}" alt="The skyline of Sydney, Australia">

Width and height dimensions

You should also include width and height dimensions for each image.

Always include width and height size attributes on your images... This approach ensures that the browser can allocate the correct amount of space in the document while the image is loading

# obtaining the dimensions in Linux
$ exiv2 banner.jpg
Image size      : 2989 x 812
1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $banner.RelPermalink }}" width="2989" height="812" alt="The skyline of Sydney, Australia">

Hugo can generate these values for you with the Width and Height properties.

1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $banner.RelPermalink }}" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">

Lazy loading

As of 2022, the <img loading=""> attribute is recent and should be in use on all <img> elements. The loading supports two values, eager and lazy.

eager loads the image immediately and is the default, traditional browser behavior.

lazy defers the loading of the image until the user scrolls to it, improving browser performance and saving on bandwidth.

1{{- $banner := resources.Get "images/banner.jpg" }}
2<img src="{{ $banner.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">

Lazy loading is a strategy to identify resources as non-blocking (non-critical) and load these only when needed. It's a way to shorten the length of the critical rendering path, which translates into reduced page load times


Optimize images

Hugo has some great Image Processing tools for optimizing your images. Unfortunately, it is not always obvious how to do some simple tasks, so I unintuitively use the Resize method to optimize images but keep their dimensions.

Create a new $jpeg variable which will run the Resize method to optimize the banner.jpg.

1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize "2989x812 jpeg" }}

While keeping the original dimensions, I see considerable file size savings with the resized JPEG image.

# printing the file size of the original banner.jpg
$ du -h banner.jpg
1.1M
# printing the file size of the Hugo optimized banner.jpg
$ du -h _huf9c78e74655fc44f9887c1375f5eec72_1142285_5ba59f127937afa35edcf83637b4809b.jpg
336K

Update the <img src=""> attribute to point to the optimized image $jpeg.

1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize "2989x812 jpeg" }}
3<img src="{{ $jpeg.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">

Finally, update the $jpeg variable to use Hugo’s Width and Height values, so we don’t have to worry about ever-changing these in the future.

1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}

printf is a useful but not well explained Go function. It lets you combine different variables values together as a formatted string. %d is a placeholder for a number value (in Go, it’s referenced as base 10 integer).

printf "%dx%d jpeg" 0 0 would return "0x0 jpeg"

printf "%dx%d jpeg" 1000 500 would return "1000x500 jpeg"

printf "%dx%d jpeg" $banner.Width $banner.Height would return "2989x812 jpeg"

1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}
3<img src="{{ $jpeg.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">

Alternative image formats

Another way to optimize page loads is to offer alternative image formats. The JPEG banner is a universally viewable format. However, newer optimized formats exist that we can also quickly provide, thanks to Hugo and HTML5. By serving a WebP (Web Picture format) supplement, there’s a potential 25% download savings.

Create a new $webp variable to generate the new image format, which follows the same syntax as $jpeg.

1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}
3{{- $webp := $banner.Resize (printf "%dx%d webp photo" $banner.Width $banner.Height) }}

There’s an additional option in the $webp variable, photo. This is known as a WebP hint, and it’s only applicable to WebP and corresponds to a set of pre-defined encoding parameters.

# printing the file size of the Hugo optimized banner as a webp image.
# as a JPEG it is 336K.
$ du -h banner_huf9c78e74655fc44f9887c1375f5eec72_1142285_2989x812_resize_q75_h2_box.webp
244K

To use the WebP image in our HTML, we introduce the <picture> and <source> elements.

1{{- $banner := resources.Get "images/banner.jpg" }}
2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}
3{{- $webp := $banner.Resize (printf "%dx%d webp photo" $banner.Width $banner.Height) }}
4<picture>
5    <source srcset="{{ $webp.RelPermalink }}" type="image/webp">
6    <img src="{{ $jpeg.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
7</picture>

The picture element offers alternative versions of an image for different scenarios. It will be up to the browser to determine the best image to use. A <picture> can only ever have one <img> element but can have multiple <source>. The image attributes such as loading, alt, and width/height can only be used with the <img> element.

The <source srcset=""> attribute contains one or more links to images, and the type is the MIME media type.

Note multiple source elements should be ordered in display preference, while the img element should link to the most compatible fallback image.

1<picture>
2    <source srcset="/images/banner_huf9c78e74655fc44f9887c1375f5eec72_1142285_2989x812_resize_q75_h2_box.webp" type="image/webp">
3    <img src="/images/banner_huf9c78e74655fc44f9887c1375f5eec72_1142285_2989x812_resize_q75_box.jpg" loading="lazy" width="2989" height="812"
4      alt="The skyline of Sydney, Australia">
5</picture>
Chrome DevTools Network showing the use of a WebP image
Using the Network DevTools tab, we can see Chrome is choosing the smaller WebP banner image

Caching busting

Caching is critical to a website’s performance in the browser and online using Content Delivery Networks. Caching stores and uses a copy of the file locally instead of always fetching it from your remote web server. It is great until you need to update the file and various caches serve the older, replaced file.

The easiest way to avoid “stale caches” is to give the file a unique filename that gets changed anytime the file is updated. Again, thankfully Hugo makes this easy using its fingerprint pipe.

Please note that the Resize method and other Hugo processed images do not require fingerprinting.

1{{- $banner := resources.Get "images/banner.jpg" | fingerprint }}
2<img src="{{ $banner.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
<img src="/images/banner.3bb0461a9d784c3118b210911c55300f2b15529d1aa0faf6ddb7e84aa00176c0.jpg" loading="lazy" width="2989" height="812" alt="The skyline of Sydney, Australia">

If I ever modify or replace banner.jpg, the partial filename will differ. It is the SHA256 checksum of the file.

banner.3bb0461a9d784c3118b210911c55300f2b15529d1aa0faf6ddb7e84aa00176c0.jpg

$ shasum --algorithm=256 banner.jpg
3bb0461a9d784c3118b210911c55300f2b15529d1aa0faf6ddb7e84aa00176c0  banner.jpg

Practical example

Putting it together in a new, generic Hugo theme template. This code sample is a mock up of a header, partial template: themes/mytheme/layouts/partials/header.html.

The banner.jpg image would be located at: themes/mytheme/assets/images/banner.jpg.

 1{{- $banner := resources.Get "images/banner.jpg" }}
 2{{- $jpeg := $banner.Resize (printf "%dx%d jpeg" $banner.Width $banner.Height) }}
 3{{- $webp := $banner.Resize (printf "%dx%d webp photo" $banner.Width $banner.Height) }}
 4<header>
 5    <h1>{{ .Site.Title }}</h1>
 6    <div class="banner">
 7      <picture>
 8          <source srcset="{{ $webp.RelPermalink }}" type="image/webp">
 9          <img src="{{ $jpeg.RelPermalink }}" loading="lazy" width="{{ $banner.Width }}" height="{{ $banner.Height }}" alt="The skyline of Sydney, Australia">
10      </picture>
11    </div>
12</header>

Written by Ben Garrett

Did you find this article helpful?
Please consider tipping me a coffee as a thank you.
Ko-fi Buy Me a Coffee
Did you find this article helpful? Please consider tipping me a coffee or three as a thank you.
Tip using Ko-fi or Buy Me a Coffee