Skip to content

Latest commit

 

History

History
895 lines (660 loc) · 35.4 KB

eleventy_introduction_en.md

File metadata and controls

895 lines (660 loc) · 35.4 KB

Eleventy (11ty) by Zach Leatherman

1. Introduction

Eleventy is a Static Site Generator created and maintained by Zach Leatherman. Eleventy allows you to develop websites based on templates and data or content files (YAML / Markdown / HTML / JSON / JS) in a source directory. Based on those source files, Eleventy will generate a fully functional static site in a destination folder. You will then be able to deploy that site on any web server capable of serving static files.

The stated goal of Eleventy is to be an alternative to Jekyll, written in Node rather than in Ruby. Like Jekyll, 11ty is a very approachable SSG, once the basic principles are well understood.

Node being quite fast, Eleventy is a performant SSG. It is also very flexible. Since it is written in Node, 11ty allows you to use the NPM ecosystem to extend its functionalities. You can also pick your favorite in a long list of templating languages. In this course we will use Nunjucks by Mozilla for all code samples.

2. Installation and configuration

Installation

Let's start by creating a folder for our new project:

mkdir eleventy-portfolio
cd eleventy-portfolio

You will need Node installed on your machine. You can then install Eleventy as an NPM module. The best way to proceed is to create a package.json that will manage all our Node dependencies.

npm init -y

That will skip a bunch of questions and create a standard file. You can come back to it and edit your package.json as you see fit. Here is the official documentation to help you find out about the available parameters and options.

We can then install Eleventy as a local dependency for our project:

npm install --save-dev @11ty/eleventy

At this point, Eleventy will only create a node_modules folder and install itself, nothing more. To see it in action, we will have to create at least one file. Let's create a basic index.html file that Eleventy will copy in the default _site destination folder.

Once we have created that file, we can learn some basic Eleventy commands:

  • npx @11ty/eleventy: to run Eleventy
  • npx @11ty/eleventy --serve: to run a local web server that will reload the site in your browser when the site changes
  • npx @11ty/eleventy --help: to explore the list of available commands and flags

Now if we type npx @11ty/eleventy in our terminal, Eleventy will create a _site folder and generate our trusty index.html file into it. Pretty impressive, right?

Let's configure Eleventy to better suit our needs.

Configuration

package.json

We will use ESM (import and export) throughout this workshop. In order for all your .js files to be interpreted as ESM instead of CommonJS, add "type": "module" to your package.json file.

file: package.json

{
  "type": "module"
}

eleventy.config.js

We will make a basic project architecture and configure Eleventy by creating an eleventy.config.js configuration file at the root of our project.

  • Remove Eleventy's default destination ./_site folder.
  • Create a ./src folder and move index.html into it.
  • Create an eleventy.config.js file at the root of the project.

Let's start by specifying source and destination directories for Eleventy:

file: eleventy.config.js

// override default config
export const config = {
  dir: {
    input: "src",
    output: "dist",
  },
};

Now when we run the npx @11ty/eleventy command in our terminal from the root of our project, Eleventy will generate a ./dist folder and will copy our index.html file to it.

Tell Eleventy to copy some folders and files

We also can use this configuration file to tell Eleventy to copy any file or folder from the source directory to the destination directory. In order to instruct Eleventy to do so, we are going to use addPassthroughCopy. Good candidates to copy are static assets like images and font files.

  • Create a ./src/assets/img/ directory and drop a few optimized images in there.
  • Create a ./src/assets/fonts/ directory and drop a few font files in there.
  • Create a ./src/assets/js/ directory and drop a JavaScript file (or two) in there.
  • Create a ./src/assets/css/ directory and drop a CSS file in there.

Let's modify our eleventy.config.js file as follows:

file: eleventy.config.js

export default function (eleventyConfig) {
  // copy files
  eleventyConfig.addPassthroughCopy("./src/assets/");
}

// override default config
export const config = {
  dir: {
    input: "src/",
    output: "dist/",
    data: "_data",
    includes: "_includes",
  },
  templateFormats: ["njk", "md", "html"],
  htmlTemplateEngine: "njk",
  markdownTemplateEngine: "njk",
};

Eleventy will now copy the ./src/assets/ directory and everything it contains to the output directory, while preserving the directories structure.

Ignore directories and files

By default, Eleventy will ignore the node_modules directory as well as the folders, files and globs specified in your .gitignore file.

