Image by 11ty.dev
I would like to share my opinionated path how I got started using.
Table of Contents
- What is Eleventy?
- Why Eleventy?
- Create a new project
- Adding content
- Running and building
- HTML templates
- Providing data for your site
- Collections
- Filters
- Plugins
- Adding toolchains for CSS and JS
What is Eleventy?
Eleventy is a popular static site generator. It creates a static site from your input files. The input files eleventy looks for are content files, html template files and data files which will be covered in this article.
It supports several data file formats, content file formats and html template engines which you can use together. In this article, I'm using Markdown files together with Nunjucks templates.
Why Eleventy?
There are many static site generators out there and you may wonder what benefits it offers compared to others. The key points which make me really love Eleventy are:
- it is built on node.js
- it does one job and does that well: create markup from content plus templates
- it is completely unopinionated about client-side JS+CSS toolchains: bring your own
- no angular/react/vue knowledge necessary.
- html first approach which makes it great for progressive enhancement.
- easily extendible and combinable with npm packages
- similar to jekyll, but with node.js as a base
- easy to mock data via the data folder.
Create a new project
mkdir my-awesome-site
cd my-awesome-site
git config --global init.defaultBranch main
npm init -y
npm i @11ty/eleventy -D
Initialize your repository
Additionally, run git init
and provide a .gitignore
if you are working with git:
node_modules
.DS_Store
Thumbs.db
# Eleventy output folder
public
Configuration
Configuration is done via a single javascript file named .eleventy.js
. Providing a configuration is optional.
My personal preference is to provide src
as input folder and public
as output folder. Additionally, I use to specify folders that are copied over to the output folder on build. These are also automatically watched by eleventy when starting the development server.
module.exports = (config) => {
// specify folders to be copied to the output folder
config.addPassthroughCopy('./src/js/');
config.addPassthroughCopy('./src/css/');
return {
markdownTemplateEngine: 'njk',
htmlTemplateEngine: 'njk',
dir: {
input: 'src', // default: '.'
output: 'public' // default: '_site'
}
}
};
Adding content
Create a markdown file and name it like this: src/index.md
---
title: "Hello world"
---
# Hello World
Welcome to my awesome {{title}} site!
Markdown and front matter
Markdown files can optionally provide a metadata block, which is marked via three hyphens ---
in the beginning and the end of the block. In this block, you can specify meta data in YAML notation.
From content to paths
For each markdown content file, eleventy creates a folder with an index.html
for nice urls:
index.md --> /
about.md --> /about/
faq.md --> /faq/
blog/hello-world.md --> /blog/hello-world/
Running and building
Finally, we can start adding the start and build tasks to our package.json
file:
{
"scripts": {
"start": "eleventy --serve",
"build": "eleventy"
}
}
-
npm start
-> start Development server -
npm run build
-> build your website -
npx serve public
-> test your build
The development server is using browsersync
which automatically keeps track of changes and updates your DOM. Additionally, if you open the page in multiple browsers, events are kept in sync, which is useful for cross-browser-testing.
HTML templates
To define your HTML structure and layouts, you can use HTML templates. There are several template engines available in Eleventy. In this example, I'm using the Nunjucks template engine.
Other template formats supported are .html
, .liquid
, .hbs
, .ejs
, .haml
, .pug
.
A general approach is to build a base html file and then build
several other structures as you need based upon it.
Add a src/_includes/base.njk
file
<!DOCTYPE html>
<html lang="en">
<head><title>{{ title }}</title></head>
<body>
<main>
{% block main %}
{{ content | safe }}
{% endblock %}
</main>
</body>
</html>
Via the {% block %}{% endblock %}
syntax, you can add several slots into your template which you can use when extending your template.
The content
variable is a reserved variable which contains the content body of the current content file.
The | safe
directive is a builtin filter which tells the template engine that the content you want to insert is safe. This way, HTML tags are not converted to plain text containing HTML entities. This allows using html inside your content.
Using your templates in your content files
In the front matter of your markdown file, specify the layout you want to use:
layout: base
Extending your templates
Next to your base.njk
file, create an article.njk
file:
{% extends "base.njk" %}
{% block main %}
<article class="article">
{{ content | safe }}
</article>
{% endblock %}
Nunjucks also has a section about inheritance in the documentation: https://mozilla.github.io/nunjucks/templating.html#template-inheritance
Includes
You can include partial layouts anywhere in your njk or markdown files:
{% include "header.njk" %}
Providing data for your site
There are several ways to provide data that can be used from inside your templates or content files:
- file specific: in the markdown's front matter
- folder specific: add a json file to a content folder
- globally
_data
directory: globally available -
_data
supports.js
,.yaml
,.json
files
_data
example
Imagine you would like to build a navigation and provide all the url entries from a JSON file:
src/_data/nav.json
[
{"title": "Home", "url": "/"},
{"title": "Blog", "url": "/blog/"}
]
Then, you can create a partial html snippet to include in your main template, eg. src/_includes/nav.njk
<nav>
<ul>
{% for link in nav %}
<li>
<a href="{{ link.url }}">{{ link.title }}</a>
</li>
{% endfor %}
</ul>
</nav>
_data
javascript example
_data/site.js
module.exports = {
name: 'My awesome site',
url: 'https://awesome.site/'
};
Can be used like this in the content:
{{ site.name }}
Having a .js
file instead of a plain json or yaml file brings the flexibility to use node.js environment variables (like, checking if you're in the development or production environment). Also, you can do API fetches from there to pull in a headless CMS, for example.
Collections
You can tag your content with a keyword and then iterate through these via collections.
This is useful for auto-generating table of contents or listing articles that are related to each other
Collections example
In your src folder, add a blog
folder with a bunch of markdown files. Tag them as posts
in your front matter:
tags: posts
Then, in your markdown or include files, you can iterate through these collection via a for loop:
index.md
:
# Blog
<ul>{% for post in collections.posts %}<li>
<a href="{{ post.url }}">
{{ post.date | date('YYYY-MM-DD') }}: {{ post.data.title }}
</a>
</li>{% endfor %}</ul>
Filters
Filters provide a way to further process your content. You can use these filters from inside your content and template files by using the pipe.
Adding a custom filter
In your .eleventy.js
file, you can add several filters you can use inside your file. You can also use third party libraries here. This is an example for a scream filter and a date formatting filter:
const moment = require('moment');
module.exports = (config) => {
config.addFilter('date', (date, format) => moment(date).format(format || 'YYYY-MM-DD'));
config.addFilter('scream', (str) => str.toUpperCase());
// ...additional config
return { ... }
};
Then, you can use this filter in your content and template files like this:
{{ content | scream | safe }}`
{{ page.date | date('YYYY-MM-DD') }}
Processing include files with a filter
If you would like to process an include with a filter, you can use the nunjucks set
directive to store includes into a variable. In my personal site, I've used this technique to minify WebGL shader code on the fly:
{% set vertexShader %}
{% include 'shaders/vertex-shader.vs' %}
{% endset %}
{{ vertexShader | glslminify | safe }}
Built-in filters
- you can use all built-in nunjucks filters
-
safe
– the content is safe to insert, so html specific characters are not converted to html entities (use this to inject html and scripts). -
url
– specify a prefix path (useful for deployment into a subdirectory, eg. on github pages). -
slug
– convert a string to an url-friendly slug (egMy site
tomy-site
) -
get*CollectionItem
– get next or previous item in collection
Plugins and tools
Eleventy provides a rich plugin ecosystem where you can add further magic✨ to your workflow 🙌.
Check out the Eleventy plugins documentation
Adding toolchains for CSS and JS
In the article, we used a pass-through-copy command and used CSS and JS without any bundling or further processing. My favorite approach is to use a CSS preprocessor plus ES module JavaScript files. These are not supported in legacy browsers such as IE11. When using progressive enhancement, JavaScript is not required to read
your content.
In the following, I will demonstrate the approach I used (only using a CSS transpiles) and a complete JS+CSS toolchain using parcel as an alternative approach.
CSS transpiler only
In my personal project, I used the sass together with concurrently, to start two processes concurrently running in my npm start
script.
npm i sass concurrently -D
To build the CSS, I'm running sass src/scss:src/css
which compiles every .scss to CSS:
{
"scripts": {
"start": "concurrently 'npm:watch-css' 'npm:serve-11ty'",
"build-11ty": "eleventy",
"serve-11ty": "eleventy --serve",
"build-css": "sass src/scss/:src/css/",
"watch-css": "sass src/scss/:src/css/ --watch",
"build": "npm run build-css -s && npm run build-11ty -s"
}
}
Or complete Javascript+CSS toolchain.
If you would like to have a complete frontend toolchain taking care of compiling JavaScript and CSS, one way to do is is to use Parcel.
npm i parcel-bundler concurrently -D
echo src/dist >> .gitignore
echo .cache >> .gitignore
For the development mode, I'm also using concurrently
to start eleventy and parcel in parallel:
{
"scripts": {
"start": "concurrently 'npm:watch-bundle' 'npm:serve-11ty'",
"build": "npm run build-bundle -s && npm run build-11ty -s",
"watch-bundle": "parcel watch src/app/index.js -d src/dist",
"build-bundle": "parcel build src/app/index.js -d src/dist",
"serve-11ty": "eleventy --serve",
"build-11ty": "eleventy"
}
}
In src/app
, put an index.js
file. Additionally, put any CSS import of your choice into it:
import './scss/styles.scss';
console.log('Hello world');
Finally, in your eleventy config, change the pass through copy to copy the parcel output into your eleventy output folder:
config.addPassthroughCopy('./src/dist/');
Then, parcel creates an index.js and index.css in the dist folder, which you can use in your html templates like this:
<!-- in your head tag -->
<link rel="stylesheet" href="/dist/index.css">
<!-- right before your closing </body> tag -->
<script src="/dist/index.js"></script>
Example project
Resources
- https://terabaud.github.io/eleventy-talk/ - My slides for a talk about 11ty (useful as a summary of this article)
- https://11ty.dev/ - Official site
- https://11ty.rocks/ - Eleventy rocks
- https://piccalil.li/course/learn-eleventy-from-scratch - in-depth course
Top comments (2)
Thank you Lea for this great post on Eleventy. I found it really helpful, especially the way you set up the Javascript+CSS toolchain which is what I was searching for.
I did have to add config.setUseGitIgnore(false) to the config file to get Eleventy to watch the files in the dist folder, as it seems addPassthroughCopy is not watching files listed in .gitignore. It would only compile my js and scss file once on npm start, but not on subsequent changes. Thought I share this info in case anyone else runs into this as well. Thanks again for writing all this.
This is an awesome read! Thank you for making this 💎