Rocking 11ty v3

I was excited to see that the wonderful 11ty team had released the next version. Since I was on version 2 for most of my sites, I decided to upgrade this site as a test of the process and document some of the things I found in case I ever get around to dealing with the others (of course I will).

Upgrade

A big change is that 11ty now fully supports ESM, which is quite a big change, so it was nice to see that there is a new upgrade helper package. This hooks into the build process and tests for incompatibilities in your configuration. Working very much like eslint, the idea is you:

  • install the package
  • import and add it to your eleventy configuration
  • run a build as normal
  • fix any errors
  • uninstall the package
  • commit changes
  • profit !!!

For the most part, this was completely trouble free, first I changed my package.json to include type: module to make the switch to ESM. Next came the actual code changes which I thought would take me 10 minutes except I was forgetting about the parts everywhere in my code where I was using module.exports = and require().

These changes applied to my custom filters, plugins, and shortcodes (as well as the tests for some of them), not forgetting the eleventy config itself so there was quite a bit to do. The upgrad helper helped identify these problems with a nice stack trace of the file causing the problem.

Breakages

I did find some breaking changes I was not expecting, or rather I was using 11ty in ways that it was not expecting. The main issue came down to several errors with the implementation of the search index which uses elasticlunr to generate a json document containing title, tags and an excerpt from each post. This json document is downloaded by the search page and fed to elasticlunr. It was too much to hope for that these errors would be fixed just by adding async to the function. I had originally cribbed the search implementation from Duncan McDougall's site and had customised it to use the page object's frontmatter and content objects to tune the output of the json file. The errors were things like 'Unhandled rejection in promise', 'TemplateContentPrematureUseError' and 'Change your code to use the async read() method on the template instead'. Searching on the 11ty issues page didn't help much until I stumbled on one of Zach's posts about template.read() and page.content.

Template Read

In v2, the search index generator (filter) code was using objects hanging off the page object that were introduced in v2 but have been removed for v3. First, the code was inspecting page.template.frontMatter and objects beneath it which gave me the template.read() error. This was fixed by following Zach's recommendation and using:


// replace instances of page.template.frontMatter.... with

const frontMatter = await page.template.read();

The await syntax doesn't work inside of a collection.forEach so I also had to replace that with a for construct.

TemplateContentPrematureUseError

The TemplateContentPermatureUseError was caused by something very similar on the page object. Page.content has been removed so that page.rawInput had to be used instead.

New Search Filter

This is what the new async version of the search filter looks like.


import elasticlunr from "elasticlunr";

async function searchFilter(collection) {
  const index = elasticlunr(function () {
    this.addField("title");
    this.addField("excerpt");
    this.addField("tags");
    this.setRef("id");
  });

  for(const page of collection){
    let excerpt = ' ';

    const frontMatter = await page.template.read();
   
    if (frontMatter.data.layout && frontMatter.data.layout === 'quotation') {
      if (frontMatter.data.attribution) {
        excerpt = frontMatter.data.attribution;
      }
    } else if (page.rawInput) {
      const excerptLength = 255; // chars
      let content = page.rawInput.replace(/(<([^>]+)>)/gi, "");
      content = content.replace(/\n/g, ' ');
      excerpt = content.substr(0, content.lastIndexOf(" ", excerptLength));
    }

    index.addDoc({
      id: page.url,
      title: frontMatter.data.title,
      excerpt: excerpt,
      tags: frontMatter.data.tags
    });
  }

  return index.toJSON();
};

export default searchFilter;

And I Forgot...

I have a suite of Cypress end to end tests that I run to check that important pages load, externally linked images are still good, the search functionality still works etc. Of course, because I set the whole site package.json file to use ESM, I had to fix up the cypress config to return an export default.