Generating pages from data with 11ty

Back to list

Published:

Eleventy or 11ty is a powerful static site generator that can be used to build website. Pretty simple out of the box, it allows some interesting things, like using external data sets like described below!

Context

I needed to be able to generate pages with content based on a data source. That data source could be a file or an API.

Imagine we have an API to fetch product data. For sake of simplicity and as it does not change anything on the process, let's focus on a rather simple structure:

{
"sku": "1232k23",
"name": "Very nice product name",
"slug": "very-nice-product-name",
"image": "/23234-234234-234234-3243423/nice.jpg"
}

The slug could either come from the API as a customizable vanity URL or computed or both.

The goal is to have pre-generated pages for product on an e-commerce site to provide the best site arrival experience. On top of those static page we can add javascript component using your favorite framework (like React or Svelte) or even simple-stupid javascript to enable e.g. add-to-cart operation and content tailoring. We won't talk about that in this post.

Project structure

I like when configuration file are easy to read. So no configuration file is better, no complexity although you need to know where to put stuff. I struggled some time because of that and the fact that some directories in the configuration are relative to the project root and some are relative to the input directory.

root
- site/
  - _includes/
  - _data/

_includes contains the templates and _data will contain the file for the data fetching.

Note that this setup requires the option --input site to be passed to 11ty.

The data file

This is the first part of the magic of 11ty: data fetching. In the _data directory, let's create a file called products.js. The name is important as it will be the name of the data set in the static generation.

That _data/products.js must export an object (or array) or a function return the data or it can even be a function returning a Promise. This opens the possibility of retrieving data on the API.

module.exports = function() {
return new Promise((resolve, reject) => {
fetch(API_PATH)
.then(json => response.json())
.then(data => resolve(data))
.catch(e => reject(e));
});
};

This is auto-magically ran at runtime and made available to the content file.

A real example will add some data preparation, like generating the slug if the vanity name is missing or aggregate data from another service like prices and availability. Caution, you should avoid that if your price and availability changes more frequently than the generation period.

The content file

This is the "hacky" part.

It seems that 11ty is expecting this file to perform the link between the data and the template.

We create a file named product.md in the site directory. The extension is not very important as long as the frontmatter is supported.

There is probably a way to do this through configuration.

The file should contain something like this:

---
layout: product
pagination:
data: products
size: 1
alias: product
---

Let's go through this line by line.

  • layout: product -- points 11ty to the template we want to use. See below.
  • pagination object -- with data we are telling 11ty to consider the products data set and with size, to create a paginated version grouped by one. This called the template for EACH entry in the product array!
  • alias: product -- sets the name of the item in the template

The real magic is the size: 1. Usually you would use that to generate paginated groups like the mix page of this site. We could also create the products list pages by changing this value with some other value (e.g. 10).

The last piece we need is the template.

The template

In the _includes folder, we crate a file named product.liquid. You can choose any of the supported templating engine.

A simple-stupid template:

---
permalink: /product/{\{ product.slug }}/index.html
---

<!doctype html>
<html lang="en">
<title>{\{ product.name }}</title>
<h1>{\{ product.name }}</h1>
<img src="{\{ product.image }}" alt="">
</html>

Note that the {\{ are to be replaced by {{.

This template will be called for each product of the products data-set. It will obviously grow in complexity to become a real product page.

Then we run the 11ty executable npx @11ty/eleventy --input site, the result is a new directory _site in which the files were generated.

Discussion

Obviously we also need to generate product list pages. But we will leave that for now.

At the end, we have the following structure:

root
- site/
  - _includes/
      product.liquid
  - _data/
      products.js
  products.md
- _site/
  - product/
    - very-nice-product-name/
      index.html

We are generating all the pages upfront with this setup. In order to make sure new product can be added to the site, we might define a generation period that satisfies the needs for new product rollout.

The process is the following:

  1. fetch data from the products endpoint
  2. structure data to be consumed in the templating
  3. generate a page per product
  4. rsync the content to the web-server so that only changed entries are updated.

After fetching the data, which depends mostly on the endpoint's performance, 11ty is quite fast to generate the pages; <20ms per page, with a catalog of 200 products it results in something like 4 seconds generation time. The rsync might be the biggest time consumer.

Obviously the web server must be configured properly to make the pages cacheable.

We might be tempted to add a hash to the filename based on relevant data so that we can set a "infinite" cache duration. Maybe for a future update.

This solution is a nice and simple solution to reduce customer browsing stress and your server can focus on providing value-adding operation: handling cart updates, checkout or dynamic personalization.