Eleventy - Group posts by year

A series of posts are grouped by the year 2024 in descending order. The block has a black square bracket as an outline to the left. The first post has a date of 2024 Feb 14 and the second posts has a date of 2024 Fed 12. The title of the image says 'group posts by year. This is cast againt a soft purple background with the eleventy possum floating beside it on a balloon.

A common organisation pattern for a blog or an “archives” page is to group posts by year.

zachleat.com archive page shows Zach Leatherman's archived posts grouped by year in descending order
Zach Leatherman has an "Archives" page that has his posts grouped by year from most recent to oldest (descending order)

Let’s create a blog with posts grouped by year in descending order (newest to oldest). The page will look like this:

a blog page with  5 posts grouped by year in descending order, most recent to oldest

This is actually tricky to pull off! Why is that?

The crux of the problem

If you are using Nunjucks as a template language, there is a groupby filter. If you add a year property to your posts collections through computed data, then you can use the groupBy filter in Nunjucks to get the grouped dataset you require.

The groupby filter will return an Objectlike this:

JSON
{
"2022" : [{ fileSlug: "post1" }],
"2023" : [{ fileSlug: "post2" }, { fileSlug: "post3" }, { fileSlug: "post4" }],
"2024" : [{ fileSlug: "post5" }]
}

The properties (keys) are sorted in ascending order (oldest to newest). If you are happy with that, then your page template for your blog page is pretty straightforward:

Nunjucks
<section class="blog">

{% for year, posts in collections.posts | groupby("data.year") %}
<h2>{{ year }}</h2>
<ol>
{% for post in posts %}
<li class="post">
<a href="{{ post.url | url }}">{{ post.data.title }}</a>
</li>
{% endfor %}
</ol>
{% endfor %}

</section>

However, I think that most of the time you want the data sorted in the opposite order. This is where things get tricky!

The crux of the problem is that you need both the properties (years) and the values (array of yearly posts) sorted in reverse order. Doing this on an Object is cumbersome and error-prone.

We want this:

JSON
{
"2024" : [{ fileSlug: "post5" }],
"2023" : [{ fileSlug: "post4" }, { fileSlug: "post3" }, { fileSlug: "post2" }],
"2022" : [{ fileSlug: "post1" }]
}

If you want to do the sorting in Nunjucks, you might expect that using the reverse filter on both the property and value will give you the desired result – it does not! Chris Kirk Nielsen describes this issue in detail and how to get around it in Nunjucks in his tutorial. Chris explains the code really well, but it requires too much explanation in my opinion! It is probably better to go in another direction.

I suggest using a Map instead and doing the work in JavaScript. It is more explicit and more efficient.

A Map is preferable over an Object in many cases because:

You can read Objects vs Maps on MDN for a more detailed comparison.

Long story short, a Map will save you grief if you use it!

The code

There is a short and elegant solution to this if you are happy to stick with Node v21+. If you are running an older version of Node, I outline an alternative version that will work across all versions of Node.

Here is an abbreviated outline of the project:

│
├─ content/   
│  ├─ posts/
│  │  ├─ post4/
│  │  │  ├─ post4.md
│  │  ├─ 2024-01-01-post5.md
│  │  ├─ post1.md
│  │  ├─ post2.md
│  │  ├─ post3.md
├─ _includes/ 
│  ├─ blog-asc.njk
│  ├─ blog-desc.njk
├─ utilities.js

Our posts files live in content/posts and we use a mixture of styles to test them out!

The blog-desc.njk template file will create our blog page with our grouped posts in descending order.

The Node v21+ version - use native Map.groupBy

There is a relatively new builtin function to do the dirty work for us – Map.groupBy. This function is part of the Array grouping ECMAScript proposal and made it into browsers and Node last year.

Add a collection in the elventy configuration file

We will create a new collection in our eleventy config (eleventy.config.js). I will call it postsByYearDescVersion21 to be super explicit for this demo. I use thegetFilteredByGlob() function to create an array of all posts, which I will sort and group.

Here is the code:

