Creating pages from data with Eleventy
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 calledpokemon.json
, the data will be available in thepokemon
variable. pagination.size
needs to be set to1
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 is1
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.