In this article, we will explore Eleventy, a fast and simple static site generator written in Node.js.
We will do so in a very practical way by incrementally building a simple example website from scratch.
During this exercise, we will learn some of the basic concepts to master with Eleventy like templates, layouts, data files and even how to use data from external sources like third-party REST APIs.
All the code from this article is available on GitHub at lmammino/11ty-sample-project.
Bootstrapping the project
Let's dive just right in by creating a new project called 11ty-sample-project
:
mkdir 11ty-sample-project
cd 11ty-sample-project
npm init -y
Installing Eleventy and building our first site
Eleventy can be installed using npm. You can install it globally in your system, but I personally prefer to install it as a development dependency for a given project. This way you can have different projects using different versions of Eleventy if needed.
npm i --save-dev @11ty/eleventy
Now let's create an index file for our Eleventy project:
echo "# My sample Eleventy website" > index.md
At this point, we are ready to run Eleventy:
node_modules/.bin/eleventy --watch --serve
Of course, for simplicity, we can put this script in our package.json
:
// ...
"scripts": {
"start": "eleventy --watch --serve"
},
// ...
So now we can run Eleventy more easily by just running:
npm start
We can now see our site at localhost:8080.
Create a custom config file
Eleventy follows some default conventions, but it is also quite flexible and allows you to change these defaults.
This is convenient if, for whatever reason, you prefer to change the default folder structure or the supported templating languages and much more.
In order to provide our custom configuration to Eleventy we have to create a file called .eleventy.js
in the root folder of our project:
module.exports = function (config) {
return {
dir: {
input: './src',
output: './build'
}
}
}
With this specific configuration, we are redefining the input and output folders for the project. All our source files will be inside src
and the generated files will be in build
.
Now let's actually create the src
folder and move index.md
file into src
. We can also remove the old build folder (_site
):
mkdir src
mv index.md src
rm -rf _site
Finally, make sure to restart Eleventy. Our site has not changed, but now all the generated files will be stored in build
.
You might have noticed that in our configuration file, the function definition receives an argument called config
. This is something that allows for more advanced configuration. We will be discussing an example shortly.
Nunjucks templates with frontmatter
So far we have been using only markdown files to define the content of our static site. Let's now create a Nunjucks template called src/page.njk
with the following content:
<!DOCTYPE html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>A new website</title>
</head>
<body>A sample page here</body>
</html>
Once we save this new file, the build will generate a new page that we can visualize at localhost:8080/page.
Interesting enough, now if we change anything in the source template, the browser will automatically refresh showing us the result of the latest changes.
This is because, once we have a complete HTML structure, Eleventy will inject a BrowserSync script in the page, that will reload the page automatically on every change. Note that this code is injected into the HTML pages only at runtime when receiving the pages through the development web server, it is not actually present in the generated HTML. For this reason, you don't have to do anything special to generate a build ready to be deployed to your production server. In any case, if you only want to generate a build, without spinning up the development web server, you can do so by running eleventy build
.
But let's talk a bit more about templates now.
In Eleventy, markdown (.md
), Nunjucks (.njk
) and many other file types (see the full list) are called templates. These files can be used as a skeleton to generate pages. Eleventy will automatically search for them in our source folder and, by default, it will generate a page for each and every one of them. We will see later how we can use a single template to generate multiple pages.
Templates can have a frontmatter part at the top which can be used to define some additional metadata.
The frontmatter part must be specified at the top of the file and is delimited by ---
as in the following example:
---
name: someone
age: 17
---
Rest of the file
Inside the frontmatter, the metadata is specified using YAML and you can even have nested properties if that makes sense for your specific use case.
In our project, I think it makes sense to use frontmatter to add a title
attribute to our new template:
---
title: A NOT SO NEW website
---
<!DOCTYPE html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{{ title }}</title>
</head>
<body>A sample page here</body>
</html>
Note how data in the frontmatter part can be used straight away in our template using the interpolation syntax of the templating language of choice ({{ variableName }}
in the case of Nunjucks).
Layouts
What if we want all the generated pages (or just some of them) to have the same HTML structure? Also, if we like to use markdown, ideally, we would like the generated HTML to be wrapped in a properly constructed HTML layout that includes a head
and a body
section.
With Eleventy, we can do this by using layouts.
Layouts can be stored inside the _includes
directory in the source folder. This is a special folder. In fact, Eleventy won't be generating pages for markdown, Nunjucks or other templates files available inside this folder. Eleventy will also make sure that all the files placed here will be easily available to the templating language of our choice.
Let's create our first layout in src/_includes/base.njk
:
---
title: My default title
---
<!DOCTYPE html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{{ title }}</title>
</head>
<body>
<main>
{{ content | safe }}
</main>
</body>
</html>
Note that the special variable content
is where the main content (coming from a template) will be placed. We use the filter safe
because we want the HTML coming from the template to be applied verbatim (no escaped text).
Without safe
the HTML coming from a template containing <h1>Hello from Eleventy</h1>
will be rendered as follows:
<!-- ... -->
<body>
<main>
<h1>Hello from Eleventy</h1>
<main>
</body>
Which, of course, is not what we want...
Now we can go back and edit index.md
to use our base template:
---
layout: base
---
# Hello from Eleventy
This is a simple Eleventy demo
Now we can try to reload our index page and check the source code of the page in the browser!
Copying static files
What if we want to add some style to our generated pages? How do we add CSS? Of course, we could easily add inline CSS in our templates and layouts, but what if we want to include an external CSS file?
Let's create src/_includes/style.css
:
html, body {
background-color: #eee;
margin: 0;
}
main {
box-sizing: border-box;
max-width: 1024px;
min-height: 100vh;
padding: 2em;
margin: 0 auto;
background: white;
}
Now how can we make sure that this CSS file gets copied to the build folder?
Let's edit the config .eleventy.js
:
module.exports = function (config) {
config.addPassthroughCopy({ './src/_includes/style.css': 'style.css' })
// ...
}
Invoking the addPassthroughCopy
function is essentially telling Eleventy that, for every build, the given source file will need to be copied (as it is) to the given destination in the build folder.
Check out the build folder, and we will see style.css
there! If it's not there, try restarting the Eleventy build.
We can now update our default layout to reference this stylesheet by adding the following code in the head
block:
<link rel="stylesheet" href="/style.css"/>
This will essentially inform the browser to load the CSS style from our style.css
file when the page is loaded.
You can use the same technique to copy client-side JavaScript files, images, videos or other static assets into your build folder.
Global data files
When building static sites, we generally have some "global" data that we want to be able to reference in our templates and layouts.
Just to deal with a very simple example, I like to keep all the site metadata (author information, copyright information, domain name, google analytics ID, etc.) in a dedicated file.
Let's create a file with some generic site information in ./src/_data/site.js
:
'use strict'
module.exports = {
author: 'Luciano Mammino',
copyrightYear: (new Date()).getFullYear()
}
The folder _data
is another special data folder. Every js
and json
file inside it will be pre-processed and made available using the file name (site
in this case) as the variable name.
Now we can update our base layout and add a footer:
{# ... #}
<main>
{{ content | safe }}
<hr/>
<small>A website by {{ site.author }} - © {{ site.copyrightYear }}</small>
</main>
{# ... #}
The collection API
When building static sites, it is very very common to have content coming from files that need to be somehow grouped into logical categories. For instance, if it is a blog, we will have a collection of blog posts and we can even group them by topic.
Let's try to create a few sample blog posts:
echo -e "---\ntitle: Post 1\nlayout: base\n---\n# post 1\n\nA sample blog post 1" > src/post1.md
echo -e "---\ntitle: Post 2\nlayout: base\n---\n# post 2\n\nA sample blog post 2" > src/post2.md
echo -e "---\ntitle: Post 3\nlayout: base\n---\n# post 3\n\nA sample blog post 3" > src/post3.md
Now let's add the tag "posts" in the frontmatter of every blog post:
---
tags: [posts]
---
Now if we want to display all the posts in another template we can do that by accessing the special variable collections.post
. For instance we can add the following to src/index.md
:
{% for post in collections.posts %}
- [{{ post.data.title }}]({{ post.url }})
{% endfor %}
For every tag in our templates, eleventy will keep a collection named after that tag. We can then access the list of templates in that collection by using collections.<name of the tag>
.
There is also a special collection named collections.all
that contains every single template. This can be used to generate sitemaps or ATOM feeds.
Generate a sitemap for your Eleventy website
Luciano Mammino ใป Sep 16 '20
For every element in a collection, we can access the data in the frontmatter of that template by using the special .data
attribute. In our example, we are doing this to access the title
attribute. There are also special attributes such as url
or date
that we can use to access additional metadata added by Eleventy itself.
Using dynamic content
Now, what if we want to get some data from an external source like a REST API?
That's actually quite easy with Eleventy!
For this tutorial, we can use an amazing FREE API that allows us to access information for all movies produced by Studio Ghibli, which we can find at ghibliapi.herokuapp.com.
With this API we can, for instance, call https://ghibliapi.herokuapp.com/films/
to get the list of all the movies.
This can be a good API for us and we can try to use Eleventy to generate a new page for every single movie.
Since we want to cache the result of this call, to avoid calling it over and over at every build we can use @11ty/eleventy-cache-assets
npm i --save-dev @11ty/eleventy-cache-assets
Now let's create src/_data/movies.js
:
'use strict'
const Cache = require('@11ty/eleventy-cache-assets')
module.exports = async function () {
return Cache('https://ghibliapi.herokuapp.com/films/', { type: 'json' })
}
Now we can access the movies
array in any template or layout.
Creating a page for every movie
Let's create a template called src/movie-page.md
---
layout: base
permalink: /movie/{{ movie.title | slug }}/
pagination:
data: movies
size: 1
alias: movie
eleventyComputed:
title: "{{ movie.title }}"
---
## {{ movie.title }}
- Released in **{{ movie.release_date }}**
- Directed by **{{ movie.director }}**
- Produced by **{{ movie.producer }}**
{{ movie.description }}
[<< See all movies](/movies)
There's a lot to unpack here! Let's start by discussing the pagination
attribute in the frontmatter.
This special attribute tells Eleventy to generate multiple pages starting from this template. How many pages? Well, that depends on the pagination.data
and the pagination.size
attributes.
The pagination.data
attribute tells eleventy what array of data do we want to iterate over, while pagination.size
is used to divide the array into chunks. In this case, by specifying 1
as size, we are essentially telling Eleventy to generate 1 page per every element in the movies
array.
When using the pagination API we can reference the current element (in the case of 1 element per page) by specifying an alias
, which in our case we defined as movie
.
At this point, we can specify the URL of every page using the permalink
attribute. Note how we are interpolating the movie
variable to extract data from the current movie.
If we need to define element-specific frontmatter data, we can do so by using the special eleventyComputed
attribute. In our example, we are doing this to make sure that every generated page will have its own title.
If we want to see how one of the pages looks like we can visit localhost:8080/movie/ponyo/.
Now we can easily create the index page to link all the movies in src/movies.md
:
---
layout: base
title: Studio Ghibli movies
---
# Studio Ghibli movies
{% for movie in movies %}
- [{{ movie.title }}](/movie/{{ movie.title | slug }})
{% endfor %}
Take some time to navigate around and, hopefully, get to know some new movies! ๐
It's a wrap ๐ฏ
And this concludes our Eleventy tutorial!
In this article we learned about the following topics:
- How to Install Eleventy and bootstrap a new project from scratch
- Creating a simple "Hello world" website
- Providing custom configuration
- Templates, frontmatter & Layouts
- Using live reload
- Copying static files
- Custom global data
- The collection API
- Using dynamic data from external sources
- The pagination API
There is a lot more we can do with Eleventy, so make sure to check out the Eleventy official documentation to learn more.
If you found this article interesting consider following me here, on Twitter and check out my personal website/blog for more articles.
Also, if you like Node.js consider checking out my book Node.js Design Patterns.
Thank you! ๐
PS: special thanks to Ben White on Twitter for providing some useful feedback!
Top comments (1)