loading...
Cover image for Getting started with Eleventy in 11 minutes

Getting started with Eleventy in 11 minutes

loige profile image Luciano Mammino Updated on ・11 min read

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now let's create an index file for our Eleventy project:

echo "# My sample Eleventy website" > index.md
Enter fullscreen mode Exit fullscreen mode

At this point, we are ready to run Eleventy:

node_modules/.bin/eleventy --watch --serve
Enter fullscreen mode Exit fullscreen mode

Of course, for simplicity, we can put this script in our package.json:

// ...
"scripts": {
  "start": "eleventy --watch --serve"
},
// ...
Enter fullscreen mode Exit fullscreen mode

So now we can run Eleventy more easily by just running:

npm start
Enter fullscreen mode Exit fullscreen mode

We can now see our site at localhost:8080.

My sample Eleventy website

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'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Once we save this new file, the build will generate a new page that we can visualize at localhost:8080/page.

A sample page generated using Nunjucks

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
    &lt;h1&gt;Hello from Eleventy&lt;/h1&gt;
  <main>
</body>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now we can try to reload our index page and check the source code of the page in the browser!

A page generated using a base layout

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;
}
Enter fullscreen mode Exit fullscreen mode

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' })

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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"/>
Enter fullscreen mode Exit fullscreen mode

This will essentially inform the browser to load the CSS style from our style.css file when the page is loaded.

Added some stylesheet to our base layout

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()
}
Enter fullscreen mode Exit fullscreen mode

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 }} - &copy; {{ site.copyrightYear }}</small>
</main>

{# ... #}
Enter fullscreen mode Exit fullscreen mode

The website now has a footer populated by data from a data file

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
Enter fullscreen mode Exit fullscreen mode

Now let's add the tag "posts" in the frontmatter of every blog post:

---
tags: [posts]
---
Enter fullscreen mode Exit fullscreen mode

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 %}
Enter fullscreen mode Exit fullscreen mode

Showing a list of posts in the home page

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.

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.

Using the Ghibli API to get the list of Studio Ghibli 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
Enter fullscreen mode Exit fullscreen mode

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' })
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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/.

THe page generated for the Ponyo movie

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 %}
Enter fullscreen mode Exit fullscreen mode

Index page showing all the movies from Studio Ghibli

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.

Node.js Design Patterns book hold by man

Thank you! 👋

PS: special thanks to Ben White on Twitter for providing some useful feedback!

Discussion

pic
Editor guide