We can also create a .eleventyignore file at the root of our project and specify a file, directory or glob pattern per line to explicitly tell Eleventy to ignore all matching files or directories.

Alternatively, you can ignore files using the configuration API in your eleventy.config.js file. That's my preferred method. While we are at it, we will also exclude those files from Eleventy's watch list.

file: eleventy.config.js

export default function (eleventyConfig) {
  // avoid processing and watching files
  eleventyConfig.ignores.add("./src/assets/**/*");
  eleventyConfig.watchIgnores.add("./src/assets/**/*");

  // copy files / folders
  eleventyConfig.addPassthroughCopy("./src/assets/");
}

// override default config
export const config = {
  dir: {
    input: "src/",
    output: "dist/",
    data: "_data",
    includes: "_includes",
  },
  templateFormats: ["njk", "md", "html"],
  htmlTemplateEngine: "njk",
  markdownTemplateEngine: "njk",
};

Assets pipeline and build tools

Eleventy doesn't offer an asset pipeline or a build tool by default. I always almost use Eleventy with build tools, whether with NPM scripts, Vite or any other alternatives.

When you start using build tools to create an assets pipeline, you will likely have to modify your addPassthroughCopy and ignore assets directories that will not need to be handled by Eleventy. Your build step will take care of them and generate what you need in your ./dist directory.

If, for example, you are using build tools to handle your CSS and JavaScript files, you will have to make the following modifications:

file: eleventy.config.js

export default function (eleventyConfig) {
  // avoid processing and watching files
  eleventyConfig.ignores.add("./src/assets/**/*");
  eleventyConfig.watchIgnores.add("src/assets/**/*");

  // copy files
  eleventyConfig.addPassthroughCopy("./src/assets/fonts/");
  eleventyConfig.addPassthroughCopy("./src/assets/img/");

  // watch compiled CSS and JS for change and reload browser
  eleventyConfig.setServerOptions({
    watch: ["./dist/assets/css/**/*.css", "./dist/assets/js/**/*.js"],
  });
}

// override default config
export const config = {
  dir: {
    input: "src/",
    output: "dist/",
    data: "_data",
    includes: "_includes",
  },
  templateFormats: ["njk", "md", "html"],
  htmlTemplateEngine: "njk",
  markdownTemplateEngine: "njk",
};

Eleventy will now completely ignore the ./src/assets/css/ and ./src/assets/js/ directories, while your build tools and scripts will generate the required outputs in your ./dist/ directory. The Eleventy Dev Server will also reload your browser whenever your compiled CSS and JS files change in your ./dist/assets/js/ and ./dist/assets/css/ directories.

Personally, I use NPM scripts combined with esbuild (JS files) Lightning CSS (CSS files) for most of my Eleventy projects.

3. Define and structure your data

Eleventy allows you to work with two main data sources:

  1. Markdown files (for the main content) and YAML front matter (for the rest of the data structure) that can easily be turned into collections (more on that later).
  2. JSON and/or JS data files that can either be static or dynamic (fetched from an API).

These two types of data sources are not mutually exclusive. They are often used simultaneously in projects. Let's dive in.

Collections

Collections in Eleventy allow you to group content items in interesting ways and to use them in your templates.

Markdown and YAML front matter

Markdown files coupled with a YAML front matter allow you to use text files as structured data sources. It's a common feature of most SSG out there.

The Markdown part of the file generally represents the main content of your data and is usually converted to HTML. The YAML front matter allows you to create a data structure with different types of data (strings, arrays, objects, etc.).

If you want to build a blog, your blogposts are going to be represented by Markdown files with a YAML front matter that could look something like the following:

file: ./src/blog/2019-07-22-markdown-yaml-front-matter.md

---
title: "This is the title"
intro: "This is an introductory paragraph for a blogpost"
imageSmall: "testimage_600.jpg"
imageMedium: "testimage_1024.jpg"
imageBig: "testimage_1500.jpg"
imageAlt: "Alternative text for picture"
categories:
  - front-end
  - Jamstack
  - Eleventy
---

## Level 2 title

This is some content for this blogpost.

In this case, the date of the blogpost will be derived from the filename. Another option is to specify a date in the YAML front-matter

---
date: 2024-11-26
---

Let's say you also need to model a data structure for a team. Each team member could be represented by a file like this one:

file: ./src/projects/jerome-coupe.md

