How I Create an Article Series in Eleventy
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
):
YAML---
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) {
continue;
}
// 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,
date,
});
}
// get the entry for this series
const existing = mapping.get(series);
// add the current post to the list
existing.posts.push(post.url);
// 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):
Pug---
pagination:
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
main.series-page(aria-labelledby="title")
h1#title= series.title
p.toc-welcome
if series.description != undefined
= series.description
else
| All articles in the #{series.title} series on #[a(href="/") #[b= metadata.title]]:
- const posts = collections.posts.filter((p) => p.data.series === series.title);
+posts(posts)
+blog-info()
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
nav.post-nav
- 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:
JSON{
"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:
YAML---
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.
- 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 theposts
field while creating the collection, but I would rather keep that field simple, since it’s easy to correlate it with the objects.↩
Automatically Generated Social Media Images with HTML, CSS, Eleventy & Puppeteer