Breadcrumb Navigation in Hugo

3 Jul 2021 | Updated on 10 Sep 2022

Introduction

Breadcrumbs provide an effective method for users to identify the current page’s location and navigate back to its parent pages if necessary. It is handy if your site relies heavily on nested sections, like mine.

The breadcrumb for this lesson post. Yes, it is six levels deep.

While Hugo does not have built-in support for breadcrumbs, making one from scratch is relatively simple. I will explain two possible methods of writing a breadcrumb partial in Hugo.

Using the .Parent variable

A Hugo page has a .Parent variable, and it returns the parent section of the page. So, for example, the .Parent of this post would be /posts/. The official Hugo documentation provides an excellent partial code that shows the breadcrumb navigation using a recursive method:

> layouts/partials/breadcrumb.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<ul class="breadcrumb">
  {{ template "breadcrumbnav" (dict "p1" . "p2" .) }}
</ul>

{{ define "breadcrumbnav" }}
  {{ if .p1.Parent }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 )}}
  {{ else if not .p1.IsHome }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 )}}
  {{ end }}
  <li{{ if eq .p1 .p2 }} class="active"{{ end }}>
    <a href="{{ .p1.RelPermalink }}">{{ .p1.Title }}</a>
  </li>
{{ end }}
Explanation
<ul class="breadcrumb">
  {{ template "breadcrumbnav" (dict "p1" . "p2" .) }}
</ul>

{{ define "breadcrumbnav" }}
  {{ if .p1.Parent }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 )}}
  {{ else if not .p1.IsHome }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 )}}
  {{ end }}
  <li{{ if eq .p1 .p2 }} class="active"{{ end }}>
    <a href="{{ .p1.RelPermalink }}">{{ .p1.Title }}</a>
  </li>
{{ end }}

First, it sets up an ordered list and calls a breadcrumbnav template with two variables, p1 and p2, both set up to be the current page (.). The rest of the code is the definition of this template. Because of how it is set up, we will look at the bottom of the template first.

<ul class="breadcrumb">
  {{ template "breadcrumbnav" (dict "p1" . "p2" .) }}
</ul>

{{ define "breadcrumbnav" }}
  {{ if .p1.Parent }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 )}}
  {{ else if not .p1.IsHome }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 )}}
  {{ end }}
  <li{{ if eq .p1 .p2 }} class="active"{{ end }}>
    <a href="{{ .p1.RelPermalink }}">{{ .p1.Title }}</a>
  </li>
{{ end }}

The last string the template prints out is the <li> item with the title of p1 (which equals the current page) and its link. Because p1 and p2 are both ., the list item has the active class.

<ul class="breadcrumb">
  {{ template "breadcrumbnav" (dict "p1" . "p2" .) }}
</ul>

{{ define "breadcrumbnav" }}
  {{ if .p1.Parent }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 )}}
  {{ else if not .p1.IsHome }}
    {{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 )}}
  {{ end }}
  <li{{ if eq .p1 .p2 }} class="active"{{ end }}>
    <a href="{{ .p1.RelPermalink }}">{{ .p1.Title }}</a>
  </li>
{{ end }}

The first half of the code checks if p1 has a section above, either a parent section or the homepage. If this is the case, it will trigger another breadcrumbnav template, which will also print out the list item and repeats until it reaches the homepage. Because the check is done before writing the list item, they will be ordered in reverse, from the homepage to the current page!

Adding this partial to the <body> of this page (/posts/breadcrumb-navigation-in-hugo/)

> layouts/posts/single.html
1
2
3
4
5
<body>
  {{ partial "breadcrumb" . }}

  <!-- ...rest of body -->
</body>

and compiling the site gives the following HTML code:

> public/posts/breadcrumb-navigation-in-hugo.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<body>
  <ol class="breadcrumb">
    <li>
      <a href="/">Welcome!</a>
    </li>
    <li>
      <a href="/posts/">Posts</a>
    </li>
    <li class="active">
      <a href="/posts/breadcrumb-navigation-in-hugo/"
      >Breadcrumb Navigation in Hugo</a>
    </li>
  </ol>
  
  <!-- ...rest of body -->
</body>

Note that my homepage does have a title of Welcome!, so I will need to do some fixes on that if I would use this code.

Parsing the URL

Another method of creating a breadcrumb is to use the URL because it will reflect on the section structure of the pages. Here is an example:

> layouts/partials/breadcrumb.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<ol class="breadcrumb">
  <li><a href="/">Home</a></li>
  {{ $rellink := "" }}
  {{ range (split .RelPermalink "/") }}
    {{ if gt (len . ) 0 -}}
      {{ $rellink = printf "%s/%s" $rellink . }}
      <li><a href="{{ $rellink }}">{{ humanize . }}</a></li>
    {{ end }}
  {{ end }}
</ol>
Explanation
<ol class="breadcrumb">
  <li><a href="/">Home</a></li>
  {{ $rellink := "" }}
  {{ range (split .RelPermalink "/") }}
    {{ if gt (len . ) 0 -}}
      {{ $rellink = printf "%s/%s" $rellink . }}
      <li><a href="{{ $rellink }}">{{ humanize . }}</a></li>
    {{ end }}
  {{ end }}
</ol>

It first defines an ordered list like before, and the first item in the list is the homepage.

<ol class="breadcrumb">
  <li><a href="/">Home</a></li>
  {{ $rellink := "" }}
  {{ range (split .RelPermalink "/") }}
    {{ if gt (len . ) 0 -}}
      {{ $rellink = printf "%s/%s" $rellink . }}
      <li><a href="{{ $rellink }}">{{ humanize . }}</a></li>
    {{ end }}
  {{ end }}
</ol>

The .RelPermalink refers to the relative link to the page, so /posts/breadcrumb-navigation-in-hugo/. If we split it by slashes, we will get a slice that looks like ["", "posts", "breadcrumb-navigation-in-hugo", ""]. We will loop over these strings to build up the breadcrumb.

<ol class="breadcrumb">
  <li><a href="/">Home</a></li>
  {{ $rellink := "" }}
  {{ range (split .RelPermalink "/") }}
    {{ if gt (len . ) 0 -}}
      {{ $rellink = printf "%s/%s" $rellink . }}
      <li><a href="{{ $rellink }}">{{ humanize . }}</a></li>
    {{ end }}
  {{ end }}
</ol>

First, we will get rid of the empty strings using the if statement. If you are confused by the . notation, have a read of this awesome article on scopes in Hugo. Then, we will append the current string to the link variable to retrieve the relative link for each section and use the humanized version of the string as the title of the list.

Below is the HTML for the breadcrumb of this page:

> public/posts/breadcrumb-navigation-in-hugo.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<body>
  <ol class="breadcrumb">
    <li>
      <a href="/">Home</a>
    </li>
    <li>
      <a href="/posts/">Posts</a>
    </li>
    <li>
      <a href="/posts/breadcrumb-navigation-in-hugo/"
      >Breadcrumb navigation in hugo</a>
    </li>
  </ol>
  
  <!-- ...rest of body -->
</body>

The end result may not look very different from the first method, if the slugs of posts and sections are pretty much identical to the titles. Slugs are often much shorter in my blog and this code will make the breadcrumbs much more compact.

CSS

Finally, we can put the lists on a single line.

> assets/css/main.css
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.breadcrumb {
  list-style: none;

  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
}

.breadcrumb li {
  display: inline;
  white-space: nowrap;
}

.breadcrumb li + li:before {
  content: ">";
  padding: 0.3rem;
}

Setting <ol> as a flexbox enables the list items to naturally wrap to the next line if there is not enough space.