Javascript
//eleventy.config.js
module.exports = function (config) {

config.addCollection("postsByYearDescVersion21", (collection) => {
let posts = collection
.getFilteredByGlob("content/posts/**/*.md")
.sort((a, b) => b.date - a.date);

const postsByYear = Map.groupBy(posts, ({ date }) =>
new Date(date).getFullYear()
);

return postsByYear;
});

// other stuff
};

You can see that we sort the posts array before we use Map.groupBy. This ensures that the keys and values will be sorted as we expect. It produces a Map object as below:

Javascript
{
2024 => [
{
fileSlug: 'post5',
date: 2024-01-01T00:00:00.000Z,
}
],
2023 => [
{
fileSlug: 'post4',
date: 2023-09-30T00:00:00.000Z,
},
{
fileSlug: 'post3',
date: 2023-08-24T00:00:00.000Z,
},
{
fileSlug: 'post2',
date: 2023-07-04T00:00:00.000Z,
}
],
2022 => [
{
fileSlug: 'post1',
date: 2022-05-01T00:00:00.000Z
}
]
}

The blog template - blog-desc.njk

In blog-desc.njk, we can loop through our postsByYearDescVersion21 collection and output the grouped posts.

Nunjucks
<section class="blog">
<p>My incredible posts grouped by year.</p>

{% for year, posts in collections.postsByYearDescVersion21 %}
<h2>{{ year }}</h2>

<ol>
{% for post in posts %}
<li class="post">
<h3><a href="{{ post.url }}"">{{ post.data.title }}</a></h3>
<span>|</span>
<time>{{ post.date | simpleDate }}</time>
</li>
{% endfor %}
</ol>
{% endfor %}

</section>

The general Node version - write your own groupBy function

We want the equivalent of the Map.groupBy function that return a Map object. Most utility libraries like lodash return an Object. We will write our own groupBy function!

Create a groupBy function in utilities.js

You can check out the question “Most efficient method to groupby on an array of objects” on StackOveflow for a discussion on this. I have lifted mortb’s answer here, you can go for more efficient but less readable alternative if you wish!

We will add our function to a file called utilities.js to keep things organised.

Javascript
// utilities.js

/**
* @description
* Takes an Array<V>, and a grouping function,
* and returns a Map of the array grouped by the grouping function.
*
* @param list An array of type V.
* @param keyGetter A Function that takes the the Array type V as an input, and returns a value of type K.
* K is generally intended to be a property key of V.
*
* @returns Map of the array grouped by the grouping function.
*/

function groupBy(list, keyGetter) {
const map = new Map();

list.forEach((item) => {
const key = keyGetter(item);
const collection = map.get(key);

if (!collection) {
map.set(key, [item]);
} else {
collection.push(item);
}
});

return map;
}

Create a collection in the eleventy configuration file

Again, we will create a new collection in our eleventy config. This time we will call it postsByYearDesc. We import (require) our groupBy function and use it on the sorted posts array.

Javascript
//eleventy.config.js
const { groupBy } = require("./utilities");

module.exports = function (config) {

config.addCollection("postsByYearDesc", (collection) => {
let posts = collection
.getFilteredByGlob("content/posts/**/*.md")
.sort((a, b) => b.date - a.date);

const postsByYear = groupBy(posts, (post) =>
new Date(post.date).getFullYear()
);

return postsByYear;
});

//other stuff
};

The blog template - blog-desc.njk

Our blog-desc.njk template is identical to the previous implementation, just the collection name is different.

Nunjucks
<section class="blog">
<p>My incredible posts grouped by year.</p>

{% for year, posts in collections.postsByYearDesc %}
<h2>{{ year }}</h2>

<ol>
{% for post in posts %}
<li class="post">
<h3><a href="{{ post.url }}"">{{ post.data.title }}</a></h3>
<span>|</span>
<time>{{ post.date | simpleDate }}</time>
</li>
{% endfor %}
</ol>
{% endfor %}

</section>

Source code

You will find the code in the group-post-by-year subfolder of the https://github.com/robole/eleventy-tutorials repo. Both versions are included. The general Node version is the default.

Can you give the repo a star to indicate that this was a worthwhile tutorial please? 🌟

Final words

In this tutorial, I demonstrated how to group posts by year with a Map in JavaScript. The code is more efficient and more comprehensible than the alternatives I have seen! Whatever templating language you use, I recommend following this pattern.

Tagged