Shortcodes are fucking awesome: Using Hugo to convert images.

Now for the sequel to “Shortcodes are awesome”. Did you know that Hugo supports image processing? As in image conversion? This is damn good, because prior to today I was storing all my raw images in this repo at .other/raw-images/ and they were being sent to cloudflare each time a build was initiated. Plus it was so annoying trying to track what I put up in posts and had transcoded myself (in a seperate dir) and also have a copy of in that folder.

So last night I dedicated an hour to trying to figure out how I could upload images to one location, and have those be converted/transcoded live on build so I could halve my file count.

And this is what I came up with:

{{/*
  Shortcode: modern_image
  Description: Displays an image, converting to WebP if not animated (i.e., not a GIF). By jonblack.gg / BiosPlus
  Parameters:
    src (string, required): Path to the image relative to 'assets/images/'.
    alt (string, optional): Alt text for the image. Defaults to 'title' or 'src'.
    caption (string, optional): Caption for the image, displayed in a <figcaption>.
    class (string, optional): CSS class for the <figure> element.
    loading (string, optional): Value for the img loading attribute (e.g., "lazy", "eager"). Defaults to "lazy".
    width (string, optional): Width attribute for the img tag.
    height (string, optional): Height attribute for the img tag.
    quality (string, optional): Quality for WebP conversion (e.g., "75"). Appended to "webp q".
  Usage:
    modern_image src="example.jpg" alt="An example image" caption="This is a caption."
    modern_image src="animated.gif" alt="An animated GIF"
*/}}

{{- $src := .Get "src" -}}
{{- if not $src -}}
  {{- errorf "modern_image shortcode: 'src' parameter is required." -}}
  {{- return -}}
{{- end -}}

{{- $alt := .Get "alt" | default (.Get "title") | default $src -}}
{{- $caption := .Get "caption" -}}
{{- $class := .Get "class" -}}
{{- $loading := .Get "loading" | default "lazy" -}}
{{- $imgWidth := .Get "width" -}}
{{- $imgHeight := .Get "height" -}}

{{- $imageAssetPath := printf "images/%s" $src -}}
{{- $image := resources.Get $imageAssetPath -}}

{{- if not $image -}}
  {{- errorf "modern_image shortcode: Image '%s' not found in 'assets/images/'. Full path attempted: '%s'" $src $imageAssetPath -}}
  {{- return -}}
{{- end -}}

<figure{{ with $class }} class="{{ . }}"{{ end }}>
  {{- if eq $image.MediaType.SubType "gif" -}}
    {{- /* Assume GIFs are animated and serve them directly to preserve animation. */ -}}
    <img src="{{ $image.RelPermalink }}"
         alt="{{ $alt }}"
         {{- with $loading }} loading="{{ . }}"{{ end -}}
         {{- with $imgWidth }} width="{{ . }}"{{ end -}}
         {{- with $imgHeight }} height="{{ . }}"{{ end -}}>
  {{- else -}}
    {{- /* For non-GIF images, process to WebP. */ -}}
    {{- $webpOptions := "webp" -}}
    {{- with .Get "quality" -}}
      {{- $webpOptions = printf "webp q%s" . -}}
    {{- end -}}
    {{- $webpImage := $image.Process $webpOptions -}}
    <img src="{{ $webpImage.RelPermalink }}"
         alt="{{ $alt }}"
         type="image/webp"
         {{- with $loading }} loading="{{ . }}"{{ end -}}
         {{- with $imgWidth }} width="{{ . }}"{{ end -}}
         {{- with $imgHeight }} height="{{ . }}"{{ end -}}>
  {{- end -}}
  {{- with $caption -}}
    <figcaption>{{ . | safeHTML }}</figcaption>
  {{- end -}}
</figure>

This amalgamation of bastardry is called the modern_image shortcode. Drop that around any existing image and it will tell hugo on build that if it’s a static image, to convert it to webp.

It simply works by surrounding an image src like so: modern_image src="2025/06/image.png" alt="image that is going to be converted", and supports additional params like quality, width, height, and a fucking caption setting that will create an additional line of centred text under the image itself.

I originally built this to do jpegXL, which will probably be the best image format of the modern age once the fucking losers at the chromium team eat dirt (rant for another time), but Hugo or atleast the cloudflare pages runners don’t support jpegxl yet. Hopefully not too far away though the second it gets support in Firefox, I’ll be switching this to transcode all images on this blog to jpegXL + including a piece of javascript so that if you’re caught coming here in a chromium based browser you’ll be informed that you’re using an inferior software.

For now, WebP will have to do.

And damn am I proud of this.