55

I'm rewriting my blog to use Jekyll. Jekyll uses the Liquid templating language so it makes it a little more difficult to learn how to customize.

I'd like to group my list of blog posts by year. How would I write the Liquid code to be able to do this?

{% for post in site.posts %}
  <li><!-- display post year here (but only once, per year) --></li>
  <li>
    <a href="{{ post.url }}">{{ post.title }}</a>
  </li>
{% endfor %}
Andrew
  • 227,796
  • 193
  • 515
  • 708
  • 1
    It involves or involved capturing and is really horrible. http://mikerowecode.com/2010/08/jekyll_archives_grouped_by_year.html – Ry- Sep 30 '13 at 04:09

8 Answers8

65

It can be done with much, much less Liquid code than in the existing answers:

{% for post in site.posts %}
  {% assign currentdate = post.date | date: "%Y" %}
  {% if currentdate != date %}
    <li id="y{{currentdate}}">{{ currentdate }}</li>
    {% assign date = currentdate %} 
  {% endif %}
    <li><a href="{{ post.url }}">{{ post.title }}</a></li>
{% endfor %}

This will return exactly the HTML specified in your question:

<li id="y2013">2013</li>
<li><a href="/2013/01/01/foo/">foo</a></li>
<li id="y2012">2012</li>
<li><a href="/2012/02/01/bar/">bar</a></li>
<li><a href="/2012/01/01/baz/">baz</a></li>

However, this is not the optimal solution, because the year numbers are "only" list items as well.
It's not much more Liquid code to put the year into a headline and to begin a new <ul> for each year's posts:

{% for post in site.posts %}
  {% assign currentdate = post.date | date: "%Y" %}
  {% if currentdate != date %}
    {% unless forloop.first %}</ul>{% endunless %}
    <h1 id="y{{post.date | date: "%Y"}}">{{ currentdate }}</h1>
    <ul>
    {% assign date = currentdate %}
  {% endif %}
    <li><a href="{{ post.url }}">{{ post.title }}</a></li>
  {% if forloop.last %}</ul>{% endif %}
{% endfor %}

The generated HTML:

<h1 id="y2013">2013</h1>
<ul>
<li><a href="/2013/01/01/foo/">foo</a></li>
</ul>
<h1 id="y2012">2012</h1>
<ul>
<li><a href="/2012/02/01/bar/">bar</a></li>
<li><a href="/2012/01/01/baz/">baz</a></li>
</ul>

You can also group by month and year instead (so that the headlines are February 2012, January 2012 and so on).

To do this, you just need to replace date: "%Y" (in the second line of both above examples) by date: "%B %Y".
(%B is the full month name, see the documentation)

TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
Christian Specht
  • 35,843
  • 15
  • 128
  • 182
  • This is great, thank you! One note is that you don't create the final for the last group. So, after the for loop, you'll want to do something like {% if site.posts.size != 0 %}{% endif %} after the for loop. – spitzanator Jan 11 '14 at 20:42
  • 1
    Or right before the loop ends, {% if forloop.last %}{% endif %}. – lfk May 12 '14 at 03:25
  • I'd just like to add that the other solution didn't work for me as my posts aren't necessarily sorted by date, but that this one did! Thank you! – counterbeing Mar 10 '15 at 01:09
  • This is a great post. I managed to adapt it to my situation. I am confused about how it works though - could anyone update this answer with comments in the Liquid code? – joshreesjones Sep 07 '15 at 21:43
  • Is it possible to display the number of posts besides year and month? – Robur_131 May 17 '20 at 20:52
40

These previous solutions are fantastic but luckily in late 2016, Jekyll added a group_by_exp filter that can do this much more cleanly.

{% assign postsByYear =
    site.posts | group_by_exp:"post", "post.date | date: '%Y'" %}
{% for year in postsByYear %}
  <h1>{{ year.name }}</h1>
    <ul>
      {% for post in year.items %}
        <li><a href="{{ post.url }}">{{ post.title }}-{{ post.date }}</a></li>
      {% endfor %}
    </ul>
{% endfor %}

Documentation can be found on the Jekyll Templates page.

Trevor
  • 716
  • 6
  • 9
  • Is it possible to do this with site.data rather than posts? For example I have 10 sets of data all with a data variable which fall within 3 dates that I want to loop like this. Or is it only possible with nested liquid loop? Thanks! – Rhys May 21 '17 at 09:24
  • Yes, you could group any array using the same technique: {% assign groupedData = site.data | group_by_exp: "data", "data.yourVariable" %} – Trevor Aug 04 '17 at 03:10
  • 1
    Thanks for this great answer. If you're on a newer version of Jekyll, this is definitely the route to go. – coryetzkorn Apr 28 '18 at 01:49
  • Thanks for the answer. The doc can be found here: https://jekyllrb.com/docs/liquid/filters/#group-by-expression – yo1995 Dec 22 '22 at 03:22
36

If you want to break it down by year, here's the code:

{% for post in site.posts  %}
    {% capture this_year %}{{ post.date | date: "%Y" }}{% endcapture %}
    {% capture next_year %}{{ post.previous.date | date: "%Y" }}{% endcapture %}

    {% if forloop.first %}
    <h2 id="{{ this_year }}-ref">{{this_year}}</h2>
    <ul>
    {% endif %}

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

    {% if forloop.last %}
    </ul>
    {% else %}
        {% if this_year != next_year %}
        </ul>
        <h2 id="{{ next_year }}-ref">{{next_year}}</h2>
        <ul>
        {% endif %}
    {% endif %}
{% endfor %}

