I discussed this on the Eleventy Discord and thought I’d repeat it here for reference. (Besides, I haven’t published anything except Thoughts & Spoilers for a while. I needed a change of pace.)

Articles on A Place For My Head can belong to a ‘series’. For example, this article is part of the ‘Colophon: Finding A Place For My Head’ series, which gets a page of its own. Every article in the series then gets a link to that page at the end, like so:

To do this, I first add a series key to the front matter of all the constituent articles (thank you to Mr Peach on Discord for noticing that I had omitted the seriesDescription):

title: "How I Create Series of Articles in Eleventy"
series: "Colophon: Finding A Place For My Head"
seriesDescription: "A complete history of the when, how, and why of this site."

Which enables me to group them by creating a custom series collection in my Eleventy configuration:

JavaScripteleventyConfig.addCollection("series", (collection) => {
  // get all posts in chronological order
  const posts = collection.getSortedByDate();

  // this will store the mapping from series to lists of posts; it can be a
  // regular object if you prefer
  const mapping = new Map();

  // loop over the posts
  for (const post of posts) {
    // get any series data for the current post, and store the date for later
    const { series, seriesDescription, date } = post.data;

    // ignore anything with no series data
    if (series === undefined) {

    // if we haven’t seen this series before, create a new entry in the mapping
    // (i.e. take the description from the first post we encounter)
    if (!mapping.has(series)) {
      mapping.set(series, {
        posts: [],
        description: seriesDescription,

    // get the entry for this series
    const existing = mapping.get(series);

    // add the current post to the list

    // update the date so we always have the date from the latest post
    existing.date = date;

  // now to collect series containing more than one post as an array that
  // Eleventy can paginate
  const normalized = [];

  // loop over the mapping (`k` is the series title)
  for (const [k, { posts, description, date }] of mapping.entries()) {
    if (posts.length > 1) {
      // add any series with multiple posts to the new array
      normalized.push({ title: k, posts, description, date });

  // return the array
  return normalized;

Now I have an array containing all known series—excluding those consisting of only one article—on which I can use Eleventy’s pagination functionality in a new template (i.e. a new file with a name like series.pug—it can be called anything and placed anywhere in the input directory, since permalink in the front matter will determine the final URI):

  data: collections.series
  size: 1
  alias: series
permalink: "| /series/#{metadata.slug(series.title)}/"
dynamicPermalink: true

extends ../layouts/index.pug

include ../layouts/mixins/toc.pug
include ../layouts/mixins/blog-info.pug

block content
    h1#title= series.title
      if series.description != undefined
        = series.description
        | All articles in the #{series.title} series on #[a(href="/") #[b= metadata.title]]:
    - const posts = collections.posts.filter((p) => p.data.series === series.title);

My HTML templating language of choice is Pug. If you’re not familiar with the syntax, I’m creating a page for each series at /series/series-slug/ (where series-slug is a URI-friendly version of the series title) which displays its title, description (or a default), and posts (in chronological order). I access those details through the series alias I specified in the pagination configuration.[1]

Finally, I add any available series data to the template for the articles themselves:

Pug- const seriesData = collections.series.find(({ title }) => title === series) || {};
- const seriesPosts = seriesData.posts || [];
if seriesPosts.length > 0
    - const thisNumber = seriesPosts.findIndex((url) => url === page.url) + 1;
    p.post-series This is the #[!= metadata.niceOrdinal(thisNumber, true)] entry in the #[a.post-series-title(href=`/series/${metadata.slug(series)}/` itemprop="isPartOf")= series] series.

This code loops over the array I created earlier to find the series data corresponding to the current article and uses a few homegrown helper functions to add a pleasingly-formatted link to the series page if found. Et voilà ! Eleventy will now generate series pages and links based on nothing more than a single key in the front matter.

Reducing the repetition

Every article in a series currently needs the full series title written out identically. If I wrote A Tale of X & Y in one article but A tale of x & y in another, I’d end up with two separate series. I might even accidentally enter different descriptions in each file, though only the first one would be used. To avoid those scenarios, I could create a JSON global data file named something like seriesInformation.json to map arbitrary identifiers to series metadata:

  "an-id-for-a-series": {
    "title": "A Series of Identifiable Events",
    "description": "A description of the series"
  "some-other-id": {
    "title": "Some Other Series",
    "description": "Not the same series as before"
  // etc.

Then my front matter could reference those identifiers instead of the titles:

title: "Some Article"
series: "an-id-for-a-series"

And my series template could retrieve the data corresponding to that series from the seriesInformation global variable. The reason why I don’t do this is that all my series so far are automatically generated based on directory structure (by means of directory data files), so there’s no repetition yet anyway.

Many thanks to bnb on Discord for bringing some ambiguous wording to my attention.

  1. If I only wanted the URIs of the posts, I could use series.posts. However, because I need the post objects in order to display all the relevant information, I instead search Eleventy’s builtin list of posts to get the ones in the series. Another alternative would be to put the post objects directly in the posts field while creating the collection, but I would rather keep that field simple, since it’s easy to correlate it with the objects.

Next in series: (#16 in Colophon: Finding A Place For My Head)