I’ve previously implemented a series taxonomy in Hugo, but it only supported a single series per post and used intersect which is kind of slow.

Here is my improved implementation of a series taxonomy in Hugo 👇

Table of contents

Configuration

As before; all taxonomies must be defined in the configuration:

[taxonomies]
  tag = "tags"
  series = "series"
  controller = "controllers"

This allows us to use series in the front matter, and a /series/ page gets created — like mine.

All taxonomies must be defined as arrays in the front matter, but unlike the previous implementation, this solution actually support multiple series 🙂

series = ["Home network v2", "Network cabling"]

Layout

I’m showing a notification at the top of the page, and a list of all pages part of the series at the bottom. So we need to add some code to our /layouts/_default/single.html layout.

Before the content

Here we are using range to build up a slice (array) with links to all post series, and then showing them using delimit:

{{- with (.GetTerms "series") -}}
  {{- $series := slice -}}
  {{- range . -}}
    {{- $series = $series | append (printf "<a href=\"%s\">%s</a>" .Permalink .Title) -}}
  {{- end -}}
  <div class="notice--info post-series-top">
    <p>This post is part of the {{ delimit $series ", " " and " }} series.</p>
  </div>
{{- end -}}

And a bit of CSS to make it look pretty:

@mixin notice($notice-color)
{
    margin: 1em 0;
    padding: 0.5em 1em;
    border-left: 0.5em solid $notice-color;
    background-color: rgba($notice-color, 0.1);

    > *:first-child {
        margin-top: 0;
    }

    > *:last-child {
        margin-bottom: 0;
    }
}

.notice--info {
    @include notice(#8ebeeb);
}

div.post-series-top {
    a {
        font-weight: bold;
    }
}

And we have a notification, showing all series titles, with link to the term pages 👍

Notification before the post content

After the content

In single.html I used a partial:

{{- if .Params.series -}}
  <div class="post-series-bottom">
    {{ partial "series.html" . }}
  </div>
{{- end -}}

Which loads the code from layouts/partials/series.html. In my previous implementation I was using intersect — which is a poor choice as it’s slow and returns the common elements of two arrays or slices. We don’t really need that.

So instead I’m first using range to loop through the series, and then range again to loop through all pages within that series. All pages are shown as links — except the page you are on, it’s highlighted in bold:

{{- with (.GetTerms "series") -}}
  {{- range . -}}
    <h3>{{ .Title }} series</h3>
    {{- $series := .Pages.ByDate -}}
    <ol>
      {{- range $series -}}
      <li>
        {{- if eq .File.UniqueID $.File.UniqueID -}}
          <b>{{ .Title }}</b>
        {{- else -}}
          <a href="{{ .Permalink }}">{{ .Title }}</a>
        {{- end -}}
      </li>
      {{- end -}}
    </ol>
  {{- end -}}
{{- end -}}
List of posts in series, bottom of page

Done

I like to have some freedom with my taxonomies, free of constraints like having to add, or only using one. This is why I’ve decided against trying to categorize posts on this site. Some posts fits easily into a single category, others do not.

With this new series implementation I don’t have to worry about some posts crossing two series 🙂

Last commit 2023-12-25, with message: replace emoji slight_smile/slightly_smiling_face