How Hugo's Menu System Works: Defining Menus in Front Matter vs Site Configuration

Hugo's menu system assembles a hierarchical navigation tree at build time by merging menu entries defined in site configuration files, page front matter, and automatic section pages, exposing the final structure through site.Menus for template rendering.

Hugo's menu system is a core component of the gohugoio/hugo static site generator, responsible for building hierarchical navigation structures at build time. It aggregates menu entries from multiple sources—site configuration, individual page front matter, and automatic section pages—into a unified tree exposed to templates via site.Menus.

The Three Sources of Menu Entries

Hugo collects raw menu entries from three distinct sources before assembling the final hierarchy.

Site Configuration

The static menus block in your site configuration (config.toml, config.yaml, or config.json) defines global menu entries independent of any specific content page. These entries typically define permanent navigation items like "Home" or "About" that may not correspond to individual content files.

Page Front Matter

Individual content pages can declare their membership in one or more menus through the menus (or legacy menu) key in front matter. This method connects specific content to navigation structures, allowing dynamic menu generation based on your content hierarchy.

Automatic Section Menus

When sectionPagesMenu is set in site configuration (e.g., sectionPagesMenu = "main"), Hugo automatically creates menu entries for every top-level section (like /blog/ or /docs/). This occurs in site.assembleMenus() without requiring manual entry definition.

How Hugo Assembles the Menu Hierarchy

The assembly process occurs in hugolib/site.go within the assembleMenus() method (lines 1398–1475). This function implements a three-phase pipeline:

  1. Flattening and enrichment: Hugo iterates over all three sources (config, pages, sections) and creates a temporary flat map keyed by menu name + entry identifier. For each entry, it:

    • Resolves pageRef to an actual Page object using s.getPage
    • Generates fully-qualified URLs when no page reference exists via s.createNodeMenuEntryURL
    • Attaches page metadata (title, URL, etc.) through navigation.SetPageValues
  2. Parent-child resolution: Hugo detects hierarchical relationships via the parent field in each menu entry. Entries specify their parent using the parent's identifier value.

  3. Tree construction: A second pass builds the children map and populates the top-level navigation.Menus structure—a map[string]navigation.Menu exposed to templates as site.Menus.

Defining Menus in Front Matter vs Configuration

Site Configuration Method

Define static menu entries in your configuration file for items that don't correspond to specific content pages:

[menus]

  [[menus.main]]
    identifier = "home"
    name       = "Home"
    url        = "/"
    weight     = 1

  [[menus.main]]
    identifier = "about"
    name       = "About"
    url        = "/about/"
    weight     = 2
    [menus.main.params]
      class = "highlight"

Front Matter Method

Add pages to menus directly in content files. The navigation/pagemenus.go file (lines 44–97) handles three possible front matter shapes:

Single menu assignment:

title: "Contact"
menus: "main"

Multiple menu assignment:

title: "Blog"
menus:
  - "main"
  - "footer"

Structured configuration with hierarchy:

title = "Products"
[menus.main]
  parent = "products"
  weight = 20
  pre    = "<i class='fa fa-box'></i>"
  [menus.main.params]
    class = "nav-product"

The structured format allows specifying parent (using the parent's identifier), weight for ordering, and pre/post for HTML markup surrounding the link.

When to Use Which

  • Use site configuration for static navigation items (Home, About, Contact) that may not have dedicated content pages or when you need precise control over URLs independent of content structure.
  • Use front matter when menu items correspond directly to content pages, allowing the menu to reflect your content hierarchy automatically. This keeps navigation logic close to the content it represents.

Accessing Menus in Templates

After assembly, menus are available through site.Menus. The navigation.MenuEntry type provides properties like URL, Name, Weight, Parent, and Children:

{{ $menu := site.Menus.main }}
<ul>
  {{ range $menu.ByWeight }}
    <li class="{{ with .Params.class }}{{ . }}{{ end }}">
      <a href="{{ .URL }}">{{ .Name }}</a>
      {{ if .HasChildren }}
        <ul>
          {{ range .Children }}
            <li><a href="{{ .URL }}">{{ .Name }}</a></li>
          {{ end }}
        </ul>
      {{ end }}
    </li>
  {{ end }}
</ul>

The MenuEntry struct (defined in navigation/menu.go) includes MenuConfig fields (identifier, name, URL, weight, params) and maintains a slice of child MenuEntry pointers for the hierarchical tree.

Summary

  • Hugo's menu system aggregates entries from site configuration, page front matter, and automatic section pages into a unified hierarchical tree during the build process.
  • The assembly logic resides in hugolib/site.go within assembleMenus() (lines 1398–1475), which flattens sources, resolves page references, and builds parent-child relationships.
  • Front matter parsing in navigation/pagemenus.go (lines 44–97) supports string, slice, or structured table formats for defining menu entries per page.
  • Site configuration defines static navigation items via the menus block, while front matter connects specific content pages to menus using the menus key.
  • Final menus are exposed to templates as site.Menus.<name>, containing MenuEntry objects with URL, Name, Weight, Parent, and Children properties.

Frequently Asked Questions

What is the difference between defining menus in config versus front matter?

Site configuration (config.toml/yaml/json) is ideal for static navigation items like "Home" or "About" that may not correspond to specific content files or require custom URLs independent of your content structure. Front matter definitions attach menu entries directly to content pages, making the navigation dynamic and content-driven. According to the Hugo source code in navigation/pagemenus.go, front matter supports three shapes: a single string, a slice of strings, or a structured table with full MenuConfig options.

How do I create nested menu items in Hugo?

Nested menus rely on the parent field in your menu entry configuration. First, define a parent item with a unique identifier in your site configuration or front matter. Then, set the parent field of child entries to match that identifier. During assembly in hugolib/site.go, Hugo detects these relationships and populates the Children slice on each MenuEntry struct. In templates, you can check .HasChildren and range over .Children to render sub-menus.

Can I add a single page to multiple menus?

Yes. In front matter, use the slice syntax to assign a page to multiple menus simultaneously. For example, menus: ["main", "footer"] in YAML or menus = ["main", "footer"] in TOML. The parsing logic in navigation/pagemenus.go detects array values and creates a separate MenuEntry for each menu name specified, all pointing to the same page. This allows you to display the same content in different navigation contexts (e.g., main navigation and footer links) without duplication.

What is sectionPagesMenu and how does it work?

sectionPagesMenu is a site configuration option that automatically generates menu entries for your top-level content sections. When you set sectionPagesMenu = "main" (or any menu name) in your config, Hugo iterates over all section pages during assembleMenus() in hugolib/site.go and creates a MenuEntry for each section using the section name as the identifier. This provides a quick way to build dynamic navigation for blogs, documentation, or other section-based content structures without manually defining each entry in configuration or front matter.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →