Home

Automatic image pre-processing in Eleventy, Part 2

2024-03-13

Background

One of the Eleventy customizations I've set up for this site is automatic generation of responsive <picture> tags from images in Markdown posts. I was going to build it myself using the Eleventy Image plugin, but then randomly stumbled upon a plugin for the markdown-it Markdown processor used by Eleventy that did exactly that. It's called markdown-it-eleventy-img and provides the glue needed between the Eleventy Image plugin and the Markdown parsing performed by markdown-it. I wrote a post about this setup back in the day, but that is now obsolete. Starting with version 4.0.0, the Eleventy Image plugin itself performs all the steps needed itself.

Motivation

The main reason I want this functionality on my site is lazyness. Or, rather, I want to be lazy without paying the price! I want to be able to just dump a large JPEG or PNG image into the directory of one of my posts, include it as a regular Markdown image, and have it offered to the browser both in multiple sizes and various modern image formats.

Configuration

To get this functionality set up using Eleventy Image, you first need to install the plugin using npm:

npm install @11ty/eleventy-img

Next up, add it to your .eleventy.js and start adding some configuration:

const { eleventyImagePlugin } = require("@11ty/eleventy-img");

module.exports = function (eleventyConfig) {
    eleventyConfig.addPlugin(eleventyImagePlugin, {
        // The image formats to generate, in order of preference
        formats: ["avif", "webp", "auto"],

        // The images sizes to generate
        widths: [368, 736, 900],

        defaultAttributes: {
            sizes: "auto",
        },
    });

    // Further configuration ...
};

Assumptions

The first time I set this functionality up, I realized my assumption about the browser's image format selection was incorrect. I thought that you could just offer a number of different formats, and the it would pick the best one automatically. In hindsight that feels a bit naïve, what is even the best format? Instead, the browser picks the first format it has support for, so the order of the formats matter.

Once my post on the subject was published I got some very helpful feedback, that made me realize my assumption about the size selection was also incorrect. Again, I thought you could offer a number of different image sizes, and the browser would pick the most suitable one automatically, but, again, it does not. Instead it's up to the developer to describe to the browser under what circumstances the different sizes should be used, using media queries.

I haven't specified any media queries in my code, just a single auto value, which acts as a default fallback rule. This will pick the image size best matching the full width of the image's parent element, which works well for me, as all of the (very few) images I use on this site are intended to be displayed across the full width of the page. With more varying image sizes I guess the ideal solution would be to set breakpoints for each individual image, but since my main motivation here is lazyness, I prefer a decent catch-all solution to a perfect one requiring a lot of manual work.

Fine-tuning

Using the above code, the responsive image generation just worked. I then proceed to add a couple of very optional extras to my configuration, specifically loading: "lazy" and decoding: "async". This improves initial page load speed, since the loading of off-screen images can be deferred until the user scrolls down to them. Having these attributes on all images is a bit of an anti-pattern, since images near the top of the page should ideally be loaded in-line rather than asynchronously. However, most of my images tend to be near the end of the post, so I don't feel too bad about adding the attributes.

They are added to the defaultAttributes, which are inserted into all generated <img> tags:

defaultAttributes: {
    sizes: "auto",
    loading: "lazy",
    decoding: "async"
},

Another small detail I've changed is the naming of the scaled images. This can be controlled by specifying a custom filenameFormat function. I know I can safely do this on this specific site, since I keep all post-related images in the directory of the post, but read up on it and make sure you pick a format that works for your site without causing any duplicate names:

const path = require("path");

...

filenameFormat: function (id, src, width, format) {
    let filename = path.basename(src, path.extname(src));
    return `${filename}-${width}.${format}`;
},

So, all in all, my configuration looks like this:

eleventyConfig.addPlugin(eleventyImagePlugin, {
    // The image formats to generate, in order of preference
    formats: ["avif", "webp", "auto"],

    // The images sizes to generate
    widths: [368, 736, 900],

    defaultAttributes: {
        sizes: "auto",
        loading: "lazy",
        decoding: "async",
    },

    filenameFormat: function (id, src, width, format) {
        let filename = path.basename(src, path.extname(src));
        return `${filename}-${width}.${format}`;
    },
});

Result

The output of the above configuration means a simple Markdown image like this:

![My image](./image.png)

Turns into this HTML:

<picture>
    <img loading="lazy" decoding="async" src="/.../image-368.png" alt="My image" width="900" height="762" srcset="/.../image-368.png 368w, /.../image-736.png 736w, /.../image-900.png 900w" sizes="auto">
    <source type="image/avif" srcset="/.../image-368.avif 368w, /.../image-736.avif 736w, /.../image-900.avif 900w" sizes="auto">
    <source type="image/webp" srcset="/.../image-368.webp 368w, /.../image-736.webp 736w, /.../image-900.webp 900w" sizes="auto">    
</picture>

In this example, the image is located in the same directory as the post, which is the structure I'm always using. The truncated path is the path of the post, e.g. /posts/my-post/. You can control the output location of the Image plugin using the urlPath option, but I prefer keeping the images with the rest of the post.

Another thing that might be good to keep in mind is that the image pre-processing will find all images in your pages, even e.g. the little header image on this site. That one I've already optimized manually, and I like to keep it as it is. You can both override certain parameters as well as have specific images be ignored entirely by the pre-processor using special parameters on the HTML elements themselves. For example, to have the Image plugin ignore my header image, I've added the eleventy:ignore parameter to it, like so: <img eleventy:ignore ... />. Check out the Image plugin documentation for the full specification.

Conclusion

It seems like with every release of Eleventy or its plugins, a customization I've built for this site becomes redundant, and I couldn't be happier about it. Even though the above solution does exactly the same thing as my previous one, I feel more comfortable using the same functionality from an official plugin. It also means that it will be maintained and keep working with future Eleventy versions.

If you want to read more about the ways I'm using Eleventy on this site, I've collected all my posts on a separate Eleventy page. Happy blogging!