Yearly Post Archives In Hugo

Added by to Tools on and tagged · hugo · site · perl ·

About 5 minutes to read (907 words)

Open Sourcing Mental Illness Rainy's Mish-Mash Socks

I spent a little time this weekend creating yearly post archives for my Hugo site.

Hugo already has pagination functionality, but I dislike this approach in blogs. For one thing, it messes up web search. I find an interesting link which Google claims is on page 12 of some blog, but now I must dig to page 14 or 15 or 23.

Years give me a fixed point to anchor my archive listings to. I could narrow it down to years and months, but I am not that prolific. Years will do fine.

Everything On One Page For Each Section

Hugo bases template selection on specificity: use the most specific template if available, otherwise use more general-purpose templates. Right now I lean on my _default layouts for all content. The _default/list.ace template provides the layout for all content collections.

= content main
  h1 {{ .Title }}
  ul.post-list
    {{ range .Data.Pages }}
      li {{ .Render "li" }}
    {{ end }}

Hugo gives the template a title and a collection of pages - along with numerous other variables, and at the other end something like this is produced.

Default listing applied to Craft section
Default listing applied to Craft section

Group Everything By Date

For a first step in the process, let’s see what happens when we tell the Pages to group by date.

= content main
  h1 {{ .Title }}
  {{ range (.Data.Pages.GroupByDate "2006") }}
    h2 {{ .Key }}
    ul.post-list
      {{ range .Pages }}
        li {{ .Render "li" }}
      {{ end }}
  {{ end }}
Now with headers for each year.
Now with headers for each year.

Not too bad. Let’s move on.

This Year’s Content

Hold on a second. I can use the first function to create a list of things published this year. I’ll put that in my index.ace layout.

= content main
  p This page lists the projects and posts I published on the site this year.
  p This is a blog, so of course you can
    a href="/index.xml"  subscribe via RSS.
  {{ range first 1 (.Data.Pages.GroupByDate "2006") }}
    h2 Posted So Far in {{ .Key }}
    ul.post-list
      {{ range .Pages }}
        li {{ .Render "li" }}
      {{ end }}
  {{ end }}
This year's stuff — perfect for the front page.
This year’s stuff — perfect for the front page.

No matter how hard I tried, I could not find a way to create a page for last year with this technique. Moving on.

Life Hack: Use Taxonomies!

Hugo does not directly support yearly archive pages. However, this Hugo community comment shows that somebody solved a very similar problem using taxonomies.

Taxonomies are special terms you create to organize your content. Categories and tags are a common example across blogs. Hugo simplifies creation of your own taxonomies and their presentation with templates.

Strictly speaking, this is kind of a hack. You would ordinarily put an entry with singular and plural names for your taxonomies in config.yml, but I do not care about pluralization in this case. I just want a way to create “year” templates.

taxonomies:
    year: "year"

In order for this to work, I need every content item to have a year entry in its front matter set to the year that item was published.

year: 2016

This is going to be tedious. Let’s not do this manually, okay?

Here comes the Perl.

Automate The Frontmatter

#!/usr/bin/env perl

use v5.22.0;
use autodie qw(:all);

use File::Find::Rule;
use File::Slurper qw(read_text write_text);
use YAML::Tiny;

sub add_metadata_to {
  my $filename = shift;
  say "Looking at $filename";

  my $original = read_text $filename;
  my ( $empty, $front_matter, $content ) = split /^---$/m, $original;
  my $yaml = YAML::Tiny->read_string( $front_matter );
  my $date = $yaml->[0]{date};

  if ( $date =~ m{^ (?<year> \d\d\d\d) - (?<month> \d\d) - }x ) {
    $yaml->[0]{year} = $+{year};
  }
  else {
    # Avoid converting simple pages.
    return;
  }

  my $new_front_matter = $yaml->write_string;
  my $new_content = $new_front_matter . "---" . $content;
  write_text $filename, $new_content;
  say "Updated $filename metadata";
}

my @files = File::Find::Rule->file()
  ->name( qw( *.md *.html ) )
  ->in( "content" );

add_metadata_to $_ for @files;

Summarize Every Year On One Page

My _default/terms.ace layout was already set up for categories and tags. The reverse alphabetical order was some whimsical experiment that I forgot to change, but it works perfect for years. Reverse alphabetical looks quite a bit like reverse chronological when things are sorted in ASCIIbetical order.

= content main
  h1 {{ .Title }}
  dl
    {{ $data := .Data }}
    {{ range $key, $value := .Data.Terms.Alphabetical.Reverse }}
      dt
        a href="/{{ $data.Plural }}/{{ $value.Name | urlize }}" {{ $value.Name }}
      dd {{ $value.Count }} posts
    {{ end }}

So by adding one little entry to config.yml and making one little edit - okay, write a Perl script to make one little edit to every page in my site - I get a usable yearly archive!

A bit bare, but the links to yearly archives exists
A bit bare, but the links to yearly archives exists

I put _default/list.ace back in its original state, and each year has a simple listing.

Just the stuff I pushed in 2015.
Just the stuff I pushed in 2015.
(see original image in new window)

What Else?

It would sure be nice to have “next year” / “previous year” links. I can use GroupBy functionality to create distinct craft and blog sections for each year’s listing. For that matter, I could see breaking Crafts and Posts listings down into yearly archives.

You know what? This is good enough for now. I set out to have yearly archive pages. I have yearly archive pages. Time to go out and enjoy an unexpectedly pleasant Seattle Sunday.