The first thing one notices when looking at the Eleventy documentation is that there isn’t really much there. It’s about as bare bones as it gets: no complicated data models, no strict directory hierarchies. Eleventy takes all files from one folder, compiles them, and generates a folder of output files. Learning how to use it was, in a word, a breeze.
Let’s get started by installing eleventy with npm (or your Node package manager of choice)
npm install --save-dev @11ty/eleventy
Eleventy will by default take any files it recognizes in the current folder, and
output processed HTML files in a new subfolder _site
. We can change this
behaviour on the command line by passing along some flags. The following command
would use npx
to run eleventy on all Markdown (.md
) and Nunjucks (.njk
)
files in the directory src/
, and write the output to dist/
npx @11ty/eleventy --input=src --output=dist --formats=md,njk
Instead of passing these flags on every run, we can set them in Eleventy’s
global config file .eleventy.js
, in the project’s root directory. This is
where we configure all of Eleventy’s behavior.
module.exports = function(eleventyConfig) {
return { dir: { input: "src", output: "dist" }}
A lot of people seem to like Eleventy for its support for all the big templating
engines. This means that migrating a blog from Jekyll (that uses Liquid
templates) to Eleventy is a breeze: you can simply keep your old blog posts
written using Liquid and start writing newer posts in any other templating
syntax you’d prefer. By default, Eleventy figures out which templating engine
to use by looking at the file extension (ie. markdown for .md
files, Nunjucks
for .njk
files, etc…), though the user can fine tune this in the config.
Templates allow you to dynamically create content pages dependent on some extra environment variables. These variables are usually stored in the YAML-formatted front matter of the template:
---
section: blog
title: Configuring a blog with Eleventy
author: Sam Roelants
tags:
- draft
- web
- static site generator
- eleventy
- configuration
description: ""
---
...
Of course, it would be horribly redundant to have to include all the layout
markup (ie, the html) in every single template file corresponding to a single
post. This is where layouts really shine.
Eleventy allows you to create a generic page
with all the layout, and populate that with posts stored in template files.
By default, all these layout files are stored in an _includes/
folder along
with the rest of your site’s sources. Getting a page to use a given layout is
as simple as adding a layout: mylayout.njk
to the front matter.
Unlike many other static site generators, Eleventy places no constraints on your directory tree. You get to organize your project however you see fit, without eleventy getting in the way. For this blog, I have something resembling the following:
./
src/
_includes/
assets/
sass/
images/
posts/
drafts/
dist/
You can define folder-wide front matter variables that apply to all template
files found in that folder. These are stored in a json file carrying the same
name as the folder, eg. posts.json
in the folder posts
. All blog posts
use the same layout-file, have the same url format and include the post
tag
so they can be collected later on. Instead of adding all these tags to the front
matter of every template, I have the following file set up:
// src/posts/posts.json:
{
"permalink": "/blog/{{title | slug }}/index.html",
"layout": "post.njk"
"tags":
- post
}
This is where using a templating language, rather than plain HTML, really starts
to shine. Filters are functions that can process the environment
variables. Some are built in (eg. to escape a string or turn it into a
slug), but users can easily
drop their own filters into the .eleventy.js
config file. If I wanted to use
an escaped version of the title somewhere in the page, I simply use { \{title | safe \}}
. In the last section, I generated a slug to form the
permalink to my blog posts using {\{url | slug \}}
.
If I want to use the date
, that is stored as a JS
Date
object — and hence is utterly useless to us — we could write a filter
readable
so that we could display the date as {\{ date | readable\}}
. We can drop this function in eleventy.js
as follows:
module.exports = function(eleventyConfig) {
const moment = require('moment');
eleventyConfig.addFilter("readable", function(date) {
return moment(date).format("LL");
});
});
};
Shortcodes are the second construction where the power of templating really
becomes apparent. They are basically reusable snippets of code that can be
predefined. I use the same format for some post metadata (the posting date and
tags) in several places throughout this blog. Shortcodes allow me to define the
following snippet in eleventy.js
:
eleventyConfig.addShortcode("metadata",
function(date, tags) {
let tagMarkup = "";
for (tag of tags) {
if (tag != "post") {
tagMarkup +=
'<a href="/thoughts/tags/' +
tag + '" class="tag">#' + tag + "</a>";
}
}
return `
<span class="post-metadata">
Posted on
<span class="posted-date">
<time datetime="${moment(date).toISOString()}">
${moment(date).format("LL")}
</time>
</span>
${tagMarkup}
</span>`;
});
While I’m not entirely sold on the idea of hardcoding part of my markup in some
config file, it does make for easy usage: Anywhere I want to print this
preformatted metadata, I simply drop in a {% metadata date, tags %}
.
The Eleventy core is pretty lean. Because it runs on node.js, all the bells and whistles it lacks are available to us through countless npm packages. This being a fairly modest and simply blog, I feel like I’ve only scratched the surface of what’s possible.
The de-facto standard for syntax highlighting code blocks is Prism.js. Because
we are pre-rendering everything, we don’t have to rely on Prism parsing our
code blocks on the fly on the client side. Instead, we can simply pass it over
all our files during the build step! Little optimizations like this add up and
reduce bundle size and render time. Right now, I am using the
eleventy-plugin-syntaxhighlight
package, but I feel like simply using the
Prism.js node package directly would give me more flexibility.
Adding the plugin is as easy as adding:
const syntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
eleventyConfig.addPlugin(syntaxHighlight);
For years, the standard for rendering math online was the Mathjax library. Despite it being a mammoth of a library, it is used on reputable math sites like Math Overflow and math.stackexchange. The rendering is fairly slow and there’s many issues with content jumping all over the place as the rendering changes the element dimensions. Also, this being a pre-generated site, I wanted something that could easily be run during the build process. The little documentation I found about running MathJax on the serverside made it seem fairly hacky and quite the effort, so I opted against it.
A new player on the field is KaTeX, developed by the guys at Kahn University. It is far more lean and has an excellent server-side rendering API. What’s more, because we have the entire NPM ecosystem at our service, adding KaTeX support was as simply as installing the KaTeX plugin for Markdown-it (the default markdown parser Eleventy uses).
let markdownIt = require("markdown-it");
let markdownItKatex = require("markdown-it-katex");
let options = {
html: true,
breaks: false,
linkify: true
};
let markdownLib = markdownIt(options).use(markdownItKatex);
eleventyConfig.setLibrary("md", markdownLib);
And presto! We have math rendering on our blog! Inline math like or seems to be working fine without messing up the line height too much. Block level math is also pretty straightforward. Nothing holding me back from calculating some complex integrals the Cauchy way:
I tend to keep a directory in my file hierarchy for static assets (ie. css, javascript files and images) that don’t need to be processed by eleventy. Eleventy comes with a built-in function that copies folders to the production directory wholesale. Unfortunately, globs are not yet allowed, so each directory that is to be copied must be listed explicitely.
eleventyConfig.addPassthroughCopy("src/assets/css");
eleventyConfig.addPassthroughCopy("src/assets/js");
eleventyConfig.addPassthroughCopy("src/assets/images");
There’s much more to be said still on what’s possible using Eleventy’s lightweight but flexible architecture, but I’ll keep it at this. Setting everything up was a breeze, despite the sometimes lacking documentation. Eleventy as an SSG has garnered so much attention in the last year that there is no lack of blog posts on how to set up the finer details. Of course, this is only the start, and I can’t wait to see how much more can be customized.