Jun 1, 2024

Upgrading to Eleventy v3

More Front-end Bloggers All from Max Böck View Upgrading to Eleventy v3 on mxb.dev

I took some time this week to upgrade my site to the newest version of Eleventy. Although v3.0.0 is still in alpha, I wanted to give it a try.

This iteration of mxb.dev is already 7 years old, so some of its internal dependencies had become quite dusty. Thankfully with static sites that didn’t matter as much, since the output was still good. Still, it was time for some spring cleaning.

Switching to ESM

Permalink to “Switching to ESM”

A big change in v3 is that Eleventy is now using ECMAScript Module Syntax (ESM). That brings it in line with modern standards for JS packages.

In “Lessons learned moving Eleventy from CommonJS to ESM”, Zach explains the motivation for the switch.

I’ve already been using ESM for my runtime Javascript for quite some time, and I was very much looking forward to get rid of the CommonJS in my build code. Here’s how to switch:

Step 1: Package Type

Permalink to “Step 1: Package Type”

The first step is to declare your project as an environment that supports ES modules. You do that by setting the type property in your package.json to “module”:

//package.json
{
    "name": "mxb.dev",
    "version:": "4.2.0",
    "type": "module",
    ...
}

Doing that will instruct node to interpret any JS file within your project as using ES module syntax, something that can import code from elsewhere and export code to others.

Step 2: Import Statements

Permalink to “Step 2: Import Statements”

Since all your JS files are now modules, that might cause errors if they still contain CommonJS syntax like module.exports = thing or require('thing'). So you’ll have to change that syntax to ESM.

You don’t need to worry about which type of package you are importing when using ESM. Recent node versions support importing CommonJS modules using an import statement.

Starting with node v22, you can probably even skip this step entirely, since node will then support require() syntax to import ES modules as well.

In an Eleventy v2 project, you’ll typically have your eleventy.config.js, files for filters/shortcodes and global data files that may look something like this:

const plugin = require('plugin-package')
// ...
module.exports = {
    something: plugin({ do: 'something' })
}

Using ESM syntax, rewrite these files to look like this:

import plugin from 'plugin-package'
// ...
export default {
    something: plugin({ do: 'something' })
}

There are ways to do this using an automated script, however in my case I found it easier to go through each file and convert it manually, so I could check if everything looked correct. It only took a couple of minutes for my site.

It’s also helpful to try running npx eleventy --serve a bunch of times in the process, it will error and tell you which files may still need work. You’ll see an error similar to this:

Original error stack trace: ReferenceError: module is not defined in ES module scope
[11ty]  This file is being treated as an ES module because it has a '.js'
        file extension and 'package.json' contains "type": "module".
        To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
[11ty]  at file://mxb/src/data/build.js?_cache_bust=1717248868058:12:1

If you absolutely have to use CommonJS in some files, renaming them to yourfile.cjs does the trick.

Gotchas

Permalink to “Gotchas”

Some minor issues you may encounter:

Eleventy Image Transform

Permalink to “Eleventy Image Transform”

Eleventy v3 also comes with a very useful new way to do image optimization. Using the eleventy-img plugin, you now don’t need a shortcode anymore to generate an optimized output. This is optional of course, but I was very eager to try it.

Previously, using something like an async image shortcode, it was not possible to include code like that in a Nunjucks macro (since these don’t support asynchronous function calls).

In v3, you can now configure Eleventy to apply image optimization as a transform, so after the templates are built into HTML files.

Basically, you set up a default configuration for how you want to transform any <img> element found in your output. Here’s my config:

// eleventy.config.js
eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
    extensions: 'html', // transform only <img> in html files
    formats: ['avif', 'auto'], // include avif version and original file type
    outputDir: './dist/assets/img/processed/', // where to write the image files
    urlPath: '/assets/img/processed/', // path prefix for the img src attribute
    widths: ['auto'], // which rendition sizes to generate, auto = original dimensions
    defaultAttributes: {
        // default attributes on the final img element
        loading: 'lazy',
        decoding: 'async'
    }
})

Now that will really try to transform all images, so it might be a good idea to look over your site and check if there are images that either don’t need optimization or are already optimized through some other method. You can exclude these images from the process by adding a custom <img eleventy:ignore> attribute to them.

All other images are transformed using the default config.
For example, if your generated HTML output contains an image like this:

<img
    src="bookcover.jpg"
    width="500"
    alt="Web Accessibility Cookbook by Manuel Matuzovic"
/>

The plugin will parse that and transform it into a picture element with the configured specs. In my case, the final HTML will look like this:

<picture>
    <source
        srcset="/assets/img/processed/Ryq16AjV3O-500.avif 500w"
        type="image/avif"
    />
    <img
        src="/assets/img/processed/Ryq16AjV3O-500.jpg"
        width="500"
        alt="Web Accessibility Cookbook by Manuel Matuzovic"
        decoding="async"
        loading="lazy"
    />
</picture>

Any attributes you set on a specific image will overwrite the default config. That brings a lot of flexibility, since you may have cases where you need special optimizations only for some images.

For example, you can use this to generate multiple widths or resolutions for a responsive image:

<img
    src="doggo.jpg"
    width="800"
    alt="a cool dog"
    sizes="(min-width: 940px) 50vw, 100vw"
    eleventy:widths="800,1200"
/>

Here, the custom eleventy:widths attribute will tell the plugin to build a 800px and a 1200px version of this particular image, and insert the correct srcset attributes for it. This is in addition to the avif transform that I opted to do by default. So the final output will look like this:

<picture>
    <source
        sizes="(min-width: 940px) 50vw, 100vw"
        srcset="
            /assets/img/processed/iAm2JcwEED-800.avif   800w,
            /assets/img/processed/iAm2JcwEED-1200.avif 1200w
        "
        type="image/avif"
    />

<img src="/assets/img/processed/iAm2JcwEED-800.jpeg" width="800" alt="a cool dog" sizes="(min-width: 940px) 50vw, 100vw" srcset=" /assets/img/processed/iAm2JcwEED-800.jpg 800w, /assets/img/processed/iAm2JcwEED-1200.jpg 1200w " decoding="async" loading="lazy" /> </picture>

I ran a quick lighthouse test after I was done and using the image transform knocked my total page weight down even further! Good stuff.

Other Stuff

Permalink to “Other Stuff”

I refactored some other aspects of the site as well - most importantly I switched to Vite for CSS and JS bundling. If you’re interested, you can find everything I did in this pull request.

Scroll to top