Building an Eleventy Boilerplate, Part 1

Posted on
Approx. reading time: 11 min (3166 words)

Eleventy is a static site generator with a fantastic ability to use templates and data files of multiple formats. In this series of posts, I will build out a new boilerplate that will make development easier. Most tutorials will show just the primary usage of Eleventy and not elaborate on more advanced scenarios. This series will help demystify some of the more advanced topics you will need to know when using Eleventy.

In this series, I am assuming that you are familiar with both Git and Node/NPM. If you are not, then this series may be a little bit difficult to follow. If you want to check out the boilder plate in it’s current state, then visit Eleventy Core on Gitlab.

Starting the new boilerplate

I use Gitlab primarily, and I am a bit lazy. Generally, I will create a new repository on Gitlab with a readme and then clone the repository locally. The readme is generally just an empty file with the project name as the header. I then added an MIT License file because I am releasing this code to the public and needing a valid license for people who want to use it. Once we clone the repository locally, we are ready for the next steps.

The next step is to initialize an npm project where the project directory. Node will allow us to use many tools in the build and speed up development times. Do a commit, and we have finished the first part of this tutorial.

Installing Initial Dependencies

The first thing that we need to do is to install Eleventy. I install it through a development dependency because it keeps my command line clean of commands that I don’t always use.

npm install @11ty/eleventy --save-dev

Once installed, I then need another library named dotenv. Dotenv allows me to fake environment variables locally so that I don’t need to make any code changes to switch between Development, Test, or Production.

npm install dotenv --save-dev

Ignoring dependency files

The next thing that I will need to do is create two (2) new files. One is .gitignore and the second .eleventyignore. These files are necessary to control which files get processed during the build. Both files should have a single line added:

node_modules/

After we edit the file, we can make another commit. Small quick commits like this will make your life more comfortable in the future; I will keep reminding you when you should do a commit. But generally, a commit is made after we add a single feature.

Eleventy Configuration

Next, we need to start configuring eleventy. Eleventy uses a dotfile for its configuration, and it is named .eleventy.js. There is a discussion about changing this, but nothing has been made certain. First, we need to enable dotenv to read in our environment variables the second we start the build. Later we will set the environment variable for the build, but we just have to know that we will store it in the NODE_ENV variable. First, we need to check if the current environment is the development environment, and if it is, we will then load the dotenv configuration. The following code goes at the top of the .eleventy.js file:

if (process.env.NODE_ENV === 'development') {
    require("dotenv").config();
}

Now the development environment will be just like the test and production environment. We will have far less code to maintain. Next up is to configure the Eleventy static site generator. We do the configuration by exporting a module. The module can return either an object with the configuration keys set or return a function that will return an object with the configuration keys set.

Setup the configuration

First, we need to set up the export. In our case, we will be using a function, as that will allow us to set some more advanced options. let’s start with this:

module.exports = function (eleventyConfig) {

}

You will notice that the configuration function takes a single argument that we named eleventyConfig. This name is just a convention; you could change this variable’s name, and nothing else would change. That argument is how we can interact with the Eleventy system and set some more advanced options. Inside the function, we are going to use the argument to set something called Deep Data Merge. Deep Data Merge allows Eleventy to make deep copies of objects and make the whole system work as you would expect. This option will most likely become the default option in the future.

module.exports = function (eleventyConfig) {
    eleventyConfig.deepDataMerge(true);
}

Return the configuration object

Great! Now we have objects that can correctly merge. Next, we will need to provide a proper configuration for Eleventy. I like to be reasonably lengthy here and set even the default values for some of the keys. This practice allows me to look at what the configuration is without looking up the documentation all the time. The configuration object is below; add it to the end of the configuration function we added earlier.

return {
    dir: {
        input: "_src",
        output: "_site",
        includes: "_includes",
        data: "_data"
    },
    templateFormats: ["html", "njk", "11ty.js"]
};

Most of this is pretty self-explanatory, but I will give a quick overview. The ‘dir’ object defines the directories to the root directory. The templateFormats key tells Eleventy which template engines the site will be using. In our case, we will be using HTML, nunjucks, and JavaScript templates. Depending on the project, I may also add markdown, but usually, I can do without it. The directories are pretty self-explanatory:

The source code for the site will go into the _src folder.
The built site will go into the _site folder.
Our template parts will go into the _includes folder.
The global data files will go into the _data folder.

Ignoring the artifacts

