fshr

The musings of a grumpy hairless ape




Auto Image Resize

I posted recently on Mastodon that I’ve been looking at responsive image sizing for this site, specifically exploring using Hugo render hooks to automagically resize images on build.

About Hugo

For those who don’t know, Hugo is a static site generator used for building websites. The difference with an SSG is that rather than either write all the HTML for a site by hand, or generate it on the fly from a database (e.g. Wordpress), an SSG “builds” a site from a set of templates and content files, and produces a set of static HTML content which can be uploaded to a web server (hence static site generator).

As part of that render/generation step, it’s possible to get Hugo to do all kinds of extra steps via render hooks, including doing stuff like dynamic image resizing.

Why do I want dynamic image resizing?

We’re now well past the time when we can assume that people are only looking at websites via a desktop browser, and it’s actually relatively safe to assume that a large percentage of web traffic is actually down to mobile browsers (whether phone or tablet).

With that in mind, it’s generally good (polite) practice to offer readers a view of your site tailored for the device they’re using; a nice wide layout for when they’re coming from a desktop browser or landscape tablet, or a narrower layout for when they’re on a phone or portrait tablet.

There’s different ways to do this, e.g. through dynamically serving up different content based on a browser user agent, but (I think) it’s becoming more common to do responsive web design using standard HTML and CSS to dynamically lay out the page based on the browser window size.

It should be no surprise that this site is built to be responsive :-), specifically it uses CSS Grid to reflow the layout as the window size changes (if you’re on a desktop you can see this by resizing the browser window).

CSS Grid does a reasonable job of taking care of resizing the general layout of the page (though I’m probably not doing it in the best way), but it doesn’t do anything “natively” with images, if I put a 1000x1000px image on the page using Hugo markdown, then I’m going to end up with a 1000x1000px image in the browser whatever the screen size, which doesn’t look good.

It is possible to do responsive images by using CSS max-width, but you’re still going to download the full size image and just resize in the browser (so wasting bandwidth and possibly costing readers more money). A better option is to offer up different image sizes depending on the specific viewport (window) size, and display the image sized to the reader’s specific needs.

You can do this relatively easily using the <picture> element and the <img> srcset and sizes attributes, but you still need to have the various differently sized images available, prefereably without having to manually produce multiple sizes by hand for every image.

This is where Hugo’s render hooks and dynamic image resizing comes in…

How do I do render hooks in Hugo?

Hugo has the concept of render hooks. Render hooks override the rendering of Markdown to HTML, repalcing the default rendering with soemthing else of your choice.

For example, in a piece of Hugo content, I may have the following markdown:

![Front of display](front1.jpg "Front of display")

Hugo will take this markdown, and render it into HTML along the lines of:

<img src="front1.jpg" alt="Front of display" title="Front of display" />

Adding a render hook overrides that default render process and tells Hugo to use what I specify instead. For example, my current render hook for images takes the above markdown and instead generates:

    <picture>    
        <source media="(max-width: 500px)" srcset="/blog/20240711-obligatory-epaper-display/front1_hu4f5f0a3eafa8594b41f422cf88b4f7dc_1820808_200x0_resize_q75_h2_box.webp">
        <source media="(max-width: 750px)" srcset="/blog/20240711-obligatory-epaper-display/front1_hu4f5f0a3eafa8594b41f422cf88b4f7dc_1820808_450x0_resize_q75_h2_box.webp">
        <source media="(max-width: 900px)" srcset="/blog/20240711-obligatory-epaper-display/front1_hu4f5f0a3eafa8594b41f422cf88b4f7dc_1820808_300x0_resize_q75_h2_box.webp">
        <source media="(min-width: 900px)" srcset="/blog/20240711-obligatory-epaper-display/front1_hu4f5f0a3eafa8594b41f422cf88b4f7dc_1820808_500x0_resize_q75_h2_box.webp">
        <img src="front1.jpg" class="centered"  alt="Front of display"   title="Front of display" />
    </picture>

So rather than just offer the browser a single fixed size image, it now has a choice of image sizes depending on the viewport width. Should a browser not understand the <picture> element, there’s also still a fallback <img> tag with the original image (at it’s full size).

The sharp eyed reader will notice the slightly “funky” image names in the above, this is down to the next piece of the puzzle, namely the dynamic image resizing.

How to I actually resize the images?

So first you need to create a render hook. In the case of images, this is a file “render-image.html” placed within the layouts structure in the Hugo project. In my setup (as I’m not using Hugo templates), this is under:

.
├── archetypes
├── assets
├── content
├── dockerfile
├── layouts
|   ├── _defaults
|   |   └── _markup
|   |       └── render-image.html
|   └── partials
└── static

Currently, for this site that file looks like:

{{ if (.Page.Resources.GetMatch (printf "%s" (.Destination | safeURL))) }}
    {{ $image := .Page.Resources.GetMatch (printf "%s" (.Destination | safeURL)) }}

    {{ $narroww := default "200x webp" }}
    {{ $singlew := default "450x webp" }}
    {{ $dualw := default "300x webp" }}
    {{ $widew := default "500x webp" }}

    {{ $data := newScratch }}
    {{ $data.Set "narrow" ($image.Resize $narroww) }}
    {{ $data.Set "single" ($image.Resize $singlew) }}
    {{ $data.Set "dual" ($image.Resize $dualw) }}
    {{ $data.Set "wide" ($image.Resize $widew) }}

    {{ $narrow := $data.Get "narrow" }}
    {{ $single := $data.Get "single" }}
    {{ $dual := $data.Get "dual" }}
    {{ $wide := $data.Get "wide" }}

    <picture>    
        <source media="(max-width: 500px)" srcset="{{with $narrow.RelPermalink }}{{.}}{{ end }}">
        <source media="(max-width: 750px)" srcset="{{with $single.RelPermalink }}{{.}}{{ end }}">
        <source media="(max-width: 900px)" srcset="{{with $dual.RelPermalink }}{{.}}{{ end }}">
        <source media="(min-width: 900px)" srcset="{{with $wide.RelPermalink }}{{.}}{{ end }}">
        <img src="{{ $image }}" class="centered" {{ with .Text }} alt="{{ . }}" {{ end }} {{ with .Title }} title="{{ . }}" {{ end }}/>
    </picture>

{{ end }}

To then break this down…

  • The wrapper {{if}} {{end}} is there to sanity check that we actually have a source image where we think we do and I haven’t typo’d the markdown in any way
  • Assuming the source image is there, we get the details and load a reference into $image for later use
  • The lines of the format {{ $narroww := default "200x webp" }} define our different image sizes, and here I’m defining 4 different sizes with different widths, but all using the WEBP format.
  • The lines of the format {{ $data.Set "narrow" ($image.Resize $narroww) }} then tells Hugo to generate a new resized image for each of the 4 sizes based on the previous settings.
  • The lines of the format {{ $narrow := $data.Get "narrow" }} gets the details of the newly generated images, including their eventual location in the final rendered site.
  • Finally we generate the <picture> element HTML, substituting in the details of the generated images, plus the original image proeprties such as Title & Alt Text.

What happens next

With that render hook in place, whenever Hugo goes to render the site it will use the render hook for any images defined in any of my markdown content files. As part of that, it will generate the 4 differently sized images for each source image and output the appropriate HTML into the HTML file for the appropriate page.

When a browser hits the site, it reads the <picture> element and loads the appropriate image based on the current viewport size.

Job done!


Posted 18 July 2024

In Webdev