---
name: "Jérôme"
surname: "Coupé"
image: "jerome_coupe.jpg"
mastodon: "https://mastodon.social/@jeromecoupe"
github: "https://github.com/jeromecoupe"
website: "https://www.webstoemp.com"
---

Jérôme Coupé is a looney front-end designer and teacher from Brussels, Belgium. When not designing or coding, he might have been seen downtown, drinking a few craft beers with friends.

Collection API

For Eleventy to group those files in an array that will allow you to work with it in your templates, you have to create a collection. Any content item can be part of one or more collections.

To create a collection, you can assign the same tag to various content items. Personally, I would much rather use the collection API and our trusty eleventy.config.js file.

This API offers you different methods to declare your collections that each have their use. My favourite and most used one by far is getFilteredByGlob(glob) that allows you create a collection from all files matching a defined glob pattern.

If all your Markdown files for your blogposts are in a ./src/blog/ directory, grouping them into a collection is quite easy. You have to add the following code to your eleventy.config.js configuration file. While we are at it, we will also add our team collection.

export default function (eleventyConfig) {
  // blogposts collection
  eleventyConfig.addCollection("blogposts", function (collection) {
    return collection.getFilteredByGlob("./src/blog/*.md");
  });

  // team collection
  eleventyConfig.addCollection("team", function (collection) {
    return collection.getFilteredByGlob("./src/team/*.md");
  });

  // ... more configuration ...
}

You can now access your collections using collections.blogposts and collections.team in your templates. We will come back to that when we get to templating.

It is also important to know that, by default, Eleventy creates a collection that contains all your content items. This special collection can be accessed in your templates by using collections.all.

When a collection is created, the following keys are automatically created:

  • page: contains most of the variables created by Eleventy
    • page.url: url used to link to this piece of content. In general, this is based on the permalink key defined for your content items.
    • page.fileSlug: input filename minus template file extension
    • page.filePathStem: inputPath minus template file extension
    • page.date: date of the source file
    • page.inputPath: path to the original source file for the template (includes input directory)
    • page.outputPath: path to the output file (depends on your output directory)
    • page.outputFileExtension: output file extension
    • page.templateSyntax: Comma separated list of template syntaxes used to process the file
    • page.rawInput: raw unparsed/unrendered plaintext content for the current template
    • page.lang: available since 2.0 with the i18n plugin
  • data: all data for this piece of content. includes data inherited from layouts and YAML front-matter data.
  • content or templateContent: rendered content of this template. Does not include layout wrappers.

Sort and filter your collections

When you create a collection in Eleventy, its items are automatically sorted in ascending order using:

  1. The date specified in the filename or in the YAML front matter of the source file. The creation date on the filesystem is used as a fallback.
  2. If some files have an identical date, the full path of the file (including the filename) will be taken into account as well.

If what you need is to sort your collection items by date, you are covered. You can also use the reverse filter in Nunjucks if needed.

By contrast, if you need to sort your team members alphabetically using the value of their surname key, you will need to use the JavaScript sort method.

export default function (eleventyConfig) {
  // Team collection
  eleventyConfig.addCollection("team", function (collection) {
    return collection
      .getFilteredByGlob("./src/team/*.md")
      .sort((a, b) => a.data.surname.localeCompare(b.data.surname));
  });

  // ... more configuration ...
}

If you need to filter a collection to exclude some data, you can use the JavaScript filter method. You can for example only include the blogposts that do not have a draft key set to true in their front matter and that have a publication date less recent than site generation date. Let's sort them by most recent dates

const now = new Date();

export default function (eleventyConfig) {
  // blogposts collection
  eleventyConfig.addCollection("blogposts", function (collection) {
    return collection
      .getFilteredByGlob("./src/blog/*.md")
      .filter((item) => item.data.draft !== true && item.date <= now)
      .reverse();
  });

  // ... more configuration ...
}

Data files (JS or JSON)

Aside from collections, the other data source you can use with Eleventy are data files. Those can be static or dynamic (fetched from an API).

By default, these files have to be stored in the ./src/_data/ directory. This can be modified using the eleventy.config.js configuration file.

export default function (eleventyConfig) {
  // ... more configuration ...
}

// override default config
export const config = {
  dir: {
    // more configuration
    data: "_data",
    // more configuration
  },
  // more configuration
};

Static data files

Static data files are JSON or JS files containing key/value pairs.

file: ./src/_data/site.js

export default {
  title: "Title of the site",
  description: "Description of the site",
  url: "https://www.mydomain.com",
  baseUrl: "/",
  author: "Name Surname",
  buildTime: new Date(),
};

