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.
Baseline: WebP is widely available
- Google Chrome
9 2011.02.03 - Microsoft Edge
18 2018.10.02 - Firefox
65 2019.01.29 - Safari
14 2020.09.16
Baseline: AVIF is newly available
- Google Chrome
85 2020.08.25 - Microsoft Edge
121 2024.01.25 - Firefox
93 2021.10.05 - Safari
16 2022.09.12

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.

<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 👋
Quite unique solution for a lack of webp/avif support in Hugo and impressive 100 score on page speed even when serving full-size images. Off course, most of your images are below the fold, hence not a big issue. I am using image.resize in render hooks for my images to generate them in different sizes (locally) however with your approach I start questioning myself is that needed. Despite PageSize Insight reporting an opportunity: Properly size images, however not taking point for that. I will seriously need to re-think if is worth to resize images or just move with WebP/AVIF approach as you describe.
For me it works like a charm and save is significant!
Hi there! Because svg is no supported in Hugo imageConfig, I propose you to alter the code as following to make it more universal (as otherwise it fails with an exception when it finds svg):
Hello,
It doesn't work for me...
I will be able to help only if you provide a little bit more details. Maybe some error from the console?
Sorry for the late reply. I will try to post in the following days errors I receive. Even if I can't find a solution to this issue with svg I thank you a lot because I was able to use webp and avif in my blog.
I think adding SVG support, requires very few additions to this code. I will be happy to help you out Arnauld.
Whole thing simplification:
Sincere apologies for not checking it properly, here is the correct multiple replace code:
How do I make this code work for the img in a Page Bundle?
Hi. I am not sure about that, sorry, I have never used page bundles.
I write a post about WebP and AVIF with Page bundles and I link this post as reference/source
Thanks a lot! Great addition to my post!
The code included gives the follow error; error calling imageConfig: image: unknown format.
What is the Hugo version that you use?
There are a couple initial issues I ran into when using this snippet:
Note I haven't incorporated the other enhancements like SVG support mentioned in the comments.
We don't need to use
imageConfig
to access the image width and height, so I swapped this:with this:
And I used
replaceRE
instead ofreplace
to use regex to find and replace both.jpg
and.jpeg
. Ex:{{ $avifPath:= replaceRE "
(jpg|jpeg)$i" ".avif" .Destination }}
Hi, thanks for your input!
Thanks for great input Tim!
I did keep the width and height :) you're right, they're also required for valid HTML. Forgot to mention, my updated snippet starting at line 2 is borrowed from @bep's render hook, I don't know exactly what it does 😄 I also added an optional title attribute to the
img
tag and wrapped the entire thing in an optionalfigure
+figcaption
if a title does exist, based on Sebastian's idea.Hi Tim! Would you please post your full code. Thank you!
Hey Pawel, thank you a ton for this. I am working with Hugo for fun on a very basic level and usually shy away from touching the more tricky stuff. Your article was super easy to understand and even I managed to make it work :D Thank you!
I am glad that my article helped you out! Enjoy!