Sekrab Garage

A getting started guide with the bare minimum

Walk with an Eleventy site, before you can run

JavaScriptHosting September 4, 22

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:

  1. eleventy generates the site under _site folder
  2. eleventy --serve generates the site, watches changes and starts a local server at localhost:8080
  3. 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
  4. 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

  1. Build the project: npm run build
  2. 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 allow dist folder. There is no node_modules folder in the bare minimum setup.
  3. In GitHub repository, go to Settings > Pages
  4. Under Build and Deploy, choose Github Actions. This will take you to newly created page .github/workflows/pages.yml
  5. Scroll down to line 39 (approximate): jobs>deploy>steps>with>path, and change it to ./dist
  6. 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.