Take the time to create the _src folder and the _data folder. We will be using the directories in just a few minutes. After that, we will need to exclude a few more of these directories from the repository. In the .gitignore file, add the following lines:

_site/
.env

These entries will keep the built site and the environment variables for dev (which we will set soon) out of the repository. These are best practices and will make things easier later on. In the .eleventyignore file, add the following line:

_site/

This entry prevents eleventy from trying to build previously generated sites. We are doing this as a precaution only; under regular operation, Eleventy shouldn’t try to do this. Now that we have finished the first round of configuration, we can now do another commit.

Setup the data files

Eleventy has a fantastic system for data files that we will be leveraging extensively. In my opinion, the data file system is what sets Eleventy apart from all other static site generators. Hopefully, this small tutorial will show-off the real power of the data system.

Creating our first data file

Now we are going to start seeing what makes Eleventy so great. The first thing is to set up our first environment variable. This variable will give us the hostname that we have selected. These variables will be different in all three environments, making a great candidate for an environment variable! The way we do that is to create a new file named .env. Inside this file, we will add the following line:

BASE_URL=http://localhost:8888/

This line will create a new environment variable named BASE_URL and assign it the value http://localhost:8888/. This concept is vital as this will become the basis for all canonical URLs and links in the coming steps. Although environment variables aren’t officially part of the Eleventy data system, they play an essential role. We will be revisiting them throughout the series, and I hope that we can all get comfortable with them.

Creating a real Eleventy data file

I like to be as organized as possible, and that includes my code. Luckily Eleventy has a few features that will help with this compulsive behaviour. First, we need to go over a few things. Eleventy treats its data like one large JavaScript object, which can be a real pain until you get used to working with it. To start, we will create a new file within the _data folder called config and two (2) new files named site.js and meta.js. Because Eleventy treats its global data folder as a single object, it will look like the following:

{
    config: {
        site: {},
        meta: {}
    }
}

That’s pretty neat! Once we internalize this, we can begin to create great data objects. We are separating the configuration portion off because we will eventually have much more than just configuration values in the data object.

So what should we store in each of these files? The answer is pretty much anything you want. I will show you how I store things, but you are free to use these as you wish. Just experiment with the way you store your data and see what works best for you.

Configuring the metadata file

The first thing is to go ahead and start adding some configuration. These are just JavaScript files that need to return an object ultimately. The way I like to do this is to simply pass the object back using a module.exports statement. The expression allows me to precompute certain variables or require specific files and then just set the proper key in the return object. One of the first orders of business is to set up some of the metadata. I like to configure as much stuff as possible, even SEO, so the meta-object will have many configurations that have to do with ancillary to the page data. Here is what I started mine out with:

module.exports = {
    environment: process.env.NODE_ENV,
    page: {
        title: {
            separator: "-",
            length: 70,
        },
        description: {
            length: 200
        },
        keywords: {
            count: 5
        }
    }
}

As you can see, one of the first things I do is set an environment variable to the value of the NODE_ENV environment variable. This variable will make it easier, later on, to access this value for configuration. Next, I configure a bunch of stuff that is specific to a page. Things like the separator used in the page title tags and the overall length of the title. The optimal size of our page description and even how many keywords appear in the keywords meta tag. Yes, I still use the keywords meta tag, it can be helpful in certain situations, and maybe someday the search engines might start using it again. But mostly, it allows me to keep track of which keywords I am targeting on an individual page and lets me easily send them to my analytics provider.

Configuring the site data file

Next, we will be configuring the site data file. This file will hold all of the data that we will need to configure the site. Things like the official site title, the domain, and other pertinent information. In our case, we will be setting only two keys. These are the name and the base URL. The code below shows how we can organize the objects:

module.exports = {
    name: "Eleventy Core",
    url: {
        base: process.env.BASE_URL
    }
}

Again, we see the use of an environment variable. We will need to ensure that we set it in each environment. We could default the variable, but I try to avoid that as I would prefer the build to fail than succeed with broken data. In this case, the BASE_URL variable fills in a nested data variable. We are also setting the name key to the name of the website. We don’t need an environment variable for that because it won’t change between environments. We use a variable because it will help with consistency and branding.

Eleventy’s secret power - Computed Data

Eleventy allows us to use a unique key in our objects named eleventyComputed. This key will enable us to provide dynamic implementations for our configurations. Instead of always using static entries as we have been until now, we can call and execute JavaScript for our data objects. Now because Eleventy treats the data folder like an object, we just need to add a new file named eleventyComputed.js to the _data folder. This file automatically gives us the ability to create dynamic properties. There is a catch though, we don’t have access to the other global data files this way, so what should we do? We simply require them at the top of the script.

const site = require("./config/site");
const meta = require("./config/meta");
const url = require("url");

As you can see, we have included the other two global data files and also added one node module. The next thing we want to do is set up the module for export:

module.exports = {

}

It’s that simple; now, we can go ahead and start defining properties. If a property is a function, we also get an argument passed in representing the objects local state.

Our first computed property

The first property I want to create is the canonicalUrl property. This property will be fundamental to start and should hold up fine for most simple sites. We use the property to generate Sitemaps and other things where the absolute URL is needed. It will also guarantee a standard URL format that our site will use. We need to add the following code to the module.exports object.

    cannonicalUrl: (data) => {
        return new URL(data.page.url, site.url.base).toString();
    },

Notice that I am using an anonymous object and that I am passing in a data parameter. This parameter will hold the frontmatter data that is present on the object currently being processed. In this case, I am accessing the URL from the page data.

The other properties

Next, I am going to configure the property that produces the Page Title. Let’s face it, most of our pages will be using a standard template, but some, such as landing pages, will need a different type of title. So we will code the most common types and override for the other types.

    pageTitle: (data) => {
        return `${data.title} ${meta.page.title.seperator} ${site.name}`
    },

Notice that I am using the separator meta information and its name from the configuration to fill in the property. This function is vital to note, as the template should be as flexible as possible out of the box.

Our next job is to ensure we set up the description nicely. Right now, we will just pass-through the value, but later we will be adding some SEO logic.

    pageDescription: (data) => {
        return data.description;
    },

Lastly, for this demo, I will be adding a set of keywords for the page. For SEO, keywords are not used at all but having them allows me to keep track of the keywords that I am targeting and later, I will be able to do some custom analytics. To prevent keyword stuffing, though, we are going only to take the first several comma-seperated terms. This number is configurable, but I also added a fallback of five.

    pageKeywords: (data) => {
        // meta.page.keywords.count
        return data.keywords.split(',').slice(0, (meta.page.keywords.count || 5)).map((item) => item.trim()).join(',');
    }

Refining the page description

This would be a good time to make a commit. Next we will make the pageDescrpition better. Traditional SEO practices state that the description should be less then 200 characters. This is appoximately how many characters will be shown in the search results. So I will truncate the description if the number of characters is greater then what we configured in the meta data file. We should turn it into something like this:

    pageDescription: (data) => {
        if (data.description.length > meta.page.description.length) {
            return `${data.description.slice(0, (meta.page.description.length - 1)).trim()}…`
        } else {
            return data.description;
        }
    },

Creating the index file

Before we build, we will need an index file. Remember when we defined the template formats that Eleventy was going to load? Well, we are going to use one of them! Create a file in the _src directory called index.njk. index.njk will serve as our default file. Eleventy uses a system for its template files called FrontMatter. Frontmatter allows us to add data to the template without having to manage multiple files. We generally define Frontmatter in the YAML language, but TOML and JS are also supported.

We will be adding the following frontmatter to our file:

---
title: Eleventy Core Boilerplate
description: Eleventy Core boilerplate is a starter file unopinionated and optimized for today's demanding web applications. We are proud to present the most useful and realistic use of the Eleventy Static Site Generator to you.
keywords: Eleventy, 11ty, Static Site, Boilerplate, Starter File
---

Notice the three (3) dashes at the beginning and end of the block? That defines the start and end of the frontmatter, so anything inside this block is YAML.

Next we are going to create a very simple webpage file. Since we are using Nunjucks, we will need a way to add variables in to the html. An example of a nunjucks variable in the code below would be {{ pageTitle }}. So lets copy this below the frontmatter:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ pageTitle }}</title>
    <meta name="description" content="{{ pageDescription }}">
    <meta name="keywords" content="{{ pageKeywords }}">
    <link rel="canonical" href="{{ cannonicalUrl }}">
</head>
<body>
    {{ content }}
</body>
</html>

Build the project

The next step is actually to run the build to get our files. I have set up the following script in my package.json:

    "build:dev": "NODE_ENV=development eleventy",

This script simply sets the NODE_ENV environment variable to development and then runs Eleventy. Just use npm run build:dev, and your project will build.

And that is it for this first part. You now have a fully functional, simple site setup. Next time, we will create some useful ancillary files and split up some of these templates for a better development experience.