Skip to content

WebP and AVIF images on a Hugo website

Hugo is the tool that I use to generate this website statically. It’s extremely fast, configurable and comes with a bunch of useful features. Added in version 0.62 Markdown Render Hooks are revolutionary. This feature allows overriding default HTML markup generated by parsing Markdown links, headings and images.

The HTML <picture> element is convenient to serve multiple image versions for different scenarios. It may specify some alternative version based on the display resolution, screen density, operating system appearance mode, and the most optimised file format based on the browser support. Using modern formats like WebP or even AVIF, we can gain significant performance improvements.

Image of WebP and AVIF specificatoins

Using Hugo image render hook sounds like an excellent feature to take full advantage of modern <picture> tag capabilities. Instead of serving the only canonical file, we can do it’s more performant siblings in WebP and AVIF format. Let’s make it happen!

Override default image HTML markup using Hugo image render hook #

Let’s have a look at the Markdown image example and its parsed HTML equivalent.

![Love of my life](cat.jpg)
<img src="cat.jpg" alt="Love of my life">

It does the job, but we can do a tad better than that in 2021. Let’s have a look at the example of an HTML that we would like to get.

<picture>
  <source srcset="cat.avif" type="image/avif" />
  <source srcset="cat.webp" type="image/webp" />
  <img
    src="cat.jpg"
    alt="Love of my life"
    loading="lazy"
    decoding="async"
    width="800"
    height="600"
  />
</picture>

Image render hook comes to the rescue. It’s a basic HTML file that resides in layouts/_default/_markup/render-image.html. The implementation that works for me looks like this, but feel free to adjust it to your needs. If you struggle, feel free to ping me on Twitter or use the comment section below — I’m more than happy to help, as always.

<picture>
  {{ $isJPG := eq (path.Ext .Destination) ".jpg" }}
  {{ $isPNG := eq (path.Ext .Destination) ".png" }}

  {{ if ($isJPG) -}}
    {{ $avifPath:= replace .Destination ".jpg" ".avif" }}
    {{ $avifPathStatic:= printf "static/%s" $avifPath }}

    {{ if (fileExists $avifPathStatic) -}}
      <source srcset="{{ $avifPath | safeURL }}" type="image/avif" >
    {{- end }}

    {{ $webpPath:= replace .Destination ".jpg" ".webp" }}
    {{ $webpPathStatic:= printf "static/%s" $webpPath }}

    {{ if (fileExists $webpPathStatic) -}}
      <source srcset="{{ $webpPath | safeURL }}" type="image/webp" >
    {{- end }}
  {{- end }}

  {{ if ($isPNG) -}}
    {{ $avifPath:= replace .Destination ".png" ".avif" }}
    {{ $avifPathStatic:= printf "static/%s" $avifPath }}

    {{ if (fileExists $avifPathStatic) -}}
      <source srcset="{{ $avifPath | safeURL }}" type="image/avif" >
    {{- end }}

    {{ $webpPath:= replace .Destination ".png" ".webp" }}
    {{ $webpPathStatic:= printf "static/%s" $webpPath }}

    {{ if (fileExists $webpPathStatic) -}}
      <source srcset="{{ $webpPath | safeURL }}" type="image/webp" >
    {{- end }}
  {{- end }}

  {{ $img := imageConfig (add "/static" (.Destination | safeURL)) }}

  <img
    src="{{ .Destination | safeURL }}"
    alt="{{ .Text }}"
    loading="lazy"
    decoding="async"
    width="{{ $img.Width }}"
    height="{{ $img.Height }}"
  />
</picture>

This solution is pretty safe to use. It assumes that the files are named the same as the file extension is the only difference. For example: cat.jpg, cat.webp and cat.avif. Before injecting a new WebP or AVIF resource to the markup, it first checks if a particular file exists in a static directory.

Generate WebP and AVIF formats #

Some of the modern graphic design tools, like Sketch or Photoshop, fully support WebP format already. These are early days for AVIF format, but there are some GUI options for it as well. There’s even Squoosh web app that does a great job. My preferred set of tooling to generate these modern formats are cwebp and avifenc command-line tools. If you are macOS user, Homebrew is probably the best place where you can get them from.

brew install webp
brew install joedrago/repo/avifenc

And this is an example how to use these CLIs.

cwebp cat.jpg -o cat.webp
avifenc --min 10 --max 30 cat.jpg cat.avif

If you are planing to convert all images in a folder to a desired format, a snippet like this may come in handy.

find ./ -type f -name '*.png' -exec sh -c 'avifenc --min 10 --max 30 $1 "${1%.png}.avif"' _ {} \;
find ./ -type f -name '*.jpg' -exec sh -c 'cwebp $1 -o "${1%.jpg}.webp"' _ {} \;

The result of it #

The whole process of adding a hook and regenerating all images on my website took me about two hours. After all, I managed to serve to my visitors around 50% slimmer images — pretty significant performance gain. Source code to my blog on GitHub is waiting for curious ones to be explored.

Hopefully, you found this article helpful, and you learned a thing or two. If you have any questions, please use the comments section below or ping me on Twitter. For now, stay safe 👋

comments powered by Disqus