Creating Separate List Pages in Hugo for Different Topics

Published: 2022 July 27

software development web development static site generator Hugo Hugo theme

Introduction

This project is built with Hugo and uses the Minimal Hugo theme.

This post is part of an ongoing effort to tailor the theme over time.

Objective

In the second site design of this project we incorporated the Minimal Hugo theme, which offers one place for articles.

In this post we’ll modify the theme to create a new third site design that adds an additional category to display content for a separate topic.

Building the solution

Overview of the solution

There are several steps required to make this change:

  1. Select a place for the new post files.
  2. Add type to article front matter.
  3. Create links to the new list pages.
  4. Update the existing list template partial file.
  5. Add a new list template partial file.
  6. Update the article template file.

Updating the existing list template partial file (Step 4) isn’t strictly necessary, but since the project’s existing code for that file needs improvement, it makes sense to make that change before creating another partial file based off of it.

Organizing post files

We first need to decide where articles for the new topic will be located.

In the second site design the articles were located in the /content/post/ directory.

In the third site design we’ll still keep all articles in that directory. There are several reasons for this:

  1. It allows all current articles to keep their current URL.
  2. Existing scripts used to help build the site continue to work without modification.
  3. It keeps all article content grouped together in one place.

This choice means we will have to make other changes so that posts about each of the two topics can be displayed in separate lists.

Adding type to articles

Existing articles

Since all articles are located in the same directory, we need another way to differentatiate which article corresponds to which topic.

We’ll do that by adding the type field to the front matter of each article and assigning it a type - either build or learn for our present purposes.

New articles

To simplify the creation of future posts, we’ll also add the type field to the post archetype file.

The updated front matter section of /archetype/post.md is below:

---
title: "{{ replace ( slicestr .TranslationBaseName 4 ) "-" " " | title }}"
seqid: {{ slicestr .TranslationBaseName 0 3 }}
slug: "{{ slicestr .TranslationBaseName 4 }}"
type: ""
date: {{ .Date }}
lastmod: {{ .Date }}
description: ""
tags: [""]
draft: true
---

The second site design had all articles grouped under the Posts directory.

For the third site design we want to separate posts into two topics: Build and Learn.

To do this we need to add links for the new topics and point them at new list pages.

Modifying the config file

To create new menu links for each topic we first need to modify the project config.toml file.

The previous relevant section:

[[menu.main]]
    url = "/"
    name = "Home"
    weight = 1

[[menu.main]]
    url = "/about/"
    name = "About"
    weight = 2

[[menu.main]]
    url = "/post/"
    name = "Posts"
    weight = 3

The updated section:

[[menu.main]]
    url = "/"
    name = "Home"
    weight = 1

[[menu.main]]
    url = "/about/"
    name = "About"
    weight = 2

[[menu.main]]
    url = "/post/build/"
    name = "Build"
    weight = 3

[[menu.main]]
    url = "/post/learn/"
    name = "Learn"
    weight = 4

Creating new list pages for the new topic lists

We want the list pages for the two topics to be located at:

So, we need to add two new directories to our project:

Next, each directory needs a new _index.md file.

As the The Ultimate Guide to Hugo Sections by Bryce Wray at CloudCannon explains, we need to use an _index.md file in each folder to tell Hugo to create what it calls a branch bundle to create a list of content.

These files do not need to have much in them. In fact, we only need a small amount of front matter.

For the /build/_index.md file:

---
title: "Build"
list_filter: ["build"]
---

And for the /learn/_index.md file:

---
title: "Learn"
list_filter: ["learn"]
---

The title field will be used by the list partial file to title the page.

The list_filter field is a user-defined field that will be used to filter posts for the relevant topic.

Creating a new list page for the existing list

Because of changes we’ll make to the list layout in the next few steps, we also need to create a list page for the post directory.

In the /content/post/ directory, we’ll add a new _index.md file:

---
title: "All Posts"
list_filter: ["build", "learn"]
---

This will maintain existing list functionality as we incorporate the changes for the new site design.

Updating the existing list template partial file

Previously, we found the syntax of the list template file and Hugo’s parsing of it confusing.

The list template file from the second site design could be found at /layouts/partials/list-page.html and it’s original code is below.

<!-- INCLUDE THIS LINE AT THE TOP SO THE PAGINATOR CORRECTLY SETS THE PAGINATION VALUE: {{ range (.Paginator 1000).Pages }} {{ partial "list-item" . }} {{ end }} -->

<main>

    <h2>{{ .Title }}</h2>
    <p>{{ .Paginator.TotalNumberOfElements }} total posts</p>
    <hr>

    {{ range (.Paginator 1000).Pages }}
        {{ partial "list-item" . }}
    {{ end }}

    <hr>

</main>

{{ partial "paginator" . }}

Obviously, including a line of code that is commented out yet is necessary for Hugo to render the page correctly was confusing and undesirable. As we researched how to alter the template to accomodate the new layout we found a better approach for this template, so it was refactored to the following:

