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

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

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

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

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

Running and building

Finally, we can start adding the start and build tasks to our package.json file:

{
  "scripts": {
    "start": "eleventy --serve",
    "build": "eleventy"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

_data javascript example

_data/site.js

module.exports = {
  name: 'My awesome site',
  url: 'https://awesome.site/'
};
Enter fullscreen mode Exit fullscreen mode

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

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

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

Then, you can use this filter in your content and template files like this:

{{ content | scream | safe }}`
{{ page.date | date('YYYY-MM-DD') }}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

Finally, in your eleventy config, change the pass through copy to copy the parcel output into your eleventy output folder:

config.addPassthroughCopy('./src/dist/');
Enter fullscreen mode Exit fullscreen mode

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

Example project

Resources

Thank you πŸ‘©β€πŸ’»

Top comments (2)

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

Collapse
 
zeeshan profile image
Mohammed Zeeshan

This is an awesome read! Thank you for making this πŸ’Ž