For two years, my blog ran on Jekyll, one of the oldest and most popular static site generators around. Jekyll is often listed alongside other static site generators like Hugo, Gatsby, Next, Nuxt, and many others that make up the so-called Jamstack.

The jamstack.org website. Various static site generators like Hugo, Next.js, Gatsby, and Jekyll are ranked in a grid view, with information on various stats, like the number of stars on GitHub.
The jamstack.org website ranks popular static site generators.

Jekyll was great and served me really well! It's very much a batteries-included static site generator, which is great for beginners because it means that you can hit the ground running and create a blog quickly and with minimal setup.

But Jekyll isn't without its problems. For one, it runs on Ruby, which I've found to be more painful to install and configure than Node, especially on Windows (even with WSL). It's also not that easy to customize Jekyll and write plugins or custom filters for it since you have to learn Ruby to do so effectively. Jekyll's documentation is also lacking in some areas, especially with regard to how you extend it or write custom plugins and tags.

I pushed Jekyll to its limits as much as I could. But I also felt that it was time for me to move on to something else—not just for the sake of trying something new but so I could enjoy a better developer experience.

So, over the course of several months in early 2021, I toiled away to recreate my website in two popular frameworks: Next.js and Gatsby. I gave up on both when I realized how much they bloated my site and slowed my page load speed. Don't get me wrong—I love React and use it every day at work. But I don't think that it's the right solution for a blog, unless you really need MDX.

My dream framework needed to have these features:

  • Built-in image optimization, with minimal configuration.
  • Support for pagination and category pages (Gatsby did this well).
  • Posts that can be authored in markdown, with front-matter data.
  • Good documentation and active releases.

After months of prototyping and research, I stumbled upon 11ty, a static site generator that's marketed as the Jekyll of JavaScript. I first learned about 11ty from Reddit and various dev bloggers, so I figured I'd give it a shot and see what all the hype is about.

Visiting the official website, I was greeted by a mischievous-looking possum hanging from a red balloon and floating across my screen.

The Eleventy mascot is a possum with brown hide and a beige face. It's hanging from a red balloon, with one paw slightly outstretched.

Okay, I thought. I can work with this.

It was then that I dedicated myself to migrating my site from Jekyll to 11ty, what I hoped would be my last experiment. And overall, I'm very pleased with how it turned out and how easily I was able to port my content over. Most of this site has remained unchanged; some other things have actually improved.

Now that I've gone through this process myself and have experience with various static site generators, I'd like to take some time to look at the advantages and drawbacks of using 11ty for a personal website or blog. Most of this is based on my short-term experience with 11ty, so keep in mind that my views may change in the future as I learn more.

Skip table of contents

Table of Contents

The Good: Why I Like 11ty

11ty Rocks—there's a reason why so many people are using it for their personal websites, blogs, and official landing pages. Even Google uses it for its high-performance blog template. Below are just a handful of reasons why 11ty is such a great framework.

1. It's Highly Configurable

As I mentioned earlier, 11ty runs on JavaScript. All of its configs are written in Node, with the base .eleventy.js file exporting a function like so:

.eleventy.js
module.exports = (eleventyConfg) => {
  // Where the magic happens.
}

Eleventy exposes all of its internal APIs to this module, meaning you can do things like:

... and so much more! I could go on forever about all the cool things you can do in 11ty.

For example, if you want to use SVG icon libraries like Feather Icons on your site, you can install the NPM package, import it into your config, and register a custom shortcode that returns a particular SVG as an inline string:

.eleventy.js
const feather = require('feather-icons');

// You'll need to pass more arguments, but this is the general idea
const iconShortcode = (icon) => feather.icons[icon].toSvg();

module.exports = (eleventyConfig) => {
  eleventyConfig.addLiquidShortcode('icon', iconShortcode);
}

Voila—you can now invoke the shortcode in any valid template language, like markdown or HTML, to render the icon:

{% icon "calendar" %}