Those values can be accessed in your templates by using the filename as a key. Data contained in a ./src/_data/site.js file will thus be accessible in your templates using the site variable. To display any value in that file in yout template, you'll have to use dot notation: {{ site.author }}.

Dynamic data files

Because data files can be JavaScript files, nothing is preventing you from connecting to an API in one of those files by using fetch or axios for example.

Each time you generate your site, Eleventy will execute that script and treat the JSON file returned by the API like a static one.

The Eleventy Fetch plugin allows you to simplfy your API calls et to locally cache responses during a configurable time period.

Permalinks and URLs

By default, Eleventy will use the folders and files structure in your source directory to generate static files in your output folder.

  • ./src/index.html will generate ./dist/index.html with / as the URL.
  • ./src/test.html will generate ./dist/test/index.html with /test/ as the URL.
  • ./src/subdir/index.html will generate ./dist/subdir/index.html with /subdir/ as the URL.

This default behavior can be changed by using a static or dynamic permalink variable in your content files or in your templates.

For example, to create a blog, you will need an index page at the following URL /blog/index.html. Your Nunjucks template ./src/pages/blog.njk should have that as the value of the permalink key in its YAML front matter.

---
permalink: "/blog/index.html"
---

You can also use variables to create dynamic permalink values. This could be in the YAML front matter of all your Markdown blogposts files.

---
permalink: "/blog/{{ page.fileSlug }}/index.html"
---

If you have a collection to group all your team members but you do not need a detail page for each member, you can use false as the value of the permalink key. Eleventy will not generate detail pages. In most of those cases, you won't need a dedicated layout either.

---
permalink: false
layout: false
---

We will see later that you will then need to use the content or templateContent key to display the content of these Markdown files.

Default values and directory data files

Instead of having to specify the same YAML front matter key / value pair for a bunch of files, Eleventy allows you to specify the same key/value pair for all the files in a directory by using JS or JSON directory data files.

If you want to specify the same key/value pair for permalink and layout for all of your blogposts, you can add a ./src/blog/blog.json, ./src/blog/blog.11data.json or ./src/blog/blog.11data.js directory data file in the ./src/blog directory and specify them there. Eleventy will apply those values to all the files in that directory or in its subdirectories.

file: ./src/blog/blog.json

{
  "layout": "layouts/blogpost.njk",
  "permalink": "blog/{{ page.fileSlug }}/index.html"
}

file: ./src/blog/blog.11tydata.js

export default = {
  layout: "layouts/blogpost.njk",
  permalink: "blog/{{ page.fileSlug }}/index.html",
};

4. Templating with Eleventy and Nunjucks

Eleventy allows you to work with several templating languages. Nunjucks from Mozilla is powerful and easy to use, so that's generally my default choice. The documentation is quite well done so we don't need to cover everything. We will learn enough to create the templates we need for our small blog project, and you can expand your knowledge on your own later on.

Main Nunjucks tags

