Skip to main content
The cover image for "I've switched to Eleventy (11ty): my thoughts"

I've switched to Eleventy (11ty): my thoughts


After nine years of using Jekyll, I've switched to using [Eleventy]( (11ty) and redesigned my blog whilst I was at it. I was finding that Jekyll was too limiting and wanted something more capable. In this article, I will talk about why I switched, how I did it, and my thoughts about Eleventy as a new user.

After nine years of using Jekyll, I’ve switched to using Eleventy (11ty) and redesigned my blog whilst I was at it. I was finding that Jekyll was too limiting and wanted something more capable. In this article, I will talk about why I switched, how I did it, and my thoughts about Eleventy as a new user.

Jekyll, and why I decided to switch #

I chose to use Jekyll in 2014 because it was the only static site generator that GitHub Pages supported. It allowed me to use multiple pages without having to duplicate all the layout markup, making it easier to grow the size of my website.

Recently, I’ve been finding Jekyll increasingly painful to use and too limited for my purposes. Since 2022, my goal has been to write at least twelve posts a year. This means that I’ve been working on posts much more frequently than I have done in the past. Ease of writing posts is much more important now.

One of my biggest annoyances with writing a post was creating thumbnails for my images - this was quite time-consuming; I wanted to fully automate it in a manner that I don’t even need to think about when writing a post.

Another thing was that the files were split into different places - the text was placed in _posts and the images were in static. I wanted the ability to make a single folder for an article and get the generator to automatically move the assets to where they are needed.

📁 static
├─ 📁 hello-indieweb
|  └─ 🖼️ cover.png
└─ 📁 switched-to-eleventy
   ├─ 🖼️ cover.png
   └─ 🖼️ schematic.png
📁 _posts
├─ 📄
└─ 📄
📁 posts
├─ 📁 2023-10-10-hello-indieweb
|  ├─ 📄
|  └─ 🖼️ cover.png
└─ 📁 2023-10-27-switched-to-eleventy
   ├─ 📄
   ├─ 🖼️ cover.png
   └─ 🖼️ schematic.png
Which would you prefer?

Whilst I could have hacked together more scripts to fix these issues, my setup was already quite convoluted. To generate my writing statistics page, I had a mess of Python scripts and complicated layout code. Liquid isn’t the most capable language to use by itself. So, I decided to look for other static site generators.

My requirements #

My first requirement was that it must be a static site generator. A static site is the easiest type of website to host - it can be hosted on any web host, even a Content Delivery Network (CDN). This gives a lot of flexibility. It also increases longevity - if, for whatever reason, I cease maintaining my website, it can continue to be hosted as a low-tech static website for a long time. To get dynamic features like commenting, I have a NodeJS server hosted on a separate subdomain.

The second requirement was that it should not require any client-side JavaScript. Requiring JavaScript is bad for SEO and accessibility and bloats the webpage. Client-side JavaScript should only be used to enhance the behaviour.

The third requirement was that the generator must be easy to extend and customise to match my workflow. I’m not fond of Ruby or Bundler; I found Bundler quite painful to use. I felt like JavaScript/TypeScript would be the better language to use as it is a web technology.

The final requirement is the generator must be fully open source. Using open-source tools is vital to achieve flexibility and longevity.

Switching to Eleventy #

After looking into the options, I decided to give Eleventy a go. I started by setting up an Eleventy project and porting a single blog post. To derisk the process, I focused on implementing and prototyping the unknowns first, such as thumbnails, SCSS, post/tags, and writing statistics. This allowed me to quickly verify whether Eleventy was a good fit before spending a lot of effort porting all my content across.

I also decided to use this opportunity to redesign my blog as well. I used to use Bootstrap; I wanted to go with something a bit more custom and unique this time. I also wanted a light/dark mode switcher.

Before I published the new Eleventy version to production, I wrote a simple script to make sure that no URLs had changed or were missing. Cool URLs never change, so I considered verifying this important.

The good #

Eleventy-Image #

Eleventy-Image is an official plugin for Eleventy that allows you to resize images and create thumbnails.

Using Eleventy-Image, I defined custom template functions to create image thumbnails based on how they are used. For example, the figure function creates a 540px width thumbnail and then renders a <figure> element to the HTML.

{% figure "./schematic.png",
    "The schematic for my plant watering system's circuit. Created using KiCAD." %}

The source image is stored in the post’s directory. The figure function saves resized versions to the static directory. This allows me to keep the text and images of a post together, and only create specific image sizes when needed.

Flexibility #

Eleventy is very flexible, powerful, and fairly easy to customise. Using JavaScript, you can define custom template filters and functions. You can even build entire pages using JS - or JSX with an additional library.

It was incredibly easy to add features such as a table of contents, writing statistics, and backlinks. For a table of contents, I used an existing plugin and only needed to add {{ toc }} to place it. I implemented writing statistics, word counts, and backlinks by creating plugins to add template functions using JavaScript.

