Last week, during the week, I soft-launched the yearly archives. This is a feature well known to anyone who has used WordPress before. WordPress produces them by default, and if my recollection is correct, it also builds monthly archives. Of course, monthly archives don’t make much sense for this blog, as there are about 4–5 posts at most.
But yearly archives have always looked cool. I do have the paginated archives, but as suggested by the URL, there should be archives that can be reached by hacking the URL. This is now possible.
From a design standpoint, the best case I have ever seen is the annual archive by the Swiss online newspaper Republik. They have an annual archive called month-by-month that is very well done. Not only are the visuals for every single article stunning, but the way they describe and summarise it at the top is also really cool1.

Screenshot of the yearly archives by Republik.
So, inspired by great minds and good ideas, I set out to create the yearly archives. Pretty quickly, I abandoned the idea of having cool summaries for every month. Partly because I don’t have that many blog posts as a source of inspiration, but also because I’m just one person, not a team of professionals.
However, setting it up from a technical perspective was a bit harder than expected.
Going astray on the technical path
First, I tried to set up something akin to a subsection and put my pages in there. This broke all kinds of other stuff, and generally, I don’t think it was a great idea. The idea was cool, but then I had problems with the subpages, and mostly, it was just hurting myself. And this is—after all—just a hobby. So I would rather not spend too much time on it.
If I’m going to spend the time, I also want to try out something new.
Content adapters
Then I read somewhere about a new feature that has been around in Hugo since version v0.126.0, the content adapters. Basically, content adapters can create pages from some data, for example by loading data or something similar. I thought this could be it; this could be the solution to my admittedly made-up issue.
I gave it a go, but I stumbled pretty quickly. The pages are not yet available at the time of building, so I couldn’t loop through my pages and find the first and last date of a page created. But anyway, perfection is the enemy of good enough, and I just decided to go as simple as I could: start with 2024, the year I published the first blog post here, and then simply go to the current year (which at the time of writing is 2025).
Technicals
The implementation of the content template is actually straightforward. It loops through the years, creates the page with the title of the year, and adds the year as a parameter as well.
{{ $currentYear := now.Year }}
{{ $startYear := 2024 }}
{{ range $year := seq $startYear $currentYear }}
{{ $firstDayOfYear := time.AsTime (printf "%d-01-01T00:00:00" $year) }}
{{ $lastDayOfYear := time.AsTime (printf "%d-12-31T23:59:59" $year) }}
{{ $pageDate := $lastDayOfYear }}
{{ if eq $year $currentYear }}
{{ $pageDate = now }}
{{ end }}
{{ $page := dict
"kind" "page"
"path" (printf "%d" $year)
"title" (printf "%d" $year)
"params" (dict "year" $year)
"dates" (dict
"lastmod" $pageDate
"publishDate" $firstDayOfYear)
}}
{{ $.AddPage $page }}
{{ end }}
In the template, I then read the parameter I have set in the content adapter, find the correct posts, and simply loop through them. Simple and easy.
{{ $currentYear := .Params.year }}
{{ $postsInYear := where (where .Site.RegularPages "Section" "posts") "Date.Year" $currentYear }}
...
{{ range $postsInYear.ByDate.Reverse }}
...
{{ end }}
I also did a little hack to be able to display a bit of navigation for the years, simply adding the following to my template:
<div class="pagination">
{{ with .Page.PrevInSection }}
<a href="{{ .RelPermalink }}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
</svg>
{{ .Title }}
</a>
{{ else }}
<a disabled>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd" />
</svg>
{{ sub $currentYear 1 }}
</a>
{{ end }}
{{ with .Page.NextInSection }}
<a href="{{ .RelPermalink }}" >
{{ .Title }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
</svg>
</a>
{{ else }}
<a disabled>
{{ add $currentYear 1 }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
</svg>
</a>
{{ end }}
</div>
Is it really pagination? Well, kind of, and there are even disabled links for the years that don’t actually exist.
And finally, you can see an overview of the years as well.