Right, so you’ve got a site, and it has navigation. Maybe it’s a list of pages, maybe it’s categories, maybe it’s a collection of your favorite 80s action heroes. The point is, it’s data. And the moment you find yourself hard-coding a list of hrefs and labels in a layout file, a little alarm should go off in your head. You’ve just created a liability. What happens when you need to add a new section? You’re digging through baseof.html or some other template, praying you don’t mangle the markup. This is why Hugo gave us the data/ directory and data templates. We’re going to use them to build a navigation menu that you can manage with a simple text file, like a civilized person.

The concept is gloriously simple. You put structured data (YAML, JSON, or TOML) in your site’s data/ directory, and then you can access it in your templates via the .Site.Data object. For navigation, YAML feels the most natural because it’s easy for humans to read and write.

Structuring Your Navigation Data

First, create a file at data/navigation.yaml. This is where we’ll define our menu items. Each item should have at least a name (what the user sees) and a url (where it links to). We can get fancy later, but let’s start simple.

- name: Home
  url: /
- name: Blog
  url: /blog/
- name: About
  url: /about/
- name: Contact
  url: /contact/

See? Not rocket science. It’s just a list of things. This is already a million times better than having this list buried in HTML inside a template. You can hand this file to someone who has never heard of Hugo, and they can probably figure out how to add a new link. This is a feature, not a bug.

The Template Code: Looping Through Data

Now, let’s use this data in a template. You’ll typically put this in a partial template, like layouts/partials/nav.html, which you then include in your header. The template code to loop through this data is a straightforward range loop.

<nav>
  <ul class="nav-list">
    {{ range .Site.Data.navigation }}
      <li>
        <a href="{{ .url }}" class="nav-link">{{ .name }}</a>
      </li>
    {{ end }}
  </ul>
</nav>

We’re accessing our data via .Site.Data.navigation—the navigation part matches the filename we created (navigation.yaml). The range function loops through each item in the list, and inside the loop, .url and .name refer to the properties of the current item. This will generate a simple, clean list of links.

Adding “Active” State: Because You’re Not a Savage

A navigation menu that doesn’t tell you where you are is about as useful as a chocolate teapot. We need to add an active class to the link of the current page. This is where we move from simple to slightly clever. We need to compare the URL of our menu item to the URL of the current page (.Permalink or .RelPermalink).

But wait, URLs can be tricky. Is /about/ the same as /about? Is /blog the same as /blog/? Hugo’s .RelPermalink includes the trailing slash, so our URLs in the data file should too for consistent comparison. Here’s the improved template:

<nav>
  <ul class="nav-list">
    {{ $currentPage := .RelPermalink }}
    {{ range .Site.Data.navigation }}
      {{ $isActive := eq $currentPage .url }}
      <li>
        <a href="{{ .url }}" class="nav-link {{ if $isActive }}active{{ end }}">{{ .name }}</a>
      </li>
    {{ end }}
  </ul>
</nav>

We store the current page’s relative permalink in a variable ($currentPage) because we need to use it multiple times inside the loop. Then, for each menu item, we check if its .url is equal to the $currentPage. The result of that comparison (true or false) is stored in $isActive. Finally, we use an if statement to conditionally add the active class.

Handling Edge Cases and Being Fancy

What if you have a section, like “Blog,” and you want the menu item to stay active for any post within that section? Comparing exact URLs fails here. This is where we might add a custom identifier or leverage Page Kind.

A more robust solution is to add a section key to relevant menu items in your data file:

- name: Home
  url: /
- name: Blog
  url: /blog/
  section: blog
- name: About
  url: /about/

And then use more complex logic in the template:

{{ $currentPage := . }}
{{ range .Site.Data.navigation }}
  {{ $isActive := false }}
  {{ if .section }}
    {{ if eq $currentPage.Section .section }}
      {{ $isActive = true }}
    {{ end }}
  {{ else }}
    {{ $isActive = eq $currentPage.RelPermalink .url }}
  {{ end }}
  <li>
    <a href="{{ .url }}" class="{{ if $isActive }}active{{ end }}">{{ .name }}</a>
  </li>
{{ end }}

Now, for any item with a section defined, it checks if the current page’s Section (e.g., “blog”) matches. Otherwise, it falls back to the direct URL match. This is the kind of control that makes data-driven templates so powerful. You’re not just dumping data to the screen; you’re writing logic to make it behave intelligently.

The biggest pitfall? Overcomplicating it at the start. Start with the simple URL-matching version. Only add complexity like section matching when you actually need it. And always, always make sure your data file URLs include the trailing slashes for consistency with Hugo’s default behavior. It saves a world of head-scratching later.