Build an 11ty calendar to list all your posts

I wanted to create a calendar/diary page for all my posts so I could have an overview of my post history. After writing about my writing schedule and making sweeping generalisations about my posting past, I thought it would be nice to create a single page that listed out all my posts by date.

I have previously thought about doing this, but the performance implications of doing it with a dynamic CMS have held me back. With my move to 11ty, the page can be build and rendered as static HTML. What a dream!

Finished Result

If you would like to see what this looks like, you can head to the diary. If you're interested in the code, you can find it on Gitlab with the accompanying data file.

Grouping by year

I originally looked to have my posts listed by year and this Gitlab solution was a great start. However, I wanted to enhance this by month and day too.

I started manipulating the code to duplicate the year functionality with month. However, to get it semantically nested with ol inside the li was turning into a bit of a headache

{% set entryYear = entry.date.getFullYear() %}
{% set entryMonth = entry.date.getMonth() %}
{% if currentYear != entryYear %}
	<h3>{{ entryYear }}</h3>
	<ul>
		<li>
{% endif %}
{% if currentMonth != entryMonth %}
{% set currentMonth = entryMonth %}
		<h4>{{ entryMonth }}</h4>
		<ul>
			<li>
{% endif %}

Page data & data files

While trying to bend Nunjucks to my will, I was constantly thinking about how much easier this would be if the array was already formatted by year and month for me to loop through.

I considered making a global data file, but as it was for one page, I thought I would keep the data close to the file.

Originally I was developing with the JavaScript code in the Front Matter:

---js
{
	title: "Diary",
	description: "All of my posts in one place, by year and month",
	layout: "page.njk",
	diary: function() {
		// ...
	}
}
---

However, after reviewing the size of the code, I opted to use a directory specific data file by creating a diary.11tydata.js alongside my diary.njk.

This makes the variables declared in here available in the corresponding file (or folder). I've used directory data files for other parts of my site, including applying a draft status to all draft posts.

The collection code

So this is what you are here for, the code.

You can see the final file on Gitlab, but thought I would walk through each bit here.

First step is to declare some date related functions - getting nice month names along with calculating the date ordinal (e.g. 3rd or 19th)

const month_names = Array.from({length: 12}, (e, i) => {
	return new Date(null, i + 1, null).toLocaleDateString("en", {month: "long"});
})

const nth = function(d) {
	if (d > 3 && d < 21) {
		return 'th';
	}
	switch (d % 10) {
		case 1:
			return 'st';
		case 2:
			return 'nd';
		case 3:
			return 'rd';
		default:
			return 'th';
	}
}

Next we export the module as a JavaScript object, with a function keyed as diary, which we will call in our template

module.exports = {
	diary: function() {
		// Code goes here
	 }
}

Rather than walk through each of the bits of code, I've included it below commented:

module.exports = {
	diary: function() {
		// Select the collection we want to loop
		let entries = this.ctx.collections.blog,
			// Create our placeholder array
			output = [];

		// Loop through each of the entries
		for(let item of entries) {
			// Check we have both a date and title
			if(item.data.title && item.date) {
				// Extract the year and month number (Jan = 0)
				let year = item.date.getFullYear(),
					month = item.date.getMonth();

				// If the year hasn't been seen before, make a stub object
				if(!output[year]) {
					output[year] = {
						title: year,
						months: []
					};
				}

				// If the month hasn't been seen before, make a stub object
				// with a nice month name as the title
				if(!output[year].months[month]) {
					output[year].months[month] = {
						title: month_names[month],
						entries: []
					};
				}

				// Add the entry to the keyed year/month array - only add the info we need
				output[year].months[month].entries.push({
					title: item.data.title,
					url: item.url,
					// This is just the date plus ordinal (e.g. 23rd)
					date: item.date.getDate() + nth(item.date.getDate()),
				});
			}
		}

		// Return our array
		return output
			// Reverse the months (most recent first)
			.map(y => {
				y.months.reverse();
				return y;
			})
			// Filter out any null years
			.filter(a => a)
			// Reverse the years (recent first)
			.reverse();
	 }
}

Displaying the posts

With our collection in a loop-able format, we can create several nested loops to output each of the entries. The resulting output is:

  • Year
    • Month
      • Date - Entry 1
      • Date - Entry 2
<ol class="diary">
{% for year in diary() %}
<li>
	<div><h2 id="{{ year.title }}">{{ year.title }}</h2></div>

	<ol>
	{% for month in year.months %}
		<div><h3>{{ month.title }}</h3></div>

		<ol class="diaryEntries">
		{% for entry in month.entries %}
			<li>{{ entry.date }} - <a href="{{ entry.url }}">{{ entry.title }}</a></li>
		{% endfor %}
		</ol>
	{% endfor %}
	</ol>
</li>
{% endfor %}
</ol>

The extra <div> elements are there purely for styling purposes (Flexbox FTW).

View this post on Github

You might also enjoy…

Mike Street

Written by Mike Street

Mike is a CTO and Lead Developer from Brighton, UK. He spends his time writing, cycling and coding. You can find Mike on Mastodon.