Jessica Huynh /hwɪn/

It’s been quite a while since I’ve touched my site but stay-at-home makes for strange decision making. I’m doing some cleanup of the design, fixing broken links, and generally making it more efficient (minifying, dropping dependencies since I’m already deployed on a CDN, that sort of thing). One of the changes I just finished was fixing up my links to the post type, categories, and tags, which all make use of Hugo’s taxonomies.

Taxonomies overview

For Hugo, taxonomies are groupings of content. You get categories and tags out-of-the-box, but can define your own. On my website, I have the default categories and tags, along with type and project. You can also remove taxonomies entirely if desired.

The documentation page on taxonomies is pretty good, so I don’t want to repeat that content so much as provide use cases for a portfolio with a blog and talk about how it’s implemented on my website.

How taxonomies work in Hugo

A taxonomy is the classification and contains terms (think of them as keys) and values assigned to terms. For example, within “categories”, we have the term “Web development”.

Starting in 0.73.0, Hugo uses the above terminology and matching page kinds; in the past the page kind of a taxonomy was called “terms” and terms were a “terminology list”. I’ll be using the new terminology and page kinds throughout this post.

To add more taxonomoies beyond the default, you can configure them in your site config file (the example here is YAML):

1
2
3
4
5
taxonomies:
  project: projects
  tag: tags
  category: categories
  type: types

You need to specify categories and tags if you want them if you do specify your own taxonomies.

If you don’t want any taxonomies, you can disable the relevant kinds:

1
2
3
disableKinds:
  - taxonomy
  - term

Implementation on jessicahuynh.info

In content pages

As I mentioned, I added projects and types in addition to the default taxonomies. For blog posts like this one, I use categories, tags, and types. For example, this post has the following bit in the YAML header:

1
2
3
tags: ["hugo", "covid-19"]
categories: ["Web development"]
types: ["post"]

Each post can only have one type, but it’s in a list so that the types term can be easily referenced on the blog layout pages (more on that later).

The project type I use for index.md in different portfolio pages, e.g. project: "Arabic Grammar". I used the singular because I didn’t need to reference it outside of portfolio pages.

Both the singular and plural are fine to be used when compiling the taxonomy and terms layout pages.

In templates and layouts

My blog has breakdowns by type/category/tag at jessicahuynh.info/<plural>. There is also an alternative view of my portfolio at https://www.jessicahuynh.info/projects that I don’t link to outside of this post. All were automatically generated.

I also reference taxonomies in the blog sidebar (bottom bar in mobile).

Taxonomy and term pages

In the layouts directory of Hugo, in the _default folder, I have taxonomy.html and term.html that generates these pages. The former lists all terms in a given taxonomy and the latter lists all pages tagged with a particular term in YAML.

In taxonomy.html, the {{ .Title }} is (if you haven’t set pluralizeListTitles in the config to False) the plural of the taxonomy. You can range over all the terms in that taxonomy with {{ range .Data.Terms }}. In my case, I list alphabetically and also point out the count of pages tagged with that term.

1
2
3
4
5
<ul>
  {{ range .Data.Terms.Alphabetical }}
    <li><a href="{{ .Page.Permalink }}">{{ .Page.Title }}</a> ({{ .Count }})</li>
  {{ end }}
</ul>

In term.html, you can range over the pages tagged with that term with {{ range .Data.Pages }}. I also put in a link to the taxonomy and some information about the page (a partial here because it’s used on portfolio pages as well).

1
2
3
4
5
6
<p>See all <a href="/{{ .Data.Plural }}">{{ .Data.Plural }}</a>.</p>
<ul>
  {{ range .Data.Pages }}
    {{ partial "postli.html" . }}
  {{ end }}
</ul>

postli.html contents:

1
2
3
4
5
<li>
  <a href="/archive/#{{ .Date.Format "2006" }}"><span class="label reddish"><i class="fa fa-calendar" aria-hidden="true"></i>&nbsp;{{ .Date.Format "Mon, Jan 2, 2006" }}</span></a>                    
  <a href="{{ .Permalink }}">{{ .Title }}</a>: 
  {{ .Description }}
</li>

Blog bar

The archive links are generated by looping over the years in posts and then directly linking to the archive page; they’re not created by referencing specified Hugo taxonomies.

To be able to easily range over categories and types, I specify all the possible ones in the site config file:

1
2
3
4
params:
  subtitle: "/hwɪn/"
  categories: ["Lifestyle","Web development"]
  types: ["post","status","recipe"]

This way, we can easily range in the blog bar template and link to the appropriate term template page:

1
2
3
4
5
<ul>
  {{ range .Site.Params.categories }}		
    <li><a href="/categories/{{ . | urlize }}">{{ . }}</a></li>
  {{ end }}