If you want to break it down to year and months it can be achieved like this:

{% for post in site.posts  %}
    {% capture this_year %}{{ post.date | date: "%Y" }}{% endcapture %}
    {% capture this_month %}{{ post.date | date: "%B" }}{% endcapture %}
    {% capture next_year %}{{ post.previous.date | date: "%Y" }}{% endcapture %}
    {% capture next_month %}{{ post.previous.date | date: "%B" }}{% endcapture %}

    {% if forloop.first %}
    <h2 id="{{ this_year }}-ref">{{this_year}}</h2>
    <h3 id="{{ this_year }}-{{ this_month }}-ref">{{ this_month }}</h3>
    <ul>
    {% endif %}

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

    {% if forloop.last %}
    </ul>
    {% else %}
        {% if this_year != next_year %}
        </ul>
        <h2 id="{{ next_year }}-ref">{{next_year}}</h2>
        <h3 id="{{ next_year }}-{{ next_month }}-ref">{{ next_month }}</h3>
        <ul>
        {% else %}    
            {% if this_month != next_month %}
            </ul>
            <h3 id="{{ this_year }}-{{ next_month }}-ref">{{ next_month }}</h3>
            <ul>
            {% endif %}
        {% endif %}
    {% endif %}
{% endfor %}

It is only a matter of where do you make the cut on the loop.

Rafa Garrido
  • 710
  • 6
  • 7
10

Some solutions above are very complex but then as @Trevor pointed out that we can levarage Jekyll's group_by_exp filter. Also I liked the solution but what I needed was grouped by Year and then inside that list grouped by Month. So, I tweaked it a little bit.

{% assign postsByYear = site.posts | group_by_exp:"post", "post.date | date: '%Y'" %}
    {% for year in postsByYear %}
      <h1>{{ year.name }}</h1>
      {% assign postsByMonth = year.items | group_by_exp:"post", "post.date | date: '%B'" %}

      {% for month in postsByMonth %}
        <h2>{{ month.name }}</h2>
        <ul>
          {% for post in month.items %}
            <li><a href="{{ post.url }}">{{ post.title }}-{{ post.date }}</a></li>
          {% endfor %}
        </ul>

      {% endfor %}
    {% endfor %}
Ankit R Gadiya
  • 449
  • 1
  • 7
  • 14
6

Variation of Ankit R Gadiya's answer. The inner for loop was displaying the html code. I needed to de-indent it to get it to properly render the markup. I also added the post's excerpt:

{% assign postsByYear = site.posts | group_by_exp:"post", "post.date | date: '%Y'" %}
{% for year in postsByYear %}
  <h1>{{ year.name }}</h1>
  {% assign postsByMonth = year.items | group_by_exp:"post", "post.date | date: '%B'" %}

{% for month in postsByMonth %}
<h2>{{ month.name }}</h2>
<ul>
  {% for post in month.items %}
    <li>
      <a href="{{ post.url }}">{{ post.title }}</a>
      <br>{{ post.excerpt }}
    </li>
  {% endfor %}
</ul>

{% endfor %}
{% endfor %}

Example:

example

Scott Gardner
  • 8,603
  • 1
  • 44
  • 36
2

Try:

{% for post in site.posts  %}
  {% capture this_year %}{{ post.date | date: "%Y" }}{% endcapture %}

  {% if forloop.first %}
  <h2 id="{{ this_year }}-ref">{{this_year}}</h2>
  <ul class="posts">
  {% else %}
      {% if this_year != last_year %}
      </ul>
      <h2 id="{{ this_year }}-ref">{{this_year}}</h2>
      <ul class="posts">
      {% endif %}
  {% endif %}

    <li>
      <span class="post-date">{{ post.date | date_to_string }} &raquo;</span>
      <a href="{{ post.url }}">{{ post.title }}</a>
    </li>

  {% if forloop.last %}
    </ul>
  {% endif %}

  {% capture last_year %}{{ this_year }}{% endcapture %}
{% endfor %}
trans
  • 1,411
  • 1
  • 13
  • 13
2
<ul>
  {% for post in site.posts %}
      {% assign year = post.date | date: "%Y" %}

      {% if year != prev_year %}
        <h3>{{year}}</h3>
      {% endif %}

      <li>
        <span>{{ post.date | date: "%B %e, %Y" }}</span>
        <a href="{{ post.url }}">{{ post.title }}</a>
      </li>
      {% assign prev_year = year %}
  {% endfor %}
</ul>
Kamran
  • 843
  • 1
  • 8
  • 19
2

Did not much like the other answer so here's an alternative for you. Basic logic: Display year/month only if it "new":

{% assign var currentYear = 0 %}
{% assign var currentMonth = 0 %}
{% for post in site.posts  %}
{% capture year %}{{ post.date | date: "%Y" }}{% endcapture %}
{% capture month %}{{ post.date | date: "%B" }}{% endcapture %}

{% if currentYear != year %}
<div>
  <h2>{{ year }}</h2>
</div>
{% assign var currentYear = year %}
{% endif %}
{% if currentMonth != month %}
<div>
  <h3>{{ month }}</h3>
</div>
{% assign var currentMonth = month %}
{% endif %}
<p>{{ post.title }}</p>
{% endfor %}
cazaimi
  • 196
  • 11