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.
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.
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 = collections.post | 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 Webmention.io 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: ""
tags:
- 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.data.title
,
item.url
, or item.page.url
.
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 ./file.md (via TemplateContentRenderError)
[11ty] 2. tag "doesnotexist" not found, file:./file.md, line:2, col:1 (via ParseError)
[11ty] 3. tag "doesnotexist" not found (via AssertionError)
[11ty]
[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.
Comments