Creating an Archive” Page for Entries

You can create an “Archive” page, with entries grouped by month/year using Craft’s group filter:

{% set allEntries = craft.entries()
  .section('blog')
  .all() %}

{% for date, entries in allEntries|group("postDate|date('F Y')") %}
  <h2>{{ date }}</h2>
  <ul>
    {% for entry in entries %}
      <li>{{ entry.getLink() }}</li>
    {% endfor %}
  </ul>
{% endfor %}

The group filter takes an array of items—entries, in this case—and groups them based on a common value. In this case we’re grouping them by a common Post Date format, F Y (month name + four-digit year), using the date filter.

Grouping by Year and then Month #

In the previous example we were only creating one level of headings: a combination of the month name and the year. If you would prefer to group entries by year first, and then by month, you can do that too using a nested group filter:

{% set allEntries = craft.entries()
  .section('blog')
  .all() %}

{% for year, entriesInYear in allEntries|group("postDate|date('Y')") %}
  <h2>{{ year }}</h2>
  <ul>
    {% for month, entriesInMonth in entriesInYear|group("postDate|date('F')") %}
      <h3>{{ month }}</h3>
      <ul>
        {% for entry in entriesInMonth %}
          <li>{{ entry.getLink() }}</li>
        {% endfor %}
      </ul>
    {% endfor %}
  </ul>
{% endfor %}

Paginating the Archive #

If you have a ton of entries and feel it makes sense to span it across multiple pages, there are two ways you might want to go about that:

  1. Paginate the entries, so each page gets the same number of resulting entries
  2. Create yearly archive pages

Let’s look at each approach.

Paginating the Entries #

To paginate the entries, we’ll use the paginate tag.

{% set entryQuery = craft.entries()
  .section('blog')
  .limit(100) %}

{% paginate entryQuery as pageEntries %}
  {% for date, entries in pageEntries|group("postDate|date('F Y')") %}
    <h2>{{ date }}</h2>
    <ul>
      {% for entry in entries %}
        <li>{{ entry.getLink() }}</li>
      {% endfor %}
    </ul>
  {% endfor %}
    
  {% if paginate.prevUrl %}
      <a href="{{ paginate.prevUrl }}">Previous Page</a>
  {% endif %}

  {% if paginate.nextUrl %}
      <a href="{{ paginate.nextUrl }}">Next Page</a>
  {% endif %}
{% endpaginate %}

Yearly Archive Pages #

If you’d prefer to dedicate each page to a full year of entries, you must first create a new route with the following settings:

  • Set the URI pattern to whatever you want the URI to look like leading up to the year (e..g blog/archive/), and then click on the year token.
  • Set the Template setting to the path to your archive template.

If your template is publicly accessible (its name doesn’t start with an underscore), the first thing it should do is check to make sure that year has been defined. If it’s not, you have two choices on how to react:

  1. Just default to the current year:
  {% if year is not defined %}
    {% set year = now|date('Y') %}
  {% endif %}
  1. Redirect to the current year’s URL:
  {% if year is not defined %}
    {% redirect 'blog/archive/' ~ now|date('Y') %}
  {% endif %}

Now let’s output the entries in the year, grouped by month:

<h1>{{ year }}</h1>

{% set entriesInYear = craft.entries()
  .section('blog')
  .after(year)
  .before(year+1)
  .all() %}

{% for month, entries in entriesInYear|group("postDate|date('F')") %}
  <h2>{{ month }}</h2>
  <ul>
    {% for entry in entries %}
      <li>{{ entry.getLink() }}</li>
    {% endfor %}
  </ul>
{% endfor %}

To make this example complete, we’ll need to work in a custom navigation. Chances are you’re going to want to display all years between now and whenever the first entry was published. To do that, first we must fetch the first entry ever:

{% set firstEntryEver = craft.entries()
  .section('blog')
  .order('postDate asc')
  .one() %}

Now loop through all of the possible years between then and now:

<ul>
  {% for year in now|date('Y') .. firstEntryEver.postDate|date('Y') %}
    <li><a href="{{ url('blog/archive/' ~ year) }}">{{ year }}</a></li>
  {% endfor %}
</ul>

Applies to Craft CMS 3.