Turn Jekyll up to Eleventy
Sometimes it pays not to over complicate things. While many of the sites we use on a daily basis require relational databases to manage their content and dynamic pages to respond to user input, for smaller, simpler sites, serving pre-rendered static HTML is usually a much cheaper — and more secure — option.
The JAMstack (JavaScript, reusable APIs, and prebuilt Markup) is a popular marketing term for this way of building websites, but in some ways it’s a return to how things were in the early days of the web, before developers started tinkering with CGI scripts or Personal HomePage. Indeed, my website has always served pre-rendered HTML; first with the aid of Movable Type and more recently using Jekyll, which Anna wrote about in 2013.
By combining three approachable languages — Markdown for content, YAML for data and Liquid for templating — the ergonomics of Jekyll found broad appeal, influencing the design of the many static site generators that followed. But Jekyll is not without its faults. Aside from notoriously slow build times, it’s also built using Ruby. While this is an elegant programming language, it is yet another ecosystem to understand and manage, and often alongside one we already use: JavaScript. For all my time using Jekyll, I would think to myself “this, but in Node”. Thankfully, one of Santa’s elves (Zach Leatherman) granted my Atwoodian wish and placed such a static site generator under my tree.
Introducing Eleventy
Eleventy is a more flexible alternative Jekyll. Besides being written in Node, it’s less strict about how to organise files and, in addition to Liquid, supports other templating languages like EJS, Pug, Handlebars and Nunjucks. Best of all, its build times are significantly faster (with future optimisations promising further gains).
As content is saved using the familiar combination of YAML front matter and Markdown, transitioning from Jekyll to Eleventy may seem like a reasonable idea. Yet as I’ve discovered, there are a few gotchas. If you’ve been considering making the switch, here are a few tips and tricks to help you on your way1.
Note: Throughout this article, I’ll be converting Matt Cone’s Markdown Guide site as an example. If you want to follow along, start by cloning the git repository, and then change into the project directory:
git clone https://github.com/mattcone/markdown-guide.git
cd markdown-guide
Before you start
If you’ve used tools like Grunt, Gulp or Webpack, you’ll be familiar with Node.js but, if you’ve been exclusively using Jekyll to compile your assets as well as generate your HTML, now’s the time to install Node.js and set up your project to work with its package manager, NPM:
- Install Node.js:
- Mac: If you haven’t already, I recommend installing Homebrew, a package manager for the Mac. Then in the Terminal type
brew install node
. - Windows: Download the Windows installer from the Node.js website and follow the instructions.
- Mac: If you haven’t already, I recommend installing Homebrew, a package manager for the Mac. Then in the Terminal type
- Initiate NPM: Ensure you are in the directory of your project and then type
npm init
. This command will ask you a few questions before creating a file calledpackage.json
. Like RubyGems’sGemfile
, this file contains a list of your project’s third-party dependencies.
If you’re managing your site with Git, make sure to add node_modules
to your .gitignore
file too. Unlike RubyGems, NPM stores its dependencies alongside your project files. This folder can get quite large, and as it contains binaries compiled to work with the host computer, it shouldn’t be version controlled. Eleventy will also honour the contents of this file, meaning anything you want Git to ignore, Eleventy will ignore too.
Installing Eleventy
With Node.js installed and your project setup to work with NPM, we can now install Eleventy as a dependency:
npm install --save-dev @11ty/eleventy
If you open package.json
you should see the following:
…
"devDependencies": {
"@11ty/eleventy": "^0.6.0"
}
…
We can now run Eleventy from the command line using NPM’s npx
command. For example, to covert the README.md
file to HTML, we can run the following:
npx eleventy --input=README.md --formats=md
This command will generate a rendered HTML file at _site/README/index.html
. Like Jekyll, Eleventy shares the same default name for its output directory (_site
), a pattern we will see repeatedly during the transition.
Configuration
Whereas Jekyll uses the declarative YAML syntax for its configuration file, Eleventy uses JavaScript. This allows its options to be scripted, enabling some powerful possibilities as we’ll see later on.
We’ll start by creating our configuration file (.eleventy.js
), copying the relevant settings in _config.yml
over to their equivalent options:
module.exports = function(eleventyConfig) {
return {
dir: {
input: "./", // Equivalent to Jekyll's source property
output: "./_site" // Equivalent to Jekyll's destination property
}
};
};
A few other things to bear in mind:
-
Whereas Jekyll allows you to list folders and files to ignore under its
exclude
property, Eleventy looks for these values inside a file called.eleventyignore
(in addition to.gitignore
). - By default, Eleventy uses markdown-it to parse Markdown. If your content uses advanced syntax features (such as abbreviations, definition lists and footnotes), you’ll need to pass Eleventy an instance of this (or another) Markdown library configured with the relevant options and plugins.
Layouts
One area Eleventy currently lacks flexibility is the location of layouts, which must reside within the _includes
directory (see this issue on GitHub).
Wanting to keep our layouts together, we’ll move them from _layouts
to _includes/layouts
, and then update references to incorporate the layouts
sub-folder. We could update the layout:
frontmatter property in each of our content files, but another option is to create aliases in Eleventy’s config:
module.exports = function(eleventyConfig) {
// Aliases are in relation to the _includes folder
eleventyConfig.addLayoutAlias('about', 'layouts/about.html');
eleventyConfig.addLayoutAlias('book', 'layouts/book.html');
eleventyConfig.addLayoutAlias('default', 'layouts/default.html');
return {
dir: {
input: "./",
output: "./_site"
}
};
}
Determining which template language to use
Eleventy will transform Markdown (.md
) files using Liquid by default, but we’ll need to tell Eleventy how to process other files that are using Liquid templates. There are a few ways to achieve this, but the easiest is to use file extensions. In our case, we have some files in our api
folder that we want to process with Liquid and output as JSON. By appending the .liquid
file extension (i.e. basic-syntax.json
becomes basic-syntax.json.liquid
), Eleventy will know what to do.
Variables
On the surface, Jekyll and Eleventy appear broadly similar, but as each models its content and data a little differently, some template variables will need updating.
Site variables
Alongside build settings, Jekyll let’s you store common values in its configuration file which can be accessed in our templates via the site.*
namespace. For example, in our Markdown Guide, we have the following values:
title: "Markdown Guide"
url: https://www.markdownguide.org
baseurl: ""
repo: http://github.com/mattcone/markdown-guide
comments: false
author:
name: "Matt Cone"
og_locale: "en_US"
Eleventy’s configuration uses JavaScript which is not suited to storing values like this. However, like Jekyll, we can use data files to store common values. If we add our site-wide values to a JSON file inside a folder called _data
and name this file site.json
, we can keep the site.*
namespace and leave our variables unchanged.
{
"title": "Markdown Guide",
"url": "https://www.markdownguide.org",
"baseurl": "",
"repo": "http://github.com/mattcone/markdown-guide",
"comments": false,
"author": {
"name": "Matt Cone"
},
"og_locale": "en_US"
}
Page variables
The table below shows a mapping of common page variables. As a rule, frontmatter properties are accessed directly, whereas derived metadata values (things like URLs, dates etc.) get prefixed with the page.*
namespace:
Jekyll | Eleventy |
---|---|
page.url |
page.url |
page.date |
page.date |
page.path |
page.inputPath |
page.id |
page.outputPath |
page.name |
page.fileSlug |
page.content |
content |
page.title |
title |
page.foobar |
foobar |
When iterating through pages, frontmatter values are available via the data
object while content is available via templateContent
:
Jekyll | Eleventy |
---|---|
item.url |
item.url |
item.date |
item.date |
item.path |
item.inputPath |
item.name |
item.fileSlug |
item.id |
item.outputPath |
item.content |
item.templateContent |
item.title |
item.data.title |
item.foobar |
item.data.foobar |
Ideally the discrepancy between page and item variables will change in a future version (see this GitHub issue), making it easier to understand the way Eleventy structures its data.
Pagination variables
Whereas Jekyll’s pagination feature is limited to paginating posts on one page, Eleventy allows you to paginate any collection of documents or data. Given this disparity, the changes to pagination are more significant, but this table shows a mapping of equivalent variables:
Jekyll | Eleventy |
---|---|
paginator.page |
pagination.pageNumber |
paginator.per_page |
pagination.size |
paginator.posts |
pagination.items |
paginator.previous_page_path |
pagination.previousPageHref |
paginator.next_page_path |
pagination.nextPageHref |
Filters
Although Jekyll uses Liquid, it provides a set of filters that are not part of the core Liquid library. There are quite a few — more than can be covered by this article — but you can replicate them by using Eleventy’s addFilter
configuration option. Let’s convert two used by our Markdown Guide: jsonify
and where
.
The jsonify
filter outputs an object or string as valid JSON. As JavaScript provides a native JSON method, we can use this in our replacement filter. addFilter
takes two arguments; the first is the name of the filter and the second is the function to which we will pass the content we want to transform:
// {{ variable | jsonify }}
eleventyConfig.addFilter('jsonify', function (variable) {
return JSON.stringify(variable);
});
Jekyll’s where
filter is a little more complicated in that it takes two additional arguments: the key to look for, and the value it should match:
{{ site.members | where: "graduation_year","2014" }}
To account for this, instead of passing one value to the second argument of addFilter
, we can instead pass three: the array
we want to examine, the key
we want to look for and the value
it should match:
// {{ array | where: key,value }}
eleventyConfig.addFilter('where', function (array, key, value) {
return array.filter(item => {
const keys = key.split('.');
const reducedKey = keys.reduce((object, key) => {
return object[key];
}, item);
return (reducedKey === value ? item : false);
});
});
There’s quite a bit going on within this filter, but I’ll try to explain. Essentially we’re examining each item
in our array
, reducing key
(passed as a string using dot notation) so that it can be parsed correctly (as an object reference) before comparing its value to value
. If it matches, item
remains in the returned array, else it’s removed. Phew!
Includes
As with filters, Jekyll provides a set of tags that aren’t strictly part of Liquid either. This includes one of the most useful, the include
tag. LiquidJS, the library Eleventy uses, does provide an include
tag, but one using the slightly different syntax defined by Shopify. If you’re not passing variables to your includes, everything should work without modification. Otherwise, note that whereas with Jekyll you would do this:
<!-- page.html -->
{% include include.html value="key" %}
<!-- include.html -->
{{ include.value }}
in Eleventy, you would do this:
<!-- page.html -->
{% include "include.html", value: "key" %}
<!-- include.html -->
{{ value }}
A downside of Shopify’s syntax is that variable assignments are no longer scoped to the include and can therefore leak; keep this in mind when converting your templates as you may need to make further adjustments.
Tweaking Liquid
You may have noticed in the above example that LiquidJS expects the names of included files to be quoted (else it treats them as variables). We could update our templates to add quotes around file names (the recommended approach), but we could also disable this behaviour by setting LiquidJS’s dynamicPartials
option to false
. Additionally, Eleventy doesn’t support the include_relative
tag, meaning you can’t include files relative to the current document. However, LiquidJS does let us define multiple paths to look for included files via its root
option.
Thankfully, Eleventy allows us to pass options to LiquidJS:
eleventyConfig.setLiquidOptions({
dynamicPartials: false,
root: [
'_includes',
'.'
]
});
Collections
Jekyll’s collections feature lets authors create arbitrary collections of documents beyond pages and posts. Eleventy provides a similar feature, but in a far more powerful way.
Collections in Jekyll
In Jekyll, creating collections requires you to add the name of your collections to _config.yml
and create corresponding folders in your project. Our Markdown Guide has two collections:
collections:
- basic-syntax
- extended-syntax
These correspond to the folders _basic-syntax
and _extended-syntax
whose content we can iterate over like so:
{% for syntax in site.extended-syntax %}
{{ syntax.title }}
{% endfor %}
Collections in Eleventy
There are two ways you can set up collections in 11ty. The first, and most straightforward, is to use the tag
property in content files:
---
title: Strikethrough
syntax-id: strikethrough
syntax-summary: "~~The world is flat.~~"
tag: extended-syntax
---
We can then iterate over tagged content like this:
{% for syntax in collections.extended-syntax %}
{{ syntax.data.title }}
{% endfor %}
Eleventy also allows us to configure collections programmatically. For example, instead of using tags, we can search for files using a glob pattern (a way of specifying a set of filenames to search for using wildcard characters):
eleventyConfig.addCollection('basic-syntax', collection => {
return collection.getFilteredByGlob('_basic-syntax/*.md');
});
eleventyConfig.addCollection('extended-syntax', collection => {
return collection.getFilteredByGlob('_extended-syntax/*.md');
});
We can extend this further. For example, say we wanted to sort a collection by the display_order
property in our document’s frontmatter. We could take the results of collection.getFilteredByGlob
and then use JavaScript’s sort
method to sort the result:
eleventyConfig.addCollection('example', collection => {
return collection.getFilteredByGlob('_examples/*.md').sort((a, b) => {
return a.data.display_order - b.data.display_order;
});
});
Hopefully, this gives you just a hint of what’s possible using this approach.
Using directory data to manage defaults
By default, Eleventy will maintain the structure of your content files when generating your site. In our case, that means /_basic-syntax/lists.md
is generated as /_basic-syntax/lists/index.html
. Like Jekyll, we can change where files are saved using the permalink
property. For example, if we want the URL for this page to be /basic-syntax/lists.html
we can add the following:
---
title: Lists
syntax-id: lists
api: "no"
permalink: /basic-syntax/lists.html
---
Again, this is probably not something we want to manage on a file-by-file basis but again, Eleventy has features that can help: directory data and permalink variables.
For example, to achieve the above for all content stored in the _basic-syntax
folder, we can create a JSON file that shares the name of that folder and sits inside it, i.e. _basic-syntax/_basic-syntax.json
and set our default values. For permalinks, we can use Liquid templating to construct our desired path:
{
"layout": "syntax",
"tag": "basic-syntax",
"permalink": "basic-syntax/{{ title | slug }}.html"
}
However, Markdown Guide doesn’t publish syntax examples at individual permanent URLs, it merely uses content files to store data. So let’s change things around a little. No longer tied to Jekyll’s rules about where collection folders should be saved and how they should be labelled, we’ll move them into a folder called _content
:
markdown-guide
└── _content
├── basic-syntax
├── extended-syntax
├── getting-started
└── _content.json
We will also add a directory data file (_content.json
) inside this folder. As directory data is applied recursively, setting permalink
to false
will mean all content in this folder and its children will no longer be published:
{
"permalink": false
}
Static files
Eleventy only transforms files whose template language it’s familiar with. But often we may have static assets that don’t need converting, but do need copying to the destination directory. For this, we can use pass-through file copy. In our configuration file, we tell Eleventy what folders/files to copy with the addPassthroughCopy
option. Then in the return statement, we enable this feature by setting passthroughFileCopy
to true
:
module.exports = function(eleventyConfig) {
…
// Copy the `assets` directory to the compiled site folder
eleventyConfig.addPassthroughCopy('assets');
return {
dir: {
input: "./",
output: "./_site"
},
passthroughFileCopy: true
};
}
Final considerations
Assets
Unlike Jekyll, Eleventy provides no support for asset compilation or bundling scripts — we have plenty of choices in that department already. If you’ve been using Jekyll to compile Sass files into CSS, or CoffeeScript into Javascript, you will need to research alternative options, options which are beyond the scope of this article, sadly.
Publishing to GitHub Pages
One of the benefits of Jekyll is its deep integration with GitHub Pages. To publish an Eleventy generated site — or any site not built with Jekyll — to GitHub Pages can be quite involved, but typically involves copying the generated site to the gh-pages
branch or including that branch as a submodule. Alternatively, you could use a continuous integration service like Travis or CircleCI and push the generated site to your web server. It’s enough to make your head spin! Perhaps for this reason, a number of specialised static site hosts have emerged such as Netlify and Google Firebase. But remember; you can publish a static site almost anywhere!
Going one louder
If you’ve been considering making the switch, I hope this brief overview has been helpful. But it also serves as a reminder why it can be prudent to avoid jumping aboard bandwagons.
While it’s fun to try new software and emerging technologies, doing so can require a lot of work and compromise. For all of Eleventy’s appeal, it’s only a year old so has little in the way of an ecosystem of plugins or themes. It also only has one maintainer. Jekyll on the other hand is a mature project with a large community of maintainers and contributors supporting it.
I moved my site to Eleventy because the slowness and inflexibility of Jekyll was preventing me from doing the things I wanted to do. But I also had time to invest in the transition. After reading this guide, and considering the specific requirements of your project, you may decide to stick with Jekyll, especially if the output will essentially stay the same. And that’s perfectly fine!
But these go to 11.
-
Information provided is correct as of Eleventy v0.6.0 and Jekyll v3.8.5 ↩