This ships zero client-side JavaScript since your packages are used at build time, on the server side, to generate static HTML. If there's a package out there that you have your heart set on, chances are that you can use it to customize 11ty. This is great because web developers are most familiar with JavaScript, and the ecosystem is booming with open-source packages that solve common problems.

By far the coolest thing about 11ty is how easy it is to write custom templating filters. Tired of repeating {{ site.url }} in your markup? Create a custom filter to prepend your site's URL to any URL string that you give it:

.eleventy.js
const site = require('./src/_data/site');

const toAbsoluteUrl = (url) => {
  if (typeof url !== 'string') {
    throw new Error(`${toAbsoluteUrl.name}: expected argument of type string but instead got ${url} (${typeof url})`);
  }
  // Replace trailing slash, e.g., site.com/ => site.com
  const siteUrl = site.url.replace(/\/$/, '');
  // Replace starting slash, e.g., /path/ => path/
  const relativeUrl = url.replace(/^\//, '');

  return `${siteUrl}/${relativeUrl}`;
}

module.exports = (eleventyConfig) => {
  eleventyConfig.addLiquidFilter('toAbsoluteUrl', toAbsoluteUrl);
}

And now you can use it like this anywhere in your code:

<a href="{{ someRelativeUrl | toAbsoluteUrl }}"></a>

This is just one of the many cool things you can do with custom filters. You can even add error boundaries and throw meaningful messages when something goes wrong to make it easier to debug your code.

2. It's Testable

Tooling is everything when it comes to creating a good developer experience. Writing tests for custom filters and plugins in Jekyll is not only poorly documented but also just a lot of work. By comparison, things are much easier with 11ty. Since you're already using JavaScript to write your configs, you can install any testing framework you like (I prefer Jest) and use it to test your code. This is great because it means you can test custom filters, ensuring that they always behave how you want them to and giving you more confidence in your output HTML.

3. It Has Built-in Pagination

With Jekyll, pagination was an afterthought, and you had to use a plugin to generate paginated blog and category pages. It worked well, but it was also the slowest part of my build (which, to be fair, wasn't that slow to begin with—around 3 minutes locally and 1.5 minutes on Netlify).

In 11ty, pagination is built right in—you can paginate All the Things to your heart's content. Eleventy uses the notion of a "tag" to group your content into collections. You can define tags in the front matter of any template file or even with directory-specific data files. So if you have another collection named notes and a source directory like src/_notes, you can stick a JSON file in there to automatically tag everything in it as part of the notes collection:

src/_notes/_notes.json
{
  "tags": ["notes"]
}

And then look it up with pagination in your front matter:

src/notes.html
---
title: Notes
permalink: /notes/
pagination:
  data: collections.notes
  size: 10
  alias: notes
---

{%- for note in notes -%}
  <a href="{{ note.url }}">{{ note.data.title }}</a>
{%- endfor -%}

You can even access the pagination object to create a pagination trail:

<ol class="pagination-trail" aria-label="Page navigation">
  {%- for pageUrl in pagination.hrefs %}
    <li>
      <a
        href="{{ pageUrl }}"
        aria-label="Page {{ forloop.index }}"
        {% if pageUrl == page.url -%}
          aria-current="page"
        {%- endif -%}
      >
        {{- forloop.index -}}
      </a>
    </li>
  {%- endfor -%}
</ol>
{%- endif -%}

Pretty cool! But even cooler is that you can generate collections programmatically, right in your 11ty config. This should sound familiar if you've ever created a Markdown-based blog in Next.js and had to use glob patterns to collect all of your posts from the file system.

This means that instead of manually tagging your content or using directory data files, you can generate a custom collection with JavaScript and give it any name that you want:

.eleventy.js
module.exports = (eleventyConfig) => {
  eleventyConfig.addCollection('posts', (collectionApi) => {
    return collectionApi.getFilteredByGlob('./src/_posts/*.md').reverse();
  });
}

Assuming that all your posts reside in Markdown files in the src/_posts directory, the code above will use Eleventy's collections API to group all files matching the glob pattern into an array of objects, each containing data about your posts via front matter.

At first glance, this may not seem too useful—it's more work than leaning on Eleventy's tagging system. But where it really shines is when you want to create two-level pagination, where you rely on Eleventy's pagination API to automatically generate category pages based on front matter, but you also want each category page to itself be paginated. While this sounds complicated, that article by Jérôme Coupé is an excellent tutorial on how to accomplish this. And it's actually much easier than you'd think!

4. It Supports Dynamic Front Matter

This one piggy-backs right off the previous point about pagination.

In Jekyll, you can't write dynamic templating logic in your front-matter blocks since doing this creates circular references. Sometimes, though, your page title or permalink depends on some dynamic (often paginated) data.

Fortunately, this limitation doesn't exist in 11ty, so you can do magical things like this:

src/_pages/blog.html
---
permalink: "/blog/{% if pagination.pageNumber > 0 %}page/{{ pagination.pageNumber | plus: 1 }}/{% endif %}"
pagination:
  data: collections.posts
  size: 10
  alias: posts
renderData:
  title: "Blog{% if pagination.pageNumber > 0 %} (Page {{ pagination.pageNumber | plus: 1 }}){% endif %}"
---

By default, permalink supports dynamic interpolation of paginated data. In this example, I check to see if I'm on any page except the root blog page. If so, I append /page/#/ to the permalink. If you want to interpolate dynamic data in other front-matter variables, you can do so with Eleventy's special renderData key (or, alternatively, computed data).

5. It Has an Excellent Image Plugin

With my Jekyll blog, I was optimizing images by hand with a Python script that generated low-quality placeholders and WebP variants for every single image in a directory. This worked surprisingly well, and it even allowed me to support animated WebP GIFs without slowing down my page load speed. It also kept my build times low since I was generating images statically.

Unfortunately, all of this came at a price: It was immensely tedious work that often held me back from writing posts because I dreaded the thought of having to optimize images by hand. What I wanted was for image optimization to be a core part of the framework I was using, not something I threw together with scripts and duct tape.

What really convinced me to give Eleventy a shot was an article by Ben Holmes about how you can use the official 11ty image plugin to optimize images in any framework and not just in 11ty itself. I was blown away when I followed along with the tutorial and, in fact, was able to generate any combination of sizes and formats for images in just a few short lines of JavaScript.

Here's some sample code from the official docs showing how you register a custom shortcode for images and generate the required output:

.eleventy.js
const Image = require("@11ty/eleventy-img");

async function imageShortcode(src, alt, sizes) {
  let metadata = await Image(src, {
    widths: [300, 600],
    formats: ["avif", "jpeg"]
  });

  let imageAttributes = {
    alt,
    sizes,
    loading: "lazy",
    decoding: "async",
  };

  return Image.generateHTML(metadata, imageAttributes);
}

module.exports = function(eleventyConfig) {
  eleventyConfig.addLiquidShortcode("image", imageShortcode);
};

You can then use it like this in any Liquid template:

{% image "path/to/my/image" "alt text" "100vw" %}

Let's talk a bit more about the image plugin and why it's so awesome.

It's Really Fast

The Eleventy plugin is powered by the Sharp image processing library, and it's blazing fast—much faster than the comparative Gatsby image plugin. I say this based on personal experience because I've tried to migrate my site to Gatsby, and the images take forever to generate—it's the main reason why so many Gatsby sites have build times on the order of 10+ minutes.

To put this into perspective, my site has around 360 source images as of this writing. Eleventy is currently writing a total of 2450 images to my output directory, some of which are pass-through copies but most of which are generated. You'd think that this would slow down my builds, but they're actually faster in 11ty than they were in Jekyll—around two to three times faster! Fresh local builds take around 90 seconds, while Netlify builds take 2 minutes with a cache (4 without).

Part of what makes the 11ty image plugin so efficient is the fact that it caches images locally once it has generated them. This is great if you have lots of paginated pages with images that get reused, like for article thumbnails. Instead of regenerating the same image several times, 11ty will process it just once, and all subsequent requests will hit the cache.

It Supports Remote Images

With most image plugins in other frameworks, you're limited to static imports or paths, meaning the images you want to transform must be located in your project's source. This isn't a big deal, but it does add a little overhead for retrieving and saving remote images.

Guess what? The 11ty image plugin supports remote URLs and not just static images! In fact, the official tutorial that I linked to earler runs the plugin on a sample image from Unsplash, generating local copies of that image just as if it had existed in your source.

It's Very Flexible

Most image plugins generate fixed markup that you have very little control over, except maybe some customization through props (in React) or static config files. This can be really limiting if you need to roll a custom solution for image optimization.

Fortunately, the Eleventy image plugin doesn't actually care how you intend to use the generated images. All it does is expose two basic APIs: one that returns metadata for all of the generated images, and another that returns a string for the image markup:

// Get the data for all image variants
let metadata = await Image(src, {
  widths: [300, 600],
  formats: ["avif", "jpeg"]
});

// Generate the markup
return Image.generateHTML(metadata);

But instead of relying on Image.generateHTML, you can return custom markup as a string! All the data you need is right there in the metadata returned by Image. This means that you can generate low-quality placeholder images as part of your build process, return markup consisting of placeholder src and srcset attributes, and define data- attributes storing the actual image data. You can then lazily load your images with very few lines of JavaScript. (I'll probably write a tutorial one day on how to do this!)

You can even customize the naming for your images through an optional argument:

const imageMetadata = await Image(fullyQualifiedImagePath, {
  // ... other arguments
  filenameFormat: (id, src, width, format) => {
    return `${name}-${width}.${format}`;
  },
  // ...
});

There's very little that you can't customize in some shape or form. I'm honestly very impressed by how much thought went into making this plugin; it's exactly what I've been searching for.

6. It Has Great Documentation

If you're not sure how to do something in 11ty, chances are that you'll find an example in the official docs in several different templating languages. And if you get stuck, you can ask for help in their very active GitHub community. I actually ran into a problem trying to deploy my site, and I received an answer within a few hours.

7. It Supports Incremental Builds

This is something I initially misunderstood about 11ty, thinking that it was slower than Jekyll. But it turns out that 11ty supports incremental builds just like Jekyll does—all you have to do is supply it the --incremental command-line flag. So if you change one file, 11ty won't rebuild your entire site—it will only write the file that changed. This makes for a great developer experience, especially if you save files frequently like I do.

8. It Has a Debug Mode

To top it all off, if you get stuck at any point during development, you have several options for debugging:

  • Logging variables to the server console with the log filter: {{ var | log }}.
  • Running eleventy in debug mode: DEBUG=Eleventy* npx @11ty/eleventy.

Debug mode is awesome—11ty logs information about every single thing that it does: how it was configured, what directories and files it found, what files it copied, what images it generated, how long any given step took, and so much more. It even includes benchmarking info at the end so you can optimize your build times!

Sample output from Eleventy's debug mode, with color-coded steps and various useful messages logged to the console.

(Also, I like the pretty colors.)

The Bad: What I Don't Like About 11ty

An honest review of Eleventy wouldn't be one if I claimed that the framework isn't without its flaws. Having said that, I still think this is one of the best static site generators around, and you should definitely give it a shot. Some of the points below border on nitpicking—that's just how good 11ty is.

1. It Requires More Configuration

Compared to frameworks like Jekyll, Eleventy is highly configurable and extensible—on par with frameworks like Gatsby that have giant plugin ecosystems. This is great because it means that you can customize nearly every aspect of the framework's internals to meet your needs.

Unfortunately, this can present some friction for beginners since it takes some time for you to understand how everything works and to debug any problems you encounter, especially if you're migrating your site from another framework. You may need to spend some time initially reading the docs, installing various packages that you need, and fiddling with configs until things work.

Of course, this is true for any new tool that you pick up—there's always a learning phase. And once you get up and running with 11ty, you'll be able to extend it more easily than most other frameworks. Alternatively, you could just use one of the many starter templates.

2. Variables Pollute the Global Namespace

If I could change one thing about 11ty, namespacing would be it. This issue bit me quite a few times as I was migrating my site from Jekyll, and I have to admit that I still don't fully understand how the 11ty data cascade works.

In Jekyll, template variables are scoped under their corresponding namespace:

  • page.x for page variables (defined at the page level).
  • layout.x for layout variables (defined at the layout level).
  • include.x for arguments passed into an include file.
  • site.x for site variables.

With 11ty, the namespacing is inconsistent. Sometimes, the title you want for a page is under someItem.data.title; other times, it's leaked into the global scope as just title. Some site variables are scoped under site, like site.url or site.title; others leak into the global scope as plain variables. In include files, all variables are globally scoped, which means you may even get naming clashes between include arguments and other global variables.

This makes for a confusing developer experience, where you have to do a bit of detective work to figure out where your data resides. When migrating my site from Jekyll to 11ty, I had to refer to Paul Lloyd's excellent article for some handy tables that show how Jekyll syntax maps to 11ty syntax.

In the grand scheme of things, this isn't really that big of a deal and is eclipsed by many of 11ty's strengths. However, I do wish there were an option to namespace things more consistently.

3. Mixed Casing and Conventions

This one's a bit of a nitpick, but 11ty uses the camelCase convention of JavaScript to extend Liquid, whereas Liquid itself follows the snake_case convention of Ruby. This can make your templates stylistically inconsistent. That said, you can easily fix this problem by overriding Liquid's existing filters with custom ones, which you may want to do anyway so you have more control over their behavior.

4. No ES Modules Support for the Eleventy Config

If you're using type: "module" in your package.json to automatically treat every JavaScript file as an ES Module, this won't work with Eleventy just yet. You'll need to remove that line from your package.json and instead use the .mjs file extension for any non-Eleventy JavaScript file that should be treated as an ES Module by your bundler (e.g., Webpack). Hopefully, Eleventy will support ESM sometime in the future. Until then, you'll want to stick to using the Common JS syntax of require and module.exports. It's not the end of the world, though.

The... Possum?

We've covered the good. And we've covered the bad. But what about the possum?

The Eleventy mascot is a possum with brown hide and a beige face. It's hanging from a red balloon, with one paw slightly outstretched.

"Why a possum? Why is the possum floating? Why a balloon? Exactly. 42."
An Homage to the James Williamson Possum Balloon

The original Eleventy mascot was designed by developer James Williamson, who passed away in 2019 after a six-year battle with ALS. It was later reinterpreted artistically by designer Phineas X. Jones and now floats through the halls of the 11ty docs.

And I just love everything about it. Those impish eyes. That scrawny little paw just barely outstretched, like it wants you to Hand it over. That thing, your dark soul.

Slave Knight Gael, the final boss of the video game Dark Souls 3, gestures towards the viewer with his hand outstretched. The captions read: Hand it over. That thing, your dark soul. The Eleventy mascot's head replaces Gael's.
There's a Dark Souls meme that nobody asked for. You're welcome, internet.

So, Should You Use 11ty?

Absolutely yes! If you want to spin up a personal website or blog, 11ty is the perfect choice. It's lightweight and much faster than alternatives like Gatsby and Jekyll, and it's backed by an active community of developers. The image plugin alone was enough to convince me to give 11ty a shot, and now I'm very happy with my current setup.

I hope you enjoyed this read! I certainly had fun migrating my site to 11ty and writing about it.

Useful Resources

Below are various tutorials and resources that helped me as I migrated my site from Jekyll to 11ty.

Comments

Post comment

This comment system is powered by the GitHub Issues API. You can learn more about how I built it or post a comment over on GitHub, and it'll show up below once you reload this page.

Loading...