One Image Two Days: Optimizing images in Hugo

March 27, 2022

5 minute read

I added a hero image to my homepage, and here’s the breakdown on how long it took me:

  1. Add the image with an <img> element and style it: 1 minute
  2. Add image optimization to my Hugo templates: 2 days


Previously, I used Hugo’s render-image hook to optimize the images I put in my Markdown files, with code by Dave Bennett. This worked well and gave me WebP images with a set of different sizes.

You can also make a custom img shortcode for this, however Hugo shortcodes only work in Markdown files and not templates, which is what I needed for my homepage template. This was confusing to me, coming from 11ty and Nunjucks, where you can use shortcodes anywhere.

Hugo uses partial templates instead of shortcodes in templates, which can accept arguments or just act as a reusable component. This is similar to Nunjuck’s include.

However, I wanted to use the same image processing code for both Markdown files and templates. Trying to stay DRY over here (don’t look at my theme repo though, it’s very wet).


I discovered DFD’s excellent image handling module for Hugo. I encountered some issues, so I forked it to customize it for my site’s setup.

Some things I changed:

  • Removed DFD’s link handler module
    • It was adding empty <a> tags around images, and <span> tags around other links. Will try to narrow this down and open a PR.
    • It changes your external links to open in the same tab by default with no config option though, and I prefer new tabs for external links (target="_blank"). There is a solid case though for the “Don’t Break the Back Button”1 rule and I’m slowly coming around on this.
    • There’s still some code in my fork that depends on the module if you use the link functionality, and I may end up forking the link handler in the future to make it work the way I want it to. The link checking is handy.
  • Use <picture> element by default instead of <img>
    • This is personal preference, until I add a JPEG fallback
    • Removed the src attribute from the <source> element as this only used for audio/video (spec).
  • Add link to Markdown images so the user can view them full-size in their browser (I don’t plan on adding a lightbox viewer due to the JavaScript weight)
    • This doesn’t work for images from /assets though
  • My cover image URLs are in the cover: key, so I added it to find-featured-images.html

This resulted in two days of going through confusing Hugo template code across 7 layout files, but I’ve definitely learned a lot about how Hugo templating works.

I still need to add JPEG fallback to the fork, currently it only outputs one format (WebP) in the source set. WebP is supported by all major browsers, but Safari added support not even two years ago, so it’s best to continue to add a JPEG fallback.

Hugo module issues

I also learned a bit about Hugo modules. I encountered a lot of difficulty trying to fork the module and then point my Hugo install at my fork, and I still don’t know what worked exactly. It was a combination of deleting go.mod/sum and liberal use of hugo mod clean.

And I learned that Hugo modules don’t like getting daisy-chained: I tried to add my theme as a module, and then add a config to the theme that has additional modules needed by the theme, and I encountered some weird error with the last module in the list of 3 not being found. I tried to reorder them or remove one to no effect.

So now my Hugo project folder is set up with all the modules in the main config, and then my theme + forked modules are cloned into the themes directory. Then I added a replacements option to my development/config.yaml to tell Hugo to use the local modules when I’m running hugo server:

2  replacements: " -> hugo-cactus-theme, -> image-handling-mod-hugo-dfd"

All this to avoid using Git Submodules, which probably would have been easier at this point.


I have a partial I can use whenever I need to put an image on a page, though it is pretty complicated. This is the one I’m using for the hero image on the home page:2

1{{ partial "helpers/wrapped-image" (dict "alt" "Luke in the shower" "image" (partial "helpers/lib/image-handling/find-image-src" (dict "src" "layouterror.jpg" )) "page" .Page "class" "img-fluid h-25" "noImageWrapper" "true" "loading" "eager") }}
2{{/* We're using loading="eager" instead of "lazy" because the image is above the fold. */}}

And this is what it looks like in the generated HTML:

Screenshot of picture element in browser dev tools

Nice optimized WebP images, hurray! And it’s the same logic for rendering Markdown images natively, so I can continue to add images the Markdown way (![alt](imgURL)).


Needless to say, optimizing images is important and one of the first things to look at when you’re trying to speed up a site.

However, one of the struggles with the whole JAMstack/static site generator (SSG) trend is optimizing images, because very few of the SSGs out there do it for you. And they all need some sort of wrangling to get it working.

Most image optimization methods I’ve seen for SSGs involve just using a third-party optimization service, and I’m not down with this for two reasons:

  1. Cost - yet another thing I have to pay for and/or keep track of quotas
  2. Broken image URLs if the service goes down or I have to switch

If you want your Markdown images optimized, a lot of them want you to use a custom shortcode in your Markdown instead of the Markdown way, which isn’t good for content portability if you change your SSG later.

This is one of the reasons I switched to Hugo last year, because of the render-image hook enables me to optimize images in my posts and not just in templates.

  1. Top Ten New Mistakes of Web Design (1999) and Don’t break the back button (2011) ↩︎

  2. Can’t wait for the alt text “Luke in the shower” to get copy-pasted into someone’s template. ↩︎

Tagged with meta hugo web

1012 words

  Reply via email