Eleventy is a static site generation that is most commonly used to take files with content, such as Markdown or HTML and combine them with templates to turn them into websites. Besides plain content files Eleventy can also process JSON files and data generated by JavaScript code. We can use these structured data formats to generate multiple pages from a single data source.

In this article we are going to explore page creation, first from static JSON data files and then we will use JavaScript to dynamically create pages from an API.

Using global data files in Eleventy

In our example we want to create a list of Pokémon that links to a detail page for each Pokémon. We start by creating a JSON file in Eleventys global data directory: _data/pokemon.json.

[
  { "dexno": "001", "name": "Bulbasaur" },
  { "dexno": "002", "name": "Ivysaur" },
  { "dexno": "003", "name": "Venusaur" }
]

Before we generate the detail pages, we want to show a list of these Pokémon on our start page. Create a new template and call it index.njk:

<h1>Pokemon</h1>

<ul>
  {%- for species in pokemon %}
  <li>{{ species.name }}</li>
  {% endfor -%}
</ul>

Eleventy exposes the data using a variable with the same name as the file. In the template we use a for loop to iterate trough the items in pokemon and render their names in a list. After building the site we should see a page that lists all the Pokémon from _data/pokemon.json.

Creating pages from global data files

The next step is to generate the detail pages for each Pokémon by using the pagination feature from Eleventy. The trick is to define a size of 1, which will tell Eleventy to create a page for each element in the array. We start by creating a template called pokemon.njk and define the size and data in the pagination frontmatter.

---
pagination:
  data: pokemon
  size: 1
permalink: 'pokemon/{{ pagination.pageNumber }}/index.html'
---

<h1>{{ pagination.items[0].name }}</h1>

There are a couple of interesting things in that template:

  • We use the pagination.data frontmatter to define the name of the variable that holds our data. Because the data file is called pokemon.json, the data will be available in the pokemon variable.
  • pagination.size needs to be set to 1 to tell Eleventy to create a page for each element in the array
  • We define a permalink for each page by using pagination.pageNumber.
  • Because we use the pagination feature, accessing the data is a bit awkward. Since pagination.items holds all the items that should be displayed on this page and our pagination size is 1 it will always contain exactly 1 element. We will see a solution to make this a bit nicer soon.

After building the site we will get the following pages:

- index.html
- pokemon/0/index.html
- pokemon/1/index.html
- pokemon/2/index.html

Better permalink for pages created from data

At the moment the URL of each page contains its page number, that is, the index of the element in the data array. However, since we have access to pagination.items in the permalink frontmatter we can tell Eleventy to create prettier permalinks:

---
pagination:
  data: pokemon
  size: 1
permalink: 'pokemon/{{ pagination.items[0].dexno }}/index.html'
---

<h1>{{ pagination.items[0].name }}</h1>

After another build we will get the following files:

- index.html
- pokemon/001/index.html
- pokemon/002/index.html
- pokemon/003/index.html

We also have access to universal filters in the permalink frontmatter and can use the slug filter to use the Pokémon name in the URL:

---
pagination:
  data: pokemon
  size: 1
permalink: 'pokemon/{{ pagination.items[0].name | slug }}/index.html'
---

<h1>{{ pagination.items[0].name }}</h1>

Now Eleventy will build the following pages:

- index.html
- pokemon/bulbasaur/index.html
- pokemon/ivysaur/index.html
- pokemon/venusaur/index.html

Eleventy recognises that using pagination.items[0] in templates is ugly and provides us with the alias option. It will setup a variable with the given name that points to pagination.items[0] if the array contains exactly one element, and to pagination.items if the array contains multiple items.

---
pagination:
  data: pokemon
  size: 1
  alias: detail
permalink: 'pokemon/{{ detail.name | slug }}/index.html'
---

<h1>{{ detail.name }}</h1>

Nice. Now we have exactly what we want: Eleventy reads our JSON file with Pokémon data and generates both an overview page and a detail page for each Pokémon with a pretty URL.

Generating pages from dynamic data

At the time of writing there are 898 different Pokémon and, let's be honest, we don't want to manually compile that list. We would also like to quickly update the list of Pokémon once new ones are released. We're in luck: in addition to JSON files we can define global data also in JavaScript files and Eleventy will expose the data to our templates and even handle promises for us.

First we delete _data/pokemon.json and replace it with _data/pokemon.js, which exports an asynchronous function that fetches the list of Pokémon from PokéAPI and returns it.

const fetch = require('node-fetch');

module.exports = async function () {
  const result = await fetch('https://pokeapi.co/api/v2/pokemon?limit=10');
  return (await result.json()).results;
};

To make this code work you need to install node-fetch from NPM. In this example we limit the results to 10, because Eleventy will execute this request whenever it builds the site and while developing it is annoying when every small change triggers the creation of 898 pages.

If we build the site, we will immediately see a couple of new pages for the Pokémon 4 to 10. We have nothing else to do since PokéAPI returns an array, where each element has a name property.

Conclusion

With the ability to generate pages from dynamic data we can use Eleventy in even more scenarios as a convenient tool to turn data into static HTML.