Hugo: WordPress-Style Archiving

Creating a WordPress-style archive view in Hugo for better content organization.

Introduction

As the clock ticks down on 2024, I thought I’d sneak in one last blog post. This time, I’m sharing my recent adventure in customizing Hugo’s archive view. Before my latest tinkering, my blog’s archive layout looked pretty basic.

Here’s what the homepage overview looked like:

And when visiting the archives page, you’d see just a plain list of posts. Functional, sure, but I wanted something more visually engaging—something akin to WordPress-style archiving that breaks posts down by year and month, with a handy post count for each month.

The WordPress style I had in mind looks something like this:

1
2
3
4
Year
| - Month (Post Count)
| - Month (Post Count)
| - Month (Post Count)

I decided to roll up my sleeves and see if I could recreate this structure in Hugo.

The Plan

Hugo’s flexibility and the Stack theme by CaiJimmy made this a fun challenge. Since I’m using the theme as a Git submodule, I had to create the necessary customizations outside the theme’s directory to keep my setup clean and maintainable.

Create an Archive Page

Setting up an archive page is straightforward! Follow these steps:

  1. Navigate to the content folder in your Hugo site.

  2. Create a new folder named archives.

  3. Inside the archives folder, create a file called archives.md.

  4. Add the following content to archives.md:

1
2
3
4
5
---
title: "Archives"
layout: "archives"
slug: "archives"
---

Customizing the Archive Layout

1. Creating the Archive Widget

The first step was to create a new archive.html file under <site-root>/layouts/partials/widget/. This file contains the logic for grouping posts by year and month and displaying the post counts.

Lines 17-44 are specific to the archive style.

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{{- $query := first 1 (where .Context.Site.Pages "Layout" "==" "archives") -}}
{{- $context := .Context -}}
{{- $limit := default 5 .Params.limit -}}
{{- if $query -}}
    {{- $archivesPage := index $query 0 -}}
    <section class="widget archives">
        <div class="widget-icon">
            {{ partial "helper/icon" "infinity" }}
        </div>
        <h2 class="widget-title section-title">{{ T "widget.archives.title" }}</h2>

        {{ $pages := where $context.Site.RegularPages "Type" "in" $context.Site.Params.mainSections }}
        {{ $notHidden := where $context.Site.RegularPages "Params.hidden" "!=" true }}
        {{ $filtered := ($pages | intersect $notHidden) }}
        {{ $archives := $filtered.GroupByDate "2006" }} <!-- Grouping by year -->

        <div class="widget-archive--list">
            {{ range $index, $item := first (add $limit 1) ($archives) }}
                {{- $id := lower (replace $item.Key " " "-") -}}
                <div class="archives-year">
                    <a href="{{ $archivesPage.RelPermalink }}#{{ $id }}">
                        {{ if eq $index $limit }}
                            <span class="year">{{ T "widget.archives.more" }}</span>
                        {{ else }}
                            <span class="year">{{ .Key }}</span>
                            <span class="count">({{ len $item.Pages }})</span>
                        {{ end }}
                    </a>
                </div>
                <div class="archives-months">
                    <ul>
                        {{ $months := $item.Pages.GroupByDate "January" }} <!-- Grouping by month -->
                        {{ range $month := $months }}
                            <li>
                                <a href="{{ $archivesPage.RelPermalink }}#{{ $id }}-{{ lower (replace $month.Key " " "-") }}">
                                    <span class="month">{{ $month.Key }}</span>
                                    <span class="count">({{ len $month.Pages }})</span>
                                </a>
                            </li>
                        {{ end }}
                    </ul>
                </div>
            {{ end }}
        </div>

        {{ if gt (len $archives) $limit }}
            <div class="pagination">
                {{ $previous := (index $archives 0) }}
                {{ $next := (index $archives 1) }}
                <span class="prev">
                    {{ if $previous }}<a href="{{ $archivesPage.RelPermalink }}#{{ lower (replace $previous.Key " " "-") }}">Previous</a>{{ end }}
                </span>
                <span class="next">
                    {{ if $next }}<a href="{{ $archivesPage.RelPermalink }}#{{ lower (replace $next.Key " " "-") }}">Next</a>{{ end }}
                </span>
            </div>
        {{ end }}
    </section>
{{- else -}}
    {{- warnf "Archives page not found. Create a page with layout: archives." -}}
{{- end }}