</ul>

The upside of this approach is that you can get appropriate capitalization and remove hyphens in between words, or avoid referencing pages, which is what would appear if we just ranged over .Site.Taxonomies, like we do for tags:

1
2
3
4
5
<ul>
  {{ range $name, $t := .Site.Taxonomies.tags  }}
    <li><a href="/tags/{{ $name | urlize }}">{{ $name }}</a></li>
  {{ end }}
</ul>

For tags, I care less about how they’re formatted, so ranging over .Site.Taxonomies.tags is sufficient and requires less maintenance. I don’t plan to have many post types or categories, so manual upkeep is sufficient.

Post metadata

Every blog post, regardless of type, has a listing of metadata, including the date, how long the post takes to read, post type, category, and tags.

Since the type, category, and any tags are specified in the YAML header, I can then easily range using .Params and create clickable links to the relevant taxonomy term page.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{{ range .Params.types }}
  <a href="{{ "/types/" | absURL }}{{ . | urlize }}">
    <span class="orangish label">
      <i class="fa fa-archive" aria-hidden="true"></i>
      &nbsp;{{ . }}
    </span>
  </a>
{{ end }}

{{ range .Params.categories }}
  <a href="{{ "/categories/" | absURL }}{{ . | urlize }}">
    <span class="reddish label">
      <i class="fa fa-folder-open" aria-hidden="true"></i>
      &nbsp;{{ . }}
    </span>
  </a>
{{ end }}

{{ range .Params.tags }}
  <a href="{{ "/tags/" | absURL }}{{ . | urlize }}">
    <span class="transparent label">
      <i class="fa fa-tag" aria-hidden="true"></i>
      &nbsp;{{ . }}
    </span>
  </a>
{{ end }}

I no longer use Font Awesome’s icons, instead preferring SVGs, but those take up lots of space for a blog post and are immaterial to the main point, so in they stay.

Portfolio single pages

Since I used the singular taxonomy term name in the content markdown page, I use that value when ranging over the pages in the portfolio content folder and then comparing. I use this for determining whether a particular project has associated content pages and list them if so. The actual determination of what is a project is already done by creating content pages under portfolio.

To establish if a project has posts, I use the following bit of templating:

1
2
3
4
5
6
7
{{ $.Scratch.Set "hasPostsAbout" "false" }}
{{ $p := .Params.project }}
{{ range .Site.Pages }}
  {{if in .Params.projects $p }}
    {{ $.Scratch.Set "hasPostsAbout" "true" }}
  {{ end }}
{{ end }}

$.Scratch.Set is used for setting template variables. If hasPostsAbout is true, I then create the section that lists posts. By default it is false.

This does require ranging through .Site.Pages and checking all of them for their project. This would be inefficient in a traditional programming context, but with static site generation and Hugo specifically, this isn’t an issue since only the generated HTML pages will be deployed.

Linking posts about a project

All blog posts related to a particular project have a box below the table of contents that says “Posts about (project name)”, a listing of all linked posts, then a link to the portfolio single page.

The algorithm for this would also be inefficient in a traditional programming context. I loop through all projects specified in the Markdown header for the blog post to allow for the possibility that a post could be linked to multiple projects. Within each sidebox, I go through all projects again, keeping track of the key (slug) and specific taxonomy term. From there, if the key matches the parent project, I go through every page within the taxonomy term. If the title of the current blog post is in that term, I populate the sidebox.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{{ $p := .Site.Taxonomies.projects }}
{{ $t := .Title }}
{{ range .Params.projects }}
  {{$c := . | urlize}} <!--slug for current project being iterated through-->
  <aside class="alsoInProject">
    <header class="sideBoxHeader">
      <a href="#">
        Posts about {{ . }}&nbsp;
        <span class="sideBoxChevron"></span>
      </a>
    </header>
    <div class="sideBoxContent">
      <ol>
        <!-- go through every project again and list the ones with the same project as the current page-->
        {{ range $key, $tax := $p }}
          {{ if eq $key $c }}
            {{ range $tax.Pages }}
              <li>
                {{if (eq $t .Title)}}
                  {{ .Title }}
                {{else}}
                <a href="{{ .Permalink }}">{{ .Title }}</a>
                {{end}}
              </li> 
            {{ end }}
          {{end}}
        {{ end }}
      </ol>
      <span class="text-right"><a href="/portfolio/{{$c}}">About the project</a></span>
    </div>
  </aside>
{{ end }}

Again, very inefficient, big O is O(N³), please don’t tell any of my former professors. There’s also a bit of conditional logic in there for not making a link to show what page is currently being viewed, which isn’t strictly necessary but a nice bit of usability.