How to create nested aside menu with named folders?

I have a such folders/file structure:

  • folder 1
    • file_1_1.md
    • file_1_2.md
  • folder 2
    • _index.md
  • folder 3
    • file_3_1.md
    • file_3_2.md
    • folder 3_1
      • file_3_1_1.md
      • file_3_1_2.md

And I want it to render like this menu:

<ul>
  <li>
    folder 1 <!-- there is no link, because don`t exist a file folder 1/_index.md -->
    <ul>
      <li>
        <a href="/folder 1/file_1_1.md">file_1_1</a>
      </li>
      <li>
        <a href="/folder 1/file_1_2.md">file_1_2</a>
      </li>
    </ul>
  </li>
  <li>
    <a href="/folder 2">folder 2</a> <!-- there is a link, because exist a file folder 2/_index.md -->
  </li>
  <li>
    folder 3 <!-- there is no link, because don`t exist a file folder 3/_index.md -->
    <ul>
      <li>
        <a href="/folder 3/file_3_1.md">file_3_1</a>
      </li>
      <li>
        <a href="/folder 3/file_3_2.md">file_3_2</a>
      </li>
      <li>
        folder 3_1 <!-- there is no link, because don`t exist a file folder 3/folder 3_1/_index.md -->
        <ul>
          <li>
            <a href="/folder 3/folder 3_1/file_3_1_1.md">file_3_1_1</a>
          </li>
          <li>
            <a href="/folder 3/folder 3_1/file_3_1_2.md">file_3_1_2</a>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>
  1. Can I do it automatically? So that when a new folder and file is added, the menu changes. (something like sectionPagesMenu = "main")
  2. If it is not possible automatically, how to do it to indicate where in the menu there will be links, and where is just text indicating the folder?

To generate a section menu you need to recursively β€œwalk” a tree of things:

  • Menu entries, or
  • The file system, or
  • Sections (directories with _index.md files)

Creating menu entries for a section menu is (a) time consuming, (b) error prone, and (c) a hassle to maintainβ€”you have to create another entry each time you create a new page.

Walking the file system with os.ReadFile and os.ReadDir is limited to the physical file system. These template functions cannot read Hugo’s virtual file system, which means you cannot mount content from an external source. Additionally, setting the active class and the correct aria-current value requires some messy comparisons, and you have to use .GetPage for every file and directory to get the .RelPermalink and .LinkTitle values.

Which leaves us with the third and best option: walk the sections. For this to work as desired, every directory must have an _index.md file. To avoid creating links to specific directories (text only), we’ll add a front matter flag to specific _index.md files.

content structure
content/
β”œβ”€β”€ folder-1/
β”‚   β”œβ”€β”€ file-1-1.md
β”‚   β”œβ”€β”€ file-1-2.md
β”‚   └── _index.md
β”œβ”€β”€ folder-2/
β”‚   └── _index.md
β”œβ”€β”€ folder-3/
β”‚   β”œβ”€β”€ folder-3-1/
β”‚   β”‚   β”œβ”€β”€ file-3-1-1.md
β”‚   β”‚   β”œβ”€β”€ file-3-1-2.md
β”‚   β”‚   └── _index.md
β”‚   β”œβ”€β”€ file-3-1.md
β”‚   β”œβ”€β”€ file-3-2.md
β”‚   └── _index.md
└── _index.md

layouts/partials/menus/section.html
{{- /*
Renders a section menu.

@param {page} page The current page.
@param {pages} pages The collection of top-level pages to walk.

@returns {template.HTML}

@example {{- partial "menus/section.html" (dict "page" . "pages" site.Sections) }}
*/}}

<nav class="menu-section">
  <ul>
    {{- partial "section-menu/walk.html" . }}
  </ul>
</nav>

{{- define "partials/section-menu/walk.html" }}
  {{- range .pages }}
    <li>
      {{- if .Params.sectionMenuTextOnly }}
        {{ .LinkTitle }}
      {{- else }}
        {{- if .Eq $.page }}
          <a class="active" aria-current="page" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
        {{- else if in $.page.Ancestors . }}
          <a class="ancestor" aria-current="true" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
        {{- else }}
          <a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
        {{- end }}
      {{- end }}
      {{- with .Pages }}
        <ul>
          {{- partial "section-menu/walk.html" (dict "page" $.page "pages" .) }}
        </ul>
      {{- end }}
    </li>
  {{- end }}
{{- end }}

Then render the menu with something like:

<aside>
  {{ partial "menus/section.html" (dict "page" . "pages" site.Sections) }}
</aside>

To show text only (no link) for a particular page, add a front matter parameter:

+++
title = 'Folder 1'
date = 2023-05-05T13:04:46-07:00
draft = false
weight = 100
sectionMenuTextOnly = true
+++

Try it:

git clone --single-branch -b hugo-forum-topic-44216 https://github.com/jmooring/hugo-testing hugo-forum-topic-44216
cd hugo-forum-topic-44216
hugo server

Active menu items are yellow, while ancestor items are cyan.

2 Likes

thanks for such a comprehensive answer
The only thing that confuses me is that the folder 1, folder 3, folder 3_1 pages will be created, although I need them exclusively for the menu. That is, I will be able to navigate to these pages through the address bar.

To show text only (no link) for a particular page, and to avoid rendering the page, use build options instead of a custom flag in front matter.

content/folder-1/_index.md

+++
title = 'Folder 1'
date = 2023-05-05T13:04:46-07:00
draft = false
weight = 100
[_build]
render = 'never'
+++

Then make one change to the partial (line 21).

layouts/partials/menus/section.html
{{- /*
Renders a section menu.

@param {page} page The current page.
@param {pages} pages The collection of top-level pages to walk.

@returns {template.HTML}

@example {{- partial "menus/section.html" (dict "page" . "pages" site.Sections) }}
*/}}

<nav class="menu-section">
  <ul>
    {{- partial "section-menu/walk.html" . }}
  </ul>
</nav>

{{- define "partials/section-menu/walk.html" }}
  {{- range .pages }}
    <li>
      {{- if eq .Params._build.render "never" }}
        {{ .LinkTitle }}
      {{- else }}
        {{- if .Eq $.page }}
          <a class="active" aria-current="page" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
        {{- else if in $.page.Ancestors . }}
          <a class="ancestor" aria-current="true" href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
        {{- else }}
          <a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a>
        {{- end }}
      {{- end }}
      {{- with .Pages }}
        <ul>
          {{- partial "section-menu/walk.html" (dict "page" $.page "pages" .) }}
        </ul>
      {{- end }}
    </li>
  {{- end }}
{{- end }}

I prefer the original approachβ€”no 404’s when visiting any segment of the URL path. And you can easily hide these pages from your sitemap and RSS feeds based on the flag.

1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.