DEV Community

Cover image for Walk with an Eleventy site, before you can run.
Ayyash
Ayyash

Posted on • Originally published at garage.sekrab.com

Walk with an Eleventy site, before you can run.

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

4. Create a src folder and add your first HTML

<!DOCTYPE html>
<html>
  <body>
    <h1>Hello World</h1>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

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 at localhost: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"
    }
  };
};
Enter fullscreen mode Exit fullscreen mode

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

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

This is used in HTML like this:

<div>
<!-- in html, this outputs: orange -->
{{ something.name }}
</div>
Enter fullscreen mode Exit fullscreen mode

If it is an array, things.json:

// src/_data/things.json
[
  {
    "name": "orange"
  },
  {
    "name": "purple"
  }
]
Enter fullscreen mode Exit fullscreen mode

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

If it is a key-value, fewthings.json:

// src/_data/fewthings.json
{
  "orange": "sunny orange",
  "purple": "dark purple"
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

Multiple files from a tags using collections

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

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

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

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

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

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

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

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

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

In a loop file, make an include:

<!-- src/loop.html -->
<!-- include _data/thingname.html as it is -->
{% include './_data/things/{{thing.name | slugify }}.html' %}
Enter fullscreen mode Exit fullscreen mode

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

Then use it:

<!-- src/_data/things/orange.html -->
<h4>Orange</h4>
<p>
  {{ theme }}
</p>
Enter fullscreen mode Exit fullscreen mode

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

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

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

In the loop:

{% for thing in things %}

  <li>
    ...
        add excerpt region
    <div>
    {% include './_data/things/{{thing.name | slugify }}.html', region: 'excerpt' %}
    </div>
  </li>
{% endfor %}
Enter fullscreen mode Exit fullscreen mode

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);

  // ...
};
Enter fullscreen mode Exit fullscreen mode

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

To add apex domain (www), also add:

A Record:
- name: @ (or domain.com)
- value: 185.199.108.153
Enter fullscreen mode Exit fullscreen mode

So here is our project on GitHub, and hosted on Pages.

Eleventy, demystified.

RESOURCES

Walk with an Eleventy site, before you can run, JavaScript, Hosting - Sekrab Garage

A getting started guide with the bare minimum

favicon garage.sekrab.com

Top comments (0)