{{ $paginator := (.Paginator 1000).Pages }}

<main>

    <h2>{{ .Title }}</h2>
    <p>{{ .Paginator.TotalNumberOfElements }} total posts</p>
    <hr>

    {{ range .Paginator.Pages }}
        {{ partial "list-item" . }}
    {{ end }}

    <hr>

</main>

{{ partial "paginator" . }}

Only two lines were changed, but it made the code far more understandable.

It’s still not clear to us why Hugo processes lines that are commented out, but setting aside that issue the reason that the first line was effective was that Hugo processes a pagination command once per page. So it processes the top pagination command and then uses that result for all other calls for pagination on the same page.

From the Hugo documentation on List Paginator Pages:

The .Paginator is static and cannot change once created.

If you call .Paginator or .Paginate multiple times on the same page, you should ensure all the calls are identical. Once either .Paginator or .Paginate is called while generating a page, its result is cached, and any subsequent similar call will reuse the cached result. This means that any such calls which do not match the first one will not behave as written.

Adding a new list template partial file

Now that we have refactored the existing list template file we can consider the changes necessary for the new list pages.

The template file from the second site design assumed that the content was in the same directory as the list page, which won’t be the case for our new list pages.

We’ll create a new list template file to accomodate this change while maintaining existing list functionality. We can start by copying the newly-updated list template file at /layouts/partials/list-page.html and name the new version /layouts/partials/list-page-for-posts.html.

Slightly later in the Hugo documentation on List Paginator Pages mentioned earlier is an example of a helpful form of the .Paginate command:

{{ $paginator := .Paginate (where .Pages "Type" "posts") 5 }}

We’ll use this form in the third site design:

{{ $paginator := .Paginate (where .Site.RegularPages "Type" "in" ( .Params.list_filter )) 1000 }}

<main>

    <h2>{{ .Title }}</h2>
    <p>{{ .Paginator.TotalNumberOfElements }} total posts</p>
    <hr>

    {{ range .Paginator.Pages }}
        {{ partial "list-item" . }}
    {{ end }}

    <hr>

</main>

{{ partial "paginator" . }}

For our updated list-page-by-type-group.html page we use .RegularPages so that we only list “regular” pages and not other list pages. (This answer on the Hugo forums explains the difference between .Pages and .RegularPages.)

Furthermore, we use .Site.RegularPages so that we list all pages from the site, since we are creating a list for files that are in a separate directory than the list page itself.

We then use the where clause to filter for the type of article that matches the list_filter parameter of the list page. The in function checks for the presence of a value in an array. (See the Hugo documentation for the functions where and in.)

Finally, we use the .Params.list_filter value to filter by the user-defined field list_filter of each topic page.

Calling the new list template partial file

As mentioned above, to maintain existing list functionality while showing the new topical list, we created a new list template partial file.

We now need to add a new list template file to call the new list template partial file.

We’ll create the file /layouts/post/list.html and add the following:

{{ partial "header" . }}

{{ partial "list-page-for-posts" . }}

{{ partial "footer" . }}

Now our existing lists and new topical lists will both work in parallel.

Updating the article template file

Though in an earlier step only one line was added to the project’s articles, it is a line that effects the way content is rendered by the theme files.

Since we added type to the front matter of all articles, we need to update a section of the post-content.html file in the /layouts/partials/ directory so that Related articles will be displayed.

The existing where clause compares the type to the hardcoded value “post”:

{{ $related := first 3 (where (where (where .Site.Pages.ByDate.Reverse ".Type" "==" "post") ".Params.tags" "intersect" .Params.tags) "Permalink" "!=" .Permalink) }}

According to the Hugo documentation on Content Types:

Hugo resolves the content type from either the type in front matter or, if not set, the first directory in the file path. E.g. content/blog/my-first-event.md will be of type blog if no type is set.

If we didn’t set the type for our articles they would all be considered to have a type of post, and the test would pass.

However, since we are setting the type in the front matter of our articles, the test no longer works.

To resolve this we simply remove the (where .Site.Pages.ByDate.Reverse ".Type" "==" "post") test and replace it with .Site.Pages.ByDate.Reverse to show articles regardless of type.

{{ $related := first 3 (where (where .Site.RegularPages.ByDate.Reverse ".Params.tags" "intersect" .Params.tags) "Permalink" "!=" .Permalink) }}

Conclusion

We now have the desired site layout and functionality using type in article frontmatter while preserving existing functionality. This approach seems to slightly go against the grain of Hugo.

Another design might make better use of Hugo’s default taxonomies. For instance, in a future update it might be worth considering using categories, one of Hugo’s default taxonomies.

Copying Directory Structure With a Bash Script - With Help From ChatGPT AI

Published: 2024 January 22

Backing Up Git Config Values With a Bash Script

Published: 2023 August 25

Backing Up Visual Studio Code (VS Code) Settings and Extensions With a Bash Script

Published: 2022 November 09