// Usage:
// {% assign backlinks = | links_to: page.url %}
eleventyConfig.addFilter("links_to", async function(posts, targetURL) {
    return posts.filter(post => getLinks(post.content).has(targetURL));

Another thing is that Eleventy allows you to customise or provide your own Markdown parser, or use different formats entirely. To get heading anchors (#), I just needed to enable that option.

Finally, Eleventy has support for JavaScript data files, which is great for fetching data from the network before building the site. I used this feature to load Webmentions from the REST API.

The bad #

Learning curve and inconsistencies #

Eleventy has quite a lot of quirks that make learning it quite difficult. It took much longer than I expected to switch to Eleventy.

First, Eleventy uses variables inconsistently. At the top of each page, there’s metadata known as front matter:

title: I've redesigned my blog and switched to Eleventy
description: ""
  - fullstack
  - reviews

To access the title and URL inside a template, you do title and page.url. Inside JavaScript or when reading a collection, it’s, item.url, or

Having a lot of global variables inside the template is quite confusing. It’s not just pages - Liquid allows you to include other liquid files. In Jekyll, you access parameters by writing include.param1. In Eleventy, the parameter is just param1.

The Eleventy liquid syntax is also slightly different from Jekyll liquid, for some reason. To include a directory, you need to quote the file name and use : instead of =:

<!-- Eleventy -->
{% include "youtube.html", id: "dQw4w9WgXcQ", caption: "Funny cat videos" %}

<!-- Jekyll -->
{% include youtube.html id="dQw4w9WgXcQ" caption="Funny cat videos" %}

But custom shortcodes do not support named parameters, you can only pass them in by order:

{% youtube "dQw4w9WgXcQ", "Funny cat videos" %}

Painful to read error messages #

The error messages produced by Eleventy are very long and point to the wrong lines in templates. It looks like the line numbers don’t include the height of the front matter.

[11ty] Problem writing Eleventy templates: (more in DEBUG output)
[11ty] 1. Having trouble rendering liquid template ./ (via TemplateContentRenderError)
[11ty] 2. tag "doesnotexist" not found, file:./, line:2, col:1 (via ParseError)
[11ty] 3. tag "doesnotexist" not found (via AssertionError)
[11ty] Original error stack trace: AssertionError: tag "doesnotexist" not found
[11ty]     at assert (/home/ruben/dev/tmp/eleventy_repro/node_modules/liquidjs/dist/liquid.node.cjs.js:319:15)
[11ty]     at Parser.parseToken (/home/ruben/dev/tmp/eleventy_repro/node_modules/liquidjs/dist/liquid.node.cjs.js:2284:17)
[11ty]     at Parser.parseTokens (/home/ruben/dev/tmp/eleventy_repro/node_modules/liquidjs/dist/liquid.node.cjs.js:2276:33)
[11ty]     at Parser.parse (/home/ruben/dev/tmp/eleventy_repro/node_modules/liquidjs/dist/liquid.node.cjs.js:2270:21)
[11ty]     at Liquid.parse (/home/ruben/dev/tmp/eleventy_repro/node_modules/liquidjs/dist/liquid.node.cjs.js:3618:28)
[11ty]     at Liquid.compile (/home/ruben/dev/tmp/eleventy_repro/node_modules/@11ty/eleventy/src/Engines/Liquid.js:255:28)
[11ty]     at Markdown.compile (/home/ruben/dev/tmp/eleventy_repro/node_modules/@11ty/eleventy/src/Engines/Markdown.js:68:28)
[11ty]     at TemplateRender.getCompiledTemplate (/home/ruben/dev/tmp/eleventy_repro/node_modules/@11ty/eleventy/src/TemplateRender.js:269:26)
[11ty]     at Template.compile (/home/ruben/dev/tmp/eleventy_repro/node_modules/@11ty/eleventy/src/TemplateContent.js:362:42)
[11ty]     at async Template._render (/home/ruben/dev/tmp/eleventy_repro/node_modules/@11ty/eleventy/src/TemplateContent.js:486:16)
[11ty] Wrote 0 files in 0.06 seconds (v2.0.1)

Eleventy would have been much easier to use if the error message was at the bottom and it gave a link to the exact line in the template. Providing the template path as path/to/file.h:123 like the rest of the paths would also make it clickable in IDEs.

Typing and IDE support #

Another thing I didn’t like is that Eleventy does not support TypeScript. I like TypeScript because it makes my IDE (VSCodium) more helpful. IntelliSense allows me to inspect what methods are available on an object, and linting shows errors when I use something incorrectly.

I tried setting up TypeScript with Eleventy, but there were no types available. Eleventy does come with index.d.ts, but it only documents a single thing and not the entire API.

One thing I did discover is that you can use JSDoc to get IntelliSense to work a little bit. JSDoc does feel a bit like “TypeScript at home” though. It’s very verbose and doesn’t work as well.

const { UserConfig } = require("@11ty/eleventy/src/UserConfig");

 * @param {UserConfig} eleventyConfig
 * @returns {void}
module.exports = function(eleventyConfig) {
    // configure here

Old-style imports: CommonJS #

As you may have seen in the above example, Eleventy uses CommonJS require rather than the more modern ES6 import. This is not great, but will be fixed in Eleventy 3.0.0.

Conclusion #

Whilst I’m not 100% happy with Eleventy, it is more capable than Jekyll and better for my purposes. Many of the issues I have are fixable, and now that I know how to use Eleventy, it should be easier to do things in the future.

rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source contributor, and a graduate from the University of Bristol. I’m a core developer for Minetest, an open source voxel game engine.


Leave comment

Shown publicly next to your comment. Leave blank to show as "Anonymous".
Optional, to notify you if rubenwardy replies. Not shown publicly.
Max 1800 characters. You may use plain text, HTML, or Markdown.