2. Updating the Default Archive Page

Next, I customized the default archives layout file: <site-root>/layouts/_default/archives.html. This file handles rendering the main archive page structure and ensures the new widget integrates seamlessly.

Lines 22-38 are specific to the archive style.

 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
33
34
35
36
37
38
39
40
41
{{ define "body-class" }}template-archives{{ end }}
{{ define "main" }}
    <header>
        {{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}}
        {{- $terms := $taxonomy.Pages -}}
        {{ if $terms }}
        <h2 class="section-title">{{ $taxonomy.Title }}</h2>
        <div class="subsection-list">
            <div class="article-list--tile">
                {{ range $terms }}
                    {{ partial "article-list/tile" (dict "context" . "size" "250x150" "Type" "taxonomy") }}
                {{ end }}
            </div>
        </div>
        {{ end }}
    </header>

    {{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }}
    {{ $notHidden := where .Site.RegularPages "Params.hidden" "!=" true }}
    {{ $filtered := ($pages | intersect $notHidden) }}

    {{ range $filtered.GroupByDate "2006" }}
    {{ $year := lower (replace .Key " " "-") }}
    <div class="archives-group" id="{{ $year }}">
        <h2 class="archives-date section-title"><a href="{{ $.RelPermalink }}#{{ $year }}">{{ .Key }}</a></h2>
        {{ range .Pages.GroupByDate "January 2006" }}
        {{ $month := lower (replace .Key " " "-") | replaceRE `-[0-9]{4}$` `` }}
        <div class="archives-group" id="{{ $year }}-{{ $month }}">
            <h3 class="archives-date section-title" id="{{ $year }}-{{ $month }}"><a href="{{ $.RelPermalink }}#{{ $year }}-{{ $month }}">{{ .Key }}</a></h3>
            <div class="article-list--compact">
                {{ range .Pages }}
                    {{ partial "article-list/compact" . }}
                {{ end }}
            </div>
        </div>
        {{ end }}
    </div>
    {{ end }}

    {{ partialCached "footer/footer" . }}
{{ end }}

3. Adding Styling

Here’s an improved version of your text:

Finally, we need to refine the styling with some SCSS magic—otherwise, you’ll end up with this “beautiful” display:

To polish the appearance, I made some SCSS tweaks in <site-root>/assets/scss/general.scss Here’s a key adjustment:

Line 27, Fixing the padding issue by 10px.

 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
a {
    text-decoration: none;
    color: var(--accent-color);

    &:hover {
        color: var(--accent-color-darker);
    }

    &.link {
        box-shadow: 0px -2px 0px rgba(var(--link-background-color), var(--link-background-opacity)) inset;
        transition: all 0.3s ease;

        &:hover {
            box-shadow: 0px calc(-1rem * var(--article-line-height)) 0px rgba(var(--link-background-color), var(--link-background-opacity-hover)) inset;
        }
    }
}

.section-title {
    text-transform: uppercase;
    margin-top: 0;
    margin-bottom: 10px;
    display: block;
    font-size: 1.6rem;
    font-weight: bold;
    color: var(--body-text-color);
    padding-top: 10px;
    a {
        color: var(--body-text-color);
    }
}

The Result

After implementing these changes, the archive view now displays posts grouped by year and month, with a clear count of posts for each month.

Conclusion

Thanks to Hugo’s flexibility and some help from GitHub Copilot, I managed to create a clean, WordPress-style archive view that makes navigating my posts a breeze. If you’re looking to customize your Hugo site, don’t hesitate to experiment—it’s incredibly rewarding!

Let me know your thoughts or share how you’ve customized your Hugo setup in the comments below.

Happy New Year, and here’s to a great 2025! 🎉

Share with your network!

Built with Hugo - Theme Stack designed by Jimmy