Walk with an Eleventy site, before you can run.
Ayyash
Posted on September 4, 2022
Every time I open the documentation website of Eleventy, I get this urge to cry, or change career! So I decided to create a small website, that has absolutely nothing, but the bare minimum, and keep it as reference, so next time I consider Eleventy, I don't have to go through their appreciated -yet painful- get started!
The final project is on StackBlitz
The plan is to cover the following:
- Eleventy Setup
- Eleventy Folders
-
_data
folder -
_includes
folder
-
- The front matter
- Multiple files from a collection
- Multiple files from an array
- Liquid specifics
- Liquid include files
- Passing variables
- Layout regions
- Bonus: delete folder first
- Hosting on GitHub Pages
The bare minimum
The target is to cover the basics, after which, finding specifics becomes easier in documentation.
- Liquid, using HTML extensions
- Install globally only
- Two configurations, one for dev, and one for publishing
- Deploy
dist
folder only, no pipelines, no actions - Use default
_data
and_includes
- No plugins
I also will deploy on Github Pages, to remind myself of how easy it is; their documentation makes you believe it's a military mission!
Eleventy Setup
1. Install Eleventy globally
npm install -g @11ty/eleventy
2. Create a folder for the project, and npm init
for future use. This is helpful in case we want to make it a local installation, or add other packages, or simply save scripts shortcut
3. Create .eleventy.js
configuration file, to change the source directory (to keep things tidy), and the only truly valuable setting is addPassthroughCopy
:
// .eleventy.js
module.exports = function (eleventyConfig) {
// this is a must, pass through your assets
eleventyConfig.addPassthroughCopy("src/assets");
return {
dir: {
input: "src"
// the default output is _site
}
}
};
4. Create a src
folder and add your first HTML
<!DOCTYPE html>
<html>
<body>
<h1>Hello World</h1>
</body>
</html>
5. Run one of the following commands:
-
eleventy
generates the site under_site
folder -
eleventy --serve
generates the site, watches changes and starts a local server atlocalhost:8080
-
eleventy --watch
generates the site, watches changes, but does not start a server, it's up to you to start a server that servers_site
folder -
eleventy --config=somethingelse
uses a different configuration file to generate the site.
6. Create a new file: .distconfig.js
to build on a different folder
// first get the dev config in
const devConfig = require("./.eleventy.js");
module.exports = function (eleventyConfig) {
// pass everything from config
const config = devConfig(eleventyConfig);
// set different output, you can deep clone first but it's too much work
return {
dir: {
input: config.dir.input,
output: "dist"
}
};
};
Run eleventy --config:.distconfig.js
, the output is in dist
folder. Deploy that!
That's it. This is the bare minimum. But not quite useful is it? Let's add our shortcut scripts in packages.json
then move on.
// packages.json
{
"scripts": {
"start": "eleventy --serve",
"watch": "eleventy --watch",
"build": "eleventy --config=.distconfig.js"
}
}
Eleventy folders
Two folders matter most, inside src
: _data
and _includes
. Contents do not get processed, so they can contain all data, layouts, shortcodes, and plugins.
_data
folder
The file name is the JavaScript object name, start with something.json
// src/_data/something.json
{
"name": "orange"
}
This is used in HTML like this:
<div>
<!-- in html, this outputs: orange -->
{{ something.name }}
</div>
If it is an array, things.json
:
// src/_data/things.json
[
{
"name": "orange"
},
{
"name": "purple"
}
]
In HTML:
<div>
<!-- in html, any html, this outputs: orange, purple -->
{{ things[0].name }}, {{ things[1].name }}
</div>
<!-- for loop in liquid -->
<ul>
{% for thing in things %}
<li>{{ thing.name }}</li>
{% endfor %}
</ul>
If it is a key-value, fewthings.json
:
// src/_data/fewthings.json
{
"orange": "sunny orange",
"purple": "dark purple"
}
In HTML:
<p>
<!-- this outputs: sunny orange, dark purple -->
{{ fewthings.orange }}, {{ fewthings.purple }}
</p>
<!-- for loop in liquid, with key-value -->
<ul>
{% for thing in fewthings %}
<!-- first element is key, second is value -->
<li>{{ thing[0] }}: {{ thing[1] }}</li>
{% endfor %}
</ul>
_Includes
folder
_includes
is where templates are created. Begin with a base html template: base.html
. Notice we did not have to create any special extensions this far. The only template variable that works out of the box is {{content}}
. In HTML extension, the safe
filter is not needed, nor will it work.
<!-- src/_includes/base.html -->
<!DOCTYPE html>
<html>
<head>
<!-- title is a variable that needs to be set in child page -->
<title>{{ title }} | Base 11ty Template</title>
</head>
<body>
<h1>Hello 11ty</h1>
<!-- here is the template literal that passes the content -->
{{ content }}
</body>
</html>
The front matter
Let's create our first file, with the front matter, the bare minimum:
<!-- src/index.html -->
---
# must be directly under _includes folder
layout: "base.html"
# title is optional
title: "a new title"
---
<h2>The base page using base html</h2>
To include the content of another file:
{% include './filelocation/file.html' %}
To create a sub layout, it's just as easy as creating the first layout, all data is fed upwards:
<!-- src/_includes/color.html -->
---
layout: base.html
---
<div>
<!-- the color variable is fed from pages using this template -->
Here is the details of {{ color }}
</div>
{{ content }}
Outside, let's create a folder colors
and a couple of html files:
<!-- src/colors/orange.html-->
---
# use child layout
layout: color.html
# pass color to first layout
color: orange
# pass title to parent layout
title: Orange
---
<h1>{{ color }} page</h1>
Multiple files from a tags
using collections
A collection
in Eleventy is a group of similar pages, the similarity is based on the tags
property. This allows us to list the similar pages, each item
has its own data
property.
<!-- src/colors/orange.html, similar purple.html -->
---
layout: color.html
color: orange
title: Orange
tags: colors
---
<h1>{{ color }} page</h1>
Listing them anywhere, here they are listed in color.html
layout
<!-- src/_includes/color.html -->
<h5>List of all colors</h5>
<ul>
<!-- 'collections' is provided by 11ty,
'colors' is the tags property,
'data' is provided by 11ty, -->
{% for item in collections.colors %}
<li>{{ item.data.color }}</li>
{% endfor %}
</ul>
Eleventy provided data
We can use page.url
to compare to collections[item].url
to single out the current item in a list
<!-- src/_includes/color.html -->
<ul>
{% for item in collections.colors %}
<!-- inline if condition for page.url and item.url, both provided by 11ty -->
<li {% if page.url == item.url %}class="selected"{% endif %}>
<a href="{{item.url}}">{{ item.data.color }}</a>
</li>
{% endfor %}
</ul>
This is how you list pages with the same tags
, but it is too fragmented. A better way is to let Eleventy loop through a data array
, and create pages accordingly.
Multiple files from an array using pagination
To generate multiple HTML files from an array in _data
folder, for example, things.json
:
<!-- src/loop.html, use front matter to create multiple files -->
---
layout: "base.html"
pagination:
data: things
# size 1 to create a page per item
size: 1
# alias it to use its props
alias: thing
---
{{ thing.name }}
This generates
/_site/loop/index.html
(0 index is removed from URL)
/_site/loop/1/index.html
To change the file names to proper names, we use permalink
, notice the /
characters, it generates a subfolder for index.html
, and thus a friendly URL.
<!-- src/loop.html -->
---
layout: "base.html"
pagination:
data: things
size: 1
alias: thing
# use permalink to fix the file names, use slugify filter to make name slug-ish
permalink: "loop/{{ thing.name | slugify }}/"
---
{{ thing.name }}
This will generate
/_site/loop/orange/index.html
/_site/loop/purple/index.html
If the name was Liberty Statue
, the slugify
filter would create: liberty-statue
Great. Now to pass back the props to the base template, we use eleventyComputed
in front matter, notice the format of the property value:
<!-- src/loop.html -->
---
layout: "base.html"
pagination:
data: things
size: 1
alias: thing
permalink: "loop/{{ thing.name | slug }}/"
# pass back the title to base template: use quotations
eleventyComputed:
title: "{{thing.name}}"
---
{{ thing.name }}
Listing them is as simple as looping through the array, unfortunately mapping the current page to one of the items is a little bit of work. Eleventy provides a pagination
object that is available in the pages that use it, one property is hrefs
, which we can use to compare to page.url
. forloop.index0
is Liquid syntax for item index in a zero-based array.
<ul>
{% for thing in things %}
<!-- pagination.hrefs is provided by 11ty
forloop.index0 is liquid syntax for zero-based index -->
<li {% if page.url == pagination.hrefs[forloop.index0] %}class="selected"{% endif %}>
<a href="/loop/{{thing.name | slugify }}">{{ thing.name }}</a>
</li>
{% endfor %}
</ul>
Liquid specifics
The following are very handy in Liquid JS:
- to assign a variable name and use it
{% assign something: 'pretty' %}
→ {{ something }}
- to open a liquid region with multiple lines
{% liquid
assign var = value | filter
# other liquid statements
%}
Liquid include files
More often than not, I want to combine two features: centralizing the array under _data
, and having the freedom of writing HTML. For that, we use Liquid include files.
To include another file as it is, another HTML file, let me begin with an example, having HTML data files in _data
folder:
<!-- /_data/things/orange.html, and a similar one purple.html -->
<h4>Orange</h4>
<p>
This is a rich HTML content file, that has access to liquid template variables
</p>
In a loop
file, make an include:
<!-- src/loop.html -->
<!-- include _data/thingname.html as it is -->
{% include './_data/things/{{thing.name | slugify }}.html' %}
Note: we must create the files manually and make sure the name of the file is equal to the output of slugify
filter. But the better way is to create a new property in our things.json
with slug
and use it everywhere instead.
Passing variables
We can pass variables and use them in HTML in Liquid:
<!-- src/loop.html -->
---
layout: "base.html"
pagination:
data: things
size: 1
alias: thing
permalink: "loop/{{ thing.name | slugify }}/"
eleventyComputed:
title: "{{thing.name}}"
---
<!-- pass a property -->
{% include './_data/things/{{thing.name | slugify }}.html', theme: 'dark' %}
Then use it:
<!-- src/_data/things/orange.html -->
<h4>Orange</h4>
<p>
{{ theme }}
</p>
Liquid layout regions
There is no support for Liquid layout blocks, there is however support for template variables. We can create a conditional in our HTML pages:
<!-- src/_data/things/orange.html -->
{% case region %}
{% when "header" %}
Header of orange
{% when "content" %}
The content region of orange, and the theme is {{ theme }}
{% endcase %}
And use it in our loop
<!-- src/loop.html -->
---
layout: "base.html"
pagination:
data: things
size: 1
alias: thing
permalink: "loop/{{ thing.name | slugify }}/"
eleventyComputed:
title: "{{thing.name}}"
---
<!-- include regions -->
<h4>{{ thing.name }}</h4>
<!-- liquid island to assign a _filepath variable -->
{% liquid
assign _slug = thing.name | slugify
assign _filepath = './_data/things/' | append: _slug | append: '.html'
%}
<article>
<header>
{% include _filepath', region: 'header' %}
</header>
<div>
<!-- passing extra params are okay -->
{% include _filepath, region: 'content', theme: 'dark' %}
</div>
</article>
I just need to remind myself that this can be taken a bit further, by making extra regions to be used elsewhere. For example, if I had an excerpt
region, I can display it on the home page.
<!-- src/_data/things/orange.html -->
{% case region %}
{% when "excerpt" %}
Short excerpt of Orange
{% when "header" %}
Header of orange
{% when "content" %}
The content region of orange, and the theme is {{ theme }}
{% endcase %}
In the loop:
{% for thing in things %}
<li>
...
add excerpt region
<div>
{% include './_data/things/{{thing.name | slugify }}.html', region: 'excerpt' %}
</div>
</li>
{% endfor %}
So this was a Getting Started. Deeper stuff is easier to find once we learn how to walk. Some of the things to look for:
- Eleventy configuration, specifically to change to Nunjucks
- Nunjucks templates and extended templates, and template regions
- Eleventy Data, fed from layouts
- Eleventy shortcodes, filters and plugins
Bonus: delete folders first
A bonus, is to delete the _site
and dist
folder before starting and building, you might find other solutions on the web, but here is the simple bare minimum, in the config files:
// .eleventy.js
// get fs from Node
const fs = require('fs');
module.exports = function (eleventyConfig) {
// rm sync _site folder, recursively
fs.rmSync('./_site', {recursive: true, force: true});
// ...
};
// .distconfig.js
const fs = require('fs');
module.exports = function (eleventyConfig) {
// rm sync dist folder
fs.rmSync('./dist', {recursive: true, force: true});
// remember: this will also remove _site
const config = devConfig(eleventyConfig);
// ...
};
Let's put it on GitHub Pages
Hosting on GitHub Pages
- Build the project:
npm run build
- Push the project to a GitHub repository (there are a number of ways to do that, none of them is straight forward, if you have a visual interface like VSCode, it makes things a tad bit easier). Ignore
_site
folder, but allowdist
folder. There is nonode_modules
folder in the bare minimum setup. - In GitHub repository, go to Settings > Pages
- Under Build and Deploy, choose Github Actions. This will take you to newly created page
.github/workflows/pages.yml
- Scroll down to line 39 (approximate):
jobs>deploy>steps>with>path
, and change it to./dist
- Commit
You probably want to pull this locally for future commits.
Give it 10 minutes, or a little more, when you go back to Settings > Pages, the new URL will be displayed on top. It most probably going to be
https://[username].github.io/[repositoryname]
If you setup a custom domain, it would be
https://[customdomain]/[repositoryname]
If your repository name is [username].github.io
, then the URL would be
https://[customedomain]
Note: you only need to set up a custom domain for the [username].github.io
just once, all new repos can be served off of your custom domain automatically.
For custom domain or subdomain, in your DNS manager, setup:
CNAME record:
- name: www [or subdomain],
- value: [username].github.io
To add apex domain (www
), also add:
A Record:
- name: @ (or domain.com)
- value: 185.199.108.153
So here is our project on GitHub, and hosted on Pages.
Eleventy, demystified.
RESOURCES
Posted on September 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.