Nunjucks has three main types of tags:

  • comments: {# this is a comment #}
  • display: {{ variable }}
  • logic: {% logic %}

Comment tags

This one is pretty easy: {# This is a comment #}. Comments never appear in the rendered code.

Display tags: variables and properties

This tags allow you to display variables like strings, numbers, booleans, arrays and objects in your templates. Most of the time, you will display variables created by you or by Eleventy when it runs. You can access properties using dot syntax, like in JavaScript. Those tags also allow you to perform maths operations or string concatenation.

Examples:

  • {{ "Hello World" }}: displays the string "Hello World"
  • {{ site.title }}: displays the value of the title key of the site object
  • {{ site.title ~ " - is an awesome site" }} will concatenate the value of the key title of the site object with the string next to it
  • {{ 8 + 2 }}: displays 10

Filters

Filters are essentially devoted to manipulate strings, numbers, booleans, arrays and objects while displaying them in your templates. Nunjucks makes a bunch of built-in filters available. Here are some examples.

  • {{ "this should be uppercase" | upper }} will output THIS SHOULD BE UPPERCASE.
  • {{ [1,2,3,4,5] | reverse }} will output 5,4,3,2,1. This filter is quite useful when combined with date sorted collections in Eleventy.
  • {{ collections.blogposts | length }} will display the number of items in your blogposts collection.
Custom filters in Eleventy

Eleventy allows you to create your own filters using JavaScript and the eleventy.config.js configuration file. these filters can then be used in most templating languages you choose to use.

For example, Nunjucks does not have a built-in date formatting filter. We can create some in Eleventy using the Luxon library. After installing that npm package in uour project, we can add a couple of filters to our config file.

// required packages
import { DateTime } from "luxon";

export default function (eleventyConfig) {
  // ... more configuration ...

  /**
   * Format date: ISO
   * @param {Date} date
   */
  eleventyConfig.addFilter("dateIso", function (date) {
    const jsDate = new Date(date);
    const dt = DateTime.fromJSDate(jsDate);
    return dt.toISO();
  });

  /**
   * Format date: Human readable format
   * @param {Date} date
   */
  eleventyConfig.addFilter("dateFull", function (date) {
    const jsDate = new Date(date);
    const dt = DateTime.fromJSDate(jsDate);
    return dt.setLocale(locale).toLocaleString(DateTime.DATE_FULL);
  });
}

Once created, we can use these filters in our templates.

<p><time datetime="{{ page.date | dateIso }}">{{ page.date | dateFull }}</time></p>

Logic tags

These tags allow you to execute operations and can be used to create variables, for loops, control structures, etc.

Assign variables

The code below will assign all blogposts in the blogposts collection to an allBlogposts variable and use the built-in reverse filter to sort them by reverse date order.

{% set blogposts = collections.blogposts | reverse %}
Control structures

Nunjucks will allow you to use traditional control structures like if and else as well as comparison operators like ===, !==, > or logical operators like and, or and not.

{% if collections.blogposts | length %}
  <p>There is at least one blogpost in this collection</p>
{% endif %}
{% if not collections.blogposts | length %}
  <p>There is no blogpost in this collection</p>
{% endif %}
{% if collections.blogposts | length >= 2 %}
  <p>There is at least two blogposts in this collection</p>
{% endif %}
for loop

When you have to display data, whether they come from an API or from Markdown files, you will have to walk through arrays or objects using for loops. Let's display title and introductions for all our blogposts in an HTML list.

{% set blogposts = collections.blogposts %}
{% for entry in blogposts %}
  {% if loop.first %}<ul>{% endif %}
    <article>
      <h2><a href="{{ entry.url }}">{{ entry.data.title }}</a></h2>
      <p>{{ entry.data.intro }}</p>
    </article>
  {% if loop.last %}</ul>{% endif %}
{% else %}
  <p>No blogposts found</p>
{% endfor %}

You will notice that we can use loop.first and loop.last to only spit out the <ul> and </ul> in the first and last iterations of the loop, respectively. In Nunjucks, an else clause is executed when no data is returned, which allows us to display a warning in HTML.

Layouts, includes, macros and shortcodes

On top of offering you an {% include %} tag, Nunjucks uses template inheritance as its layout model with {% extends %}. This allows you to define blocks with {% block blockname %} in a template and then to override the content of those blocks with child templates that extends the parent one. These chains of templates can be as long as you wish.

Includes as well as template inheritance can be used with Eleventy. The only quirk is that templates to extend as well as template to include must all be located in the includes directory specified in your eleventy.config.js configuration file. By default, this directory is _includes and the path you specify in your config file is relative to your source directory.

file: eleventy.config.js

export default function (eleventyConfig) {
  // more configuration
}

// override default config
export const config = {
  dir: {
    // path is relative to the input directory
    // "_includes" is the default value
    includes: "_includes",
  },
  // more configuration
};

When a parent template is extended by a child template, variables defined in the child template are accessible in the parent template.

Apart from {% extends %} and {% include %}, Nunjucks allows you to use macros that are reusable code snippets to which you can pass variables. Eleventy has a similar concept with the shortcodes.

Let's come back to our project and create the templates we need using everything we have learned so far.

Layouts

file: ./src/_includes/layouts/base.njk

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>{{ metaTitle }} - {{ site.title }}</title>
  <meta name="description" content="{{ metaDescription }}">
  <meta name="author" content="{{ site.author }}">

  <!-- CSS -->
  <link rel="stylesheet" href="/css/main.min.css?ref={{ site.buildTime | date('X') }}">

  <!-- Feed -->
  <link rel="alternate" href="/feed.xml" title="{{ site.title }} - Blog RSS" type="application/atom+xml">

  <!-- Open Graph -->
  <meta property="og:type" content="article" />
  <meta property="article:author" content="{{ site.author }}" />
  <meta property="og:url" content="{{ site.url ~ page.url }}" />
  <meta property="og:title" content="{{ metaTitle }}" />
  <meta property="og:description" content="{{ metaDescription }}" />
  <meta property="og:image" content="{{ metaImage }}" />

  <!-- Favicon + Apple icon (minimal) -->
  <link rel="icon" href="favicon.ico" />
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">

</head>
<body>
  {% include "partials/siteheader.njk" %}

  {% block content %}{% endblock %}

  {% include "partials/sitefooter.njk" %}
  <script src="/js/main.bundle.js?ref={{ site.buildTime | date('X') }}"></script>
</body>
</html>

Here is an included file for the footer

file: ./src/_includes/partials/sitefooter.njk

<div class="c-sitefooter">
  <p>&copy; {{ site.buildTime | date("Y") }} - La casa productions</p>
</div>

As far as blogposts go, we need a special layout which will extend our base layout. This blogpost layout will be used by all the Markdown files in our collection to generate detail pages. This layout is the one specified for all our blogposts using a directory data file (see above).

file: ./src/_includes/layouts/blogpost.njk

{% extends "layouts/base.njk" %}

{% set metaTitle = title %}
{% set metaDescription = intro %}
{% set metaImage = site.url ~ "/assets/img/blogposts/" ~ imageSmall %}

{% block content %}
  <main>
    <div class="c-blogpost">
      <div class="c-blogpost__media">
        <picture>
            <source
              srcset="/assets/img/blogposts/{{ imageMedium }} 1024w,
                      /assets/img/blogposts/{{ imageBig }} 1500w"
              sizes="(min-width: 1140px) 1140px,
                     100vw"
              media="(min-width: 500px)">
            <img
              src="/assets/img/blogposts/{{ imageSmall }}"
              class="o-fluidimage"
              alt="{{ imageAlt }}">
        </picture>
      </div>

      <div class="c-blogpost__body">

        <header>
          <p class="c-suptitle  c-suptitle--dark"><time datetime="{{ page.date | date('YYYY-MM-DD') }}">{{ page.date | date("MMMM Do, YYYY") }}</time></p>
          <h1 class="c-h1">{{ title }}</h1>
          <div class="c-blogpost__intro">
            <p>{{ excerpt }}</p>
          </div>
        </header>

        <div class="c-richtext">
          {{ content | safe }}
        </div>

      </div>
    </div>
  </main>
{% endblock %}

All blogposts in markdown use this blogposts.njk which will, in turn, extend the base.njk template to generate all blogposts detail pages.

The blogposts files specify the layout they use using a layout key taking the path to the layout file as value (here layouts/blogpost.njk). As we have seen earlier, it is often more practical to use directory data files to spécify that layout key for all files in a directory.

Pages

Here is an example of template for the about page, where will will display a list of our team members. It extends our base layout and sets several variables that will be available in the base layout.

file: ./src/pages/about.njk

---
permalink: /about/index.html
---
{% extends "layouts/base.njk" %}

{% set metaTitle = "About us" %}
{% set metaDescription = "Meet the team behind the blog" %}
{% set metaImage = site.url ~ "/img/meta/team.jpg" %}

{% block content %}
  <h1 class="h1">Meet our awesome team</h1>

  {% set team = collections.team %}
  {% for member in team %}
    {% if loop.first %}<ul class="c-teamlist">{% endif %}
      <li class="c-teamlist__item">
        <article class="c-teammember">
          <img class="o-fluidimage" src="/assets/img/team/{{ member.image }}" >
          <h2 class="c-teammember__title">{{ member.name }} {{ member.surname }}</h2>
          <div class="c-teammember__bio">{{ member.content | safe }}</div>
          {% if member.mastodon or member.github or member.website %}
            <ul class="u-hlist  u-hlist--xs">
              {% if member.mastodon %}<li><a href="{{ member.mastodon }}">{% include "svg/icon-mastodon.svg" %}</a></li>{% endif %}
              {% if member.github %}<li><a href="{{ member.github }}">{% include "svg/icon-github.svg" %}</a></li>{% endif %}
              {% if member.website %}<li><a href="{{ member.website }}">{% include "svg/icon-website.svg" %}</a></li>{% endif %}
            </ul>
          {% endif %}
        </article>
      </li>
    {% else %}
      <p>No team member found</p>
    {% if loop.last %}</ul>{% endif %}
  {% endfor %}
{% endblock %}

Pagination

For the archive page of our blog, we will use the pagination provided by Eleventy. Pagination specifies what data must be paginated (this can be any iterable), how many items must be displayed per page with size and which alias must be used for the paginated data.

file: ./src/pages/blog.njk

---
pagination:
  data: collections.blogposts
  size: 12
  alias: posts
permalink: blog{% if pagination.pageNumber > 0 %}/page{{ pagination.pageNumber + 1}}{% endif %}/index.html
---
{% extends "layouts/base.njk" %}

{% set metaTitle = "Blog archives" %}
{% set metaDescription = "A blog about Belgium" %}
{% set metaImage = site.url ~ "/img/meta/default.jpg" %}

{% block content %}
  <h1 class="h1">A blog about Belgium</h1>

  {% for post in posts %}
    {% if loop.first %}<ul class="l-grid  l-grid--fluid">{% endif %}
      <li>
        <article class="c-blogpost">
          <a class="c-blogpost__link" href="{{ post.url }}">
            <img class="c-blogpost__image  o-fluidimage" src="/assets/img/blogposts/{{ post.imageSmall }}" >
            <h2 class="c-blogpost__title">{{ post.title }}</h2>
            <p class="c-blogpost__intro">{{ post.intro }}</p>
          </a>
        </article>
      </li>
    {% if loop.last %}</ul>{% endif %}
  {% else %}
    <p>No blogpost found</p>
  {% endfor %}

  {# pagination #}
  {% set totalPages = pagination.hrefs | length %}
  {% set currentPage = pagination.pageNumber + 1 %}
  {% if totalPages > 1 %}
    <ul class="c-pagination">
      {% if pagination.href.previous %}
        <li class="c-pagination__item  c-pagination__item--first">
          <a class="c-pagination__link" href="{{ pagination.href.first }}">First</a>
        </li>
      {% endif %}

      {% if currentPage > 1 %}
        <li class="c-pagination__item">
          <a class="c-pagination__link" href="{{ pagination.hrefs[pagination.pageNumber - 1] }}">{{ currentPage - 1 }}</a>
        </li>
      {% endif %}

      <li class="c-pagination__item">
        <span class="c-pagination__current" href="{{ pagination.hrefs[pagination.pageNumber] }}">{{ currentPage }}</span>
      </li>

      {% if currentPage < totalPages %}
        <li class="c-pagination__item">
          <a class="c-pagination__link" href="{{ pagination.hrefs[pagination.pageNumber + 1] }}">{{ currentPage + 1 }}</a>
        </li>
      {% endif %}

      {% if pagination.href.next %}
        <li class="c-pagination__item  c-pagination__item--last">
          <a class="c-pagination__link" href="{{ pagination.href.last }}">Last</a>
        </li>
      {% endif %}
    </ul>
  {% endif %}

{% endblock %}

The pagination function in Eleventy is much more powerful than it seems at first sight. If the data for our blogposts came as a big JSON file returned by an API, we could use the ./src/_data/_blogposts.js file as a data source and use the same pagination function to generate all the detail pages with a single file. In order to accomplish that, we have to specify a value of 1 for size and to craft a dynamic permalink pattern that corresponds to the URLs we want.

Here is a simplified template we could use:

file: ./src/pages/blogpost_entry.njk

---
pagination:
  data: blogposts
  size: 1
  alias: blogpost
permalink: blog/{{ blogpost.slug }}/index.html
---
{% extends "layouts/base.njk" %}

{% set metaTitle = blogpost.title %}
{% set metaDescription = blogpost.intro %}
{% set metaImage = site.url ~ "/assets/img/blogposts/" ~ blogpost.imageSmall %}

{% block content %}

  <h1>{{ blogpost.title }}</h1>
  <p><time datetime="{{ blogpost.date | date('Y-M-DD') }}">{{ blogpost.date | date("MMMM Do, Y") }}</time></p>
  <p>{{ blogpost.intro }}</p>
  {{ blogpost.body | safe }}

{% endblock %}

Workshop exercise

Start with the provided static templates to create a fully functional blog.

  • Configure Eleventy and create a date and a limit filter
  • Generate a homepage with the list of the latest 6 blogposts using the limit filter
  • Generate a paginated archive for all our blogposts
  • Generate detail pages for blogposts
  • Create an about page listing team members and providing contact details
  • Create a navigation using a data file (home, blog, about). The navigation must highlight the current section.