Listing year or month only once in blog archive

A few variations of this website ago, I decided I really liked the idea of listing the posts in my blog arranged by year and month. Basically, instead of each entry having the full date (year, month, and day), I want the list of blog posts to look like this:

- 2022
    - May
        - Blog post
        - Blog post
        - Blog post
    - April
        - Blog post
    - March
        - Blog post
- 2021
    - December
        - Blog post
        - Blog post

Rather than creating loops for each year and month, I wanted to be able to just pop the year and month into separate paragraph or header tags, and only actually generate those when required — aka when it's the first time it's being listed, and no other time.

I think there might be a function in WordPress that does this automatically, but getting it to work in another CMS, including Kirby, resulted in some head-scratching at first. I'm writing this here for my future self and anyone else who wants it.

Basically, we need to take advantage of the foreach loop and some extra variables to make this work. For example, to only list the year the first time, we'll create a new variable $posted_year just before the loop, and set it to something like 0. Then, inside the loop, we'll compare that variable to the year of the current post (via another variable $post_year here) — if it does not match (eg. 0 ≠ 2022), then the year is shown. Then, still in the loop, we'll set that new variable to the year of the current post. When the function loops again, the variable is now set as the year of the previous post, so when we test if it matches the current post and it does (2022 == 2022), we just... don't post the year.

<?php
   $posted_year = 0;
   $posted_month = 0;
   foreach($page->children()->flip() as $blogpost):
      $post_year = $blogpost->date()->toDate('Y');
      $post_month = $blogpost->date()->toDate('mY');
      if($post_year != $posted_year):
         $posted_year = $post_year; ?>
         <h2><?= $posted_year ?></h2>
      <?php endif ?>
      <?php if($post_month != $posted_month):
         $posted_month = $post_month; ?>
         <h3><?= $blogpost->date()->toDate('F') ?></h3>
      <?php endif ?>
      <p><span class="date"><?= $blogpost->date()->toDate('d') ?> &mdash;</span> <a href="<?= $blogpost->url() ?>"><?= $blogpost->title() ?></a></p>
   <?php endforeach ?>

Note: for the month variable, make sure it's a unique variable year-over-year, so you never ever have to worry about some months not showing up because they match from the last post even though it's from a different year. I like the month number and year squished together: toDate('mY').

This could very well be how everyone has done this forever, or there might be a simpler way to do it that I have altogether failed to think of, but after several unfruitful Google searches, this is what I came up with and it works a treat.

Join the conversation

Respond on Twitter or on your own website with a webmention and it will show up here. Or, you can fill out some forms:

Available formatting commands

Use Markdown commands or their HTML equivalents to add simple formatting to your comment:

Text markup
*italic*, **bold**, ~~strikethrough~~, `code` and <mark>marked text</mark>.
Lists
- Unordered item 1
- Unordered list item 2
1. Ordered list item 1
2. Ordered list item 2
Quotations
> Quoted text
Code blocks
```
// A simple code block
```
```php
// Some PHP code
phpinfo();
```
Links
[Link text](https://example.com)
Full URLs are automatically converted into links.

Send a webmention

If you've posted a response on your own website, you can manually submit the URL to your post and it will be added here. Your page must contain a link to this post.