Blog

Maybe not Eleventy

Eleventy is a very popular JAMStack site generator. So why wouldn’t you choose Eleventy for your next project?

We’ve chosen Eleventy for multiple projects for compelling reasons: it supports many template languages (including our favourite: Nunjucks), it has rapid builds and produces real static sites with 0kb client-side JS 😍. But it’s good to be aware of Eleventy’s downsides. To me these are: a lack of dynamic serverless rendering, a risk of data overloading and no proper support for internationalisation. In this article, I’ll explain why these are problematic to me.

No dynamic rendering by design

Eleventy is a static site generator. It’s designed to pre-render a site and explicitly not meant for server-side rendering. If Eleventy works as advertised, why am I making a big deal out of it? My problem is that there is no upgrade path from static to (partial) dynamic rendering. So if later on I want some pages to show personal or time-specific data or to show instant previews as content editors make changes, I’m stuck.

Eleventy’s pre-rendering of static pages is great for a fast time-to-first-byte (TTFB). But traditional server-side rendered applications could achieve the same by adding caching. And new meta frameworks (like Next.js, Nuxt.js, Sapper and Elder.js) offer dynamic server-side (and client-side) rendering as well as enabling pre-rendering some or all static pages. This means I can start with a simple static site and incrementally make it more dynamic using the same framework.

So how does Eleventy’s design make it impossible to add dynamic rendering? Effectively Eleventy is Gulp on steroids. It takes a collection of source files, passes them through its pipeline and writes all output files to the filesystem. The filenames of the output match the urls of your web pages. These can be configure parametrised permalinks for a single page or of an entire collection in one template. For example:

---
pagination:
  data: allArticles
  size: 1
  alias: article
permalink: {{ categorySlug }}/{{ articles.slug }}/index.html
---

The build pipeline merges all data files through the cascade, exposes the data to the template and resolves the permalinks. But the big caveat is that there’s no way to trace a single web page url back to the related template and data file(s). This means I can never dynamically render a single web page, but always need to render my entire site.

Eleventy’s carefully engineered build pipeline makes it impossible for me to mimic the rendering process in a dynamic serverless function which I could use for specific urls. I applaud Eleventy for encouraging plain HTML as a baseline without full-page hydration and client-side routing. But it could learn something when compared to those other frameworks that allow both pre-rendering static pages as well as dynamic rendering.

Curse of the Cascade

Imagine joining an existing Eleventy project and your first task is to fix a small data issue on a web page. Your colleagues already pointed you to the relevant template, but how do you know what data is available in the template and where it comes from?

Eleventy offers many different ways to add data without touching any configuration (zero config). You can add global, directory based and local data files, you can define data in front matter in templates and layouts, and you can use computed data properties. All of these different data sources are merged via Eleventy’s data cascade into a single data object exposed to the template. Here’s an example of where data inside an article.njk template could come from:

.eleventy.js                      lowest prio (7):  data via addGlobalData method
_data/
  site.json                        lower prio (6):  global data file
_layouts/layouts/
  base.njk                          high prio (3):  data in layout front matter
blog/
  blog.json                          low prio (5b): data in parent dir data file
  posts/
    posts.json                       low prio (5a): data in directory data file
    article.njk                   higher prio (2):  data in template front matter
    article.11tydata.graphql      medium prio (4):  data in template data file
    article.11tydata.js          highest prio (1):  eleventyComputed in template js

As powerful as this is, it is also difficult to determine what data is coming from where. Currently your only option is starting in the template and manually working your way up the cascade, while creating a mental model in your head.

Running Eleventy with DEBUG=Eleventy* gives some pointers, but no definitive answers. As far as I know you can’t simply {{ _11ty.data | dump }} all available data. Though for collections you could play with the pagination before hook to access all available data.

For now I’d advise to agree on how to use data with your team and document it for your future selves. My recommendation would be to limit the different data sources, avoid overwriting data properties through the cascade, and scope all data in a file to a property with the same name as the data file so you can more easily find it. I would love it if a future version of Eleventy would allow you to easily view all data available in a template with a trace of where the data originated from. So you could simply dump the data with {{ _11ty.data | dump }} or {{ _11ty.dataCascade | dump }}.

Data Overload

Eleventy allows you to add your own data source and connect directly to a content API. Great! But as we discovered when you do, you risk overloading data and DDoS-ing your own API. We once ran through our DatoCMS API limit in two days while the project wasn’t even live yet. What caused our issue?

This is how our project was set up: we used Eleventy’s custom data file feature to fetch data directly from DatoCMS using *.11tydata.graphql files:

// eleventy.js (simplified)
module.exports = function (config) {
  config.addDataExtension('graphql', (query) => 
    fetch('https://graphql.datocms.com/', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${DATO_API_TOKEN}` },
      body: JSON.stringify({ query })
  }));
}

In stark contrast with the data cascade, our extension enables us to fetch data from our CMS in ultra readable WYSIWYG GraphQL queries like this one:

# src/article.11tydata.graphql (simplified)
query Articles {
  allArticles {
    title
    slug
    image { alt, url, width, height }
    etc
  }
}

We used this in combination with pagination:

---
pagination:
  data: allArticles
  size: 1
  alias: article
permalink: blog/{{ articles.slug }}/index.html
---

We expected Eleventy to execute the query and iterate over its contents to render each page. But in reality, it executes the query for every single page in the pagination. So when editing a template and saving it regularly, we fire hundreds of requests to our content API, quickly reaching our API limit (and needlessly slowing our builds).

Since Eleventy calls our custom extension with only the data file contents and not Eleventy’s pagination object (with pageNumber etc), I don’t see any use of this rapid fire. We solved our issue by adding a custom caching layer to our extension (Eleventy’s cache plugin doesn’t work in this situation). For the future, I would love it if Eleventy adds configurable caching and exposes the pagination object to the custom data extensions.

¿Setenta?

What’s Eleventy in Spanish? Setenta? I don’t know. More importantly, Eleventy does not have native support for internationalisation (i18n). A typical i18n implementation features at least some of the following: locale-specific routing, loading localised content, pluralisation of interface strings, text direction, meta attributes and more. These are non-trivial to add to a project, but things we often need working on international and multi-language projects.

We are not the first to run into this issue. But current suggestions and some plugins don’t scale (aren’t DRY) as complexity grows with every locale added with structures like:

src/
  en/
    en.json
    index.html
    blog.html
    contact.html
  nl/
    nl.json
    index.html
    blog.html
    contact.html
  ../

The best option right now might be to use a community plugin based on gettext and vote on this Eleventy i18n issue. I hope Eleventy or the community (yes, that includes us) will develop an i18n setup for Eleventy as feature-complete as Nuxt I18n.

Conclusion

My overall verdict is that Eleventy is very suitable for some projects, especially real static websites, but beware of its caveats. You can work around the data cascade, data overloading and add some form of i18n. But you can never upgrade your static Eleventy site with dynamic (server-side) rendering.

← All blog posts