How to Group Posts by Year With Jekyll Archives

Skip to the solution

A few years ago, I set up an archive page for this site that displays all of my posts in a single reverse chronological list. To add more context, and make the list easier to scan, I added subheadings that group those posts into sections based on what year they were published. I thought I’d write up how I automated that in my Jekyll templates, and highlight a little gotcha I ran into the other day with the Jekyll Archives plugin.

The Basics

Generating a list of all your posts in Jekyll is straightforward enough. Jekyll has a global site.posts variable you can use in any template. It lets you retrieve and loop through posts like so:

{% for post in site.posts %}
<article>
        <h1><a href="{{ post.url }}">{{ post.title }}</a></h1>
        <time datetime="{{ post.date | date: "%Y-%m-%d" }}">
                {{ post.date | date_to_string }}
        </time>
        {{ post.content }}
</article>
{% endfor %}

What Jekyll doesn’t provide is a built-in way to chunk the results into groups based on their date. You have to figure that out on your own.

You could filter the posts manually, but you’ll have to add specific code for each year. That also means you’ll have to update your templates every time January 1st rolls around if you want to keep things up to date.

That’s not much of an inconvenience for a personal site, but I’m a big believer in making computers do the computer stuff. A few years ago I found a great tip on Stack Overflow on how to automate this with a little restructuring of that loop.

To achieve what we want, we compare the year of the post in the current iteration of the loop to the year of the previous one. If they don’t match, you’ve hit a new year. At that point you can insert a subheading or whatever HTML you like to create your group. It looks something like this:

{% for post in site.posts %}

        {% capture year_of_current_post %}
        {{ post.date | date: "%Y" }}
        {% endcapture %}

        {% capture year_of_previous_post %}
        {{ post.previous.date | date: "%Y" }}
        {% endcapture %}

        {% if forloop.first %}
        <h2>{{ year_of_current_post }}</h2>
        <ul>
        {% endif %}

                <li><a href="{{ post.url }}">{{ post.title }}</a></li>

        {% if forloop.last %}
        </ul>
        {% else %}
        {% if year_of_current_post != year_of_previous_post %}
        </ul>

        <h2>{{ year_of_previous_post }}</h2>
        <ul>
        {% endif %}
        {% endif %}

{% endfor %}

The tricky bit is getting the order of the opening and closing HTML elements right, since that’s based on the conditions inside the loop. I suggest starting with the simplest possible markup if you need to get your bearings, then build on top of that as you get more comfortable with the logic.

Enter Jekyll Archives

The other day I decided these subheadings would be useful on the other archive pages on my site, too.

I use the Jekyll Archives plugin to build those. While my main archive page uses Jekyll’s built-in tools, Jekyll Archives provides more options for automating the creation of tag, category, and date-based collections.

After copying the basic loop above into the template for one of these pages, I noticed something screwy. I was missing the subheadings for some years but not others.

To debug this I made the template render the values for year_of_current_post and year_of_previous_post directly to the page, where I could see them. That’s when I had a little lightbulb moment.

A screenshot of links on an archive page with year variables printed out
These two posts are from different years. But they’re saying the year of the post that came before them is the same as the one they were published in.

Jekyll post objects come with some useful information built in, including properties that tell you what posts come before and after them in the overall timeline of your site. You can see how I’m using those built-in properties in the code example above to retrieve the year of the previous post relative to the current one. (That’s the post.previous.date bit.)

That works fine on the main archive page, where I’m showing all posts in chronological order. But the posts on these archive pages are a subset of all of my posts. The post that comes next in the photo category or the design tag won’t necessarily be the next post in the overall timeline of the blog. In this context, the built-in post.next and post.previous properties are pointing to the wrong thing. Here’s a quick visualization of the issue:

Jekyll post set example graphic

Imagine the upper row represents all the posts on your Jekyll site. The highlighted ones are the posts with a “design” tag. When you add those posts to their own list and ask post 9 who its next sibling is, it will still tell you post 10. But what we were expecting was 14.

Fortunately, Jekyll Archives provides its own page.posts array. Using the loop, we can step through the items in that array to get what we need. It’s similar to the loop above, but with a slight tweak to the variables at the beginning:

{% for post in page.posts %}

        {% capture year_of_current_post %}
        {{ post.date | date: "%Y" }}
        {% endcapture %}

        {% capture year_of_previous_post_in_set %}
        {{ page.posts[forloop.index].date | date: "%Y" }}
        {% endcapture %}

        {% if forloop.first %}
        <h2>{{ year_of_current_post }}</h2>
        <ul>
        {% endif %}

                <li><a href="{{ post.url }}">{{ post.title }}</a></li>

        {% if forloop.last %}
        </ul>
        {% else %}
        {% if year_of_current_post != year_of_previous_post_in_set %}
        </ul>

        <h2>{{ year_of_previous_post_in_set }}</h2>
        <ul>
        {% endif %}
        {% endif %}

{% endfor %}

Instead of grabbing the date of the previous post with its built-in post.previous property, we use the current index of the loop to pluck the right post out of Jekyll Archive’s page.posts array and filter out the date. That’s this bit:

{% capture year_of_previous_post_in_set %}
{{ page.posts[forloop.index].date | date: "%Y" }}
{% endcapture %}

Then we use the resulting year_of_previous_post_in_set variable everywhere we were using year_of_previous_post in the setup above. And with that, we’ve got an accurate way to detect differences in publication year between posts in archive sets, and the hook we need to create those subheadings.