Render hooks in Hugo
9th April 2025
Readers beware: technical post about the blog’s backend lies ahead.
One thing I quite like about Hugo, the static site generator that I’ve recently ported this blog to, is how it handles images. Instead of having a post in the content/posts/
directory reference images in the /static
directory, you can use ‘page bundles’ to associate the images with specific posts like so:
/content/posts/
├── post1.md
├── post2/
│ ├── index.md
│ └── post2-image.jpg
└── post3.md
Not only does this keep everything neat and tidy, but it also makes my whole post-structure far more portable. If you specify image attributes in your CSS and don’t apply any build-time optimisations, this is where you stop. You can reference images with standard Markdown syntax, and they’ll display nicely in both whatever processor you use to write your posts as well as on your final site.
However, I have an image shortcode which applies some built-time optimisations – it resizes the image to 2x the max-width of the screen, converts it to webp, and a few other compression things. If you want to do something like this, you have to call the shortcode using Hugo’s templating syntax, e.g. {{< img src="image1.jpg" >}}
. This is perfectly serviceable, but compromises on a few things. I lose portability: if I ever want to leave Hugo, it’s going to be a pain to reroute all of my image paths. It’s also cumbersome syntax which isn’t in my muscle memory, so breaks writing flow when I have to reference the source of older posts to figure out how to format them. Most significantly, it doesn’t play nice with how images are handled in Obsidian, which is what I use to write and manage my posts. If I want to see whether an image is rendering properly, I have to start up a live-server every time, which is onerous.
This is where render hooks come in. Instead of a shortcode, I can offload all of my image styling and processing to a piece of html that will intercept before markdown is rendered to HTML, and instead reroute it through whatever processing you specify in the render hook. This means that you can reference images using standard markdown syntax, i.e. 
, and still benefit from all of the build-time image processing that Hugo offers. I actually have zero image styling in my style.css
, it’s all handled by this one render hook.
Furthermore, render hooks can string parse for .Title
, so if you want to override the default parameters you specified in your render hook trivially. For instance, my default max-width
is 100%, but let’s say I have a really tall image; I can simply reference the image in my markdown as 
and the render hook will substitute the default max-width
for my input. You can do this kind of string parsing for any parameter you specify in the render hook.
If you want to replicate how I handle images, create render-image.html
at /layouts/default/_markup/
, and populate it with the following:
{{ $src := .Destination }}
{{ $alt := .Text | default "" }}
{{ $title := .Title | default "" }}
<!-- Default style values -->
{{ $maxWidth := "100%" }}
{{ $maxHeight := "400px" }}
{{ $margin := "30px auto" }}
<!-- Parse parameters from title attribute -->
{{ if $title }}
{{ if findRE "max-width=" $title }}
{{ $maxWidth = replaceRE ".*max-width=([^ ]*).*" "$1" $title }}
{{ end }}
{{ if findRE "max-height=" $title }}
{{ $maxHeight = replaceRE ".*max-height=([^ ]*).*" "$1" $title }}
{{ end }}
{{ if findRE "margin=" $title }}
{{ $margin = replaceRE ".*margin=([^ ]*).*" "$1" $title }}
{{ end }}
{{ end }}
{{ $resource := .Page.Resources.GetMatch $src }}
{{ if $resource }}
{{ $jpeg := $resource.Fit "2680x800" }}
{{ $webp := $jpeg.Process "webp" }}
<picture>
<source srcset="{{ $webp.RelPermalink }}" type="image/webp" sizes="100vw">
<source srcset="{{ $jpeg.RelPermalink }}" type="{{ $jpeg.MediaType }}" sizes="100vw">
<img
src="{{ $jpeg.RelPermalink }}"
alt="{{ $alt }}"
loading="lazy"
decoding="async"
style="display: block; max-width: {{ $maxWidth }}; max-height: {{ $maxHeight }}; height: auto; width: auto; margin: {{ $margin }};">
</picture>
{{ else }}
<img src="{{ $src }}" alt="{{ $alt }}" style="display: block; max-width: {{ $maxWidth }}; max-height: {{ $maxHeight }}; height: auto; width: auto; margin: {{ $margin }};">
{{ end }}