30.5 Rendering Menus in Templates: .Site.Menus
Alright, let’s talk about Hugo’s menu system in templates. This is where the rubber meets the road, where your beautifully defined menus in config.toml finally get to show off on the actual website. We’ll be using .Site.Menus—your gateway to all the menu magic.
Think of .Site.Menus as a big dictionary (a map, in Go parlance) that Hugo plops into your template. Each key in this map is the name of a menu you defined (like "main" or "footer"), and the value is a slice (an array) of menu entries. Your job is to range over that slice and turn it into HTML. It’s shockingly straightforward once you get the hang of it.
The Basic Loop: Your Bread and Butter
Let’s start with the absolute minimum. You want to render a simple unordered list for your main navigation. Here’s how you do it. This is the code you’ll write, forget, and then come back to copy-paste a hundred times.
<nav>
<ul>
{{ range .Site.Menus.main }}
<li><a href="{{ .URL }}">{{ .Name }}</a></li>
{{ end }}
</ul>
</nav>
Dead simple, right? We range over the slice of menu items in .Site.Menus.main. For each item, we print a list item with a link. .URL and .Name are the fields you (hopefully) set up in your config file. If this works, congratulations, you’ve passed the first test. But we both know real-world nav isn’t this polite.
Making it Actually Useful: The Active State
A navigation menu that doesn’t show you where you are is about as useful as a chocolate teapot. We need to highlight the current page. This is where the .Page and .IsMenuCurrent/.HasMenuCurrent functions come in. These are Hugo’s gifts to you.
The .Page method returns the actual Page object associated with a menu entry, but only if you remembered to set pageRef or linked to a page’s Permalink in your config. If you just typed url = "/about/", this will be nil. It’s a common gotcha. Always link to your content’s Permalink if you want the fancy page association.
Now, for the active state magic:
<nav>
<ul>
{{ $currentPage := . }}
{{ range .Site.Menus.main }}
<li>
<a href="{{ .URL }}" {{ if $currentPage.IsMenuCurrent "main" . }}class="active"{{ end }}>
{{ .Name }}
</a>
</li>
{{ end }}
</ul>
</nav>
Here’s the why: We store the current page context ($.) in a variable $currentPage because inside the range loop, the context changes. . is now the menu entry, not the overall page. $currentPage.IsMenuCurrent "main" . checks “Is the menu entry I’m currently looping over the exact same page as the one the user is looking at?” It’s a precise match.
But what if you’re on a child page and want its parent in the menu to be highlighted? That’s where .HasMenuCurrent comes in. It’s less common but invaluable for multi-level menus.
Unleashing the Front Matter Power
Remember when I said you could define menus in a content file’s front matter? Here’s how you access those specific menus. It’s not .Site.Menus, it’s .Menus.
Let’s say you’re in a blog post template and want to show a contextual menu that was defined just for that section:
{{ with .Menus.sidebar }}
<aside>
<h3>Related Links</h3>
<ul>
{{ range . }}
<li><a href="{{ .URL }}">{{ .Name }}</a></li>
{{ end }}
</ul>
</aside>
{{ end }}
The with check is crucial. If the author didn’t define a “sidebar” menu for this particular page, the whole block is gracefully skipped. This is how you avoid rendering empty <aside> tags all over the place.
Handling the Nested Menus (The “Ugh, Fine” Part)
Hugo supports nested menus, but its template handling for them is… let’s call it “manual.” You have to check for children and then recursively range through them yourself. It’s not pretty, but it’s powerful. You’ll likely want to use a partial to keep your sanity.
First, create a partial like layouts/partials/menu.html:
{{/*
layouts/partials/menu.html
Recursively renders a menu slice.
*/}}
<ul>
{{ range . }}
<li>
<a href="{{ .URL }}">{{ .Name }}</a>
{{ if .HasChildren }}
{{ partial "menu.html" .Children }}
{{ end }}
</li>
{{ end }}
</ul>
Then, in your template, you call it for the top-level items:
<nav>
{{ partial "menu.html" .Site.Menus.main }}
</nav>
This works because the .Children property of a menu entry is just another slice of menu entries, so our partial can call itself recursively. It’s a classic programming pattern, and the fact that we have to implement it ourselves in the template is both absurd and perfectly on-brand for Hugo’s “here are the legos, now build the Death Star” philosophy.
Best Practices and Pitfalls
Always Check
.URL: Before Hugo v0.76.0, accessing.URLon a menu entry with no URL defined would panic. It’s safer now, but it’s still a good habit to check if a menu entry is valid before trying to render it, especially when you’re pulling in dynamic data.External Links: If you set
target="_blank"in your config, userel="noopener"alongside it in your template for security and performance. Don’t make Hugo do it for you; be explicit.<a href="{{ .URL }}" {{ if eq .Params.target "_blank" }}target="_blank" rel="noopener"{{ end }}>Accessibility: Your
<nav>and<ul>are semantic HTML. Use them. For the currently active item, don’t just change the color with aclass="active". Usearia-current="page". It’s a single extra attribute that makes a world of difference.<a href="{{ .URL }}" {{ if $currentPage.IsMenuCurrent "main" . }}class="active" aria-current="page"{{ end }}>
The menu system is a workhorse. It’s not without its quirks, but once you bend it to your will, it’s incredibly flexible. You’re not just listing links; you’re building the core structure of your user’s experience. So build it well.