DEV Community

Cover image for Eleventy in eleven minutes
Lea Rosema (she/her) for Studio M - Song

Posted on

Eleventy in eleven minutes

Image by 11ty.dev

I would like to share my opinionated path how I got started using.

Table of Contents

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 (eg My site to my-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

Thank you 👩‍💻

Top comments (2)

 
webmarks profile image
Dany

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.

 
zeeshan profile image
Mohammed Zeeshan

This is an awesome read! Thank you for making this 💎