Hugo Page 方法 HasMenuCurrent:精准标记导航菜单祖先级高亮状态
【免费下载链接】hugoThe world’s fastest framework for building websites.项目地址: https://gitcode.com/gh_mirrors/hu/hugo
导读
HasMenuCurrent是 Hugo 模板系统中用于判断"当前页面是否位于某个菜单项的子级或后代层级"的核心方法,常与IsMenuCurrent搭配使用,以在导航菜单中正确输出ancestor等祖先级高亮样式。本文以 docs/content/en/methods/page/HasMenuCurrent.md 为主线,结合源码实现与测试用例,讲解该方法的语义、调用签名、使用前提以及递归高亮的完整实战模板,帮助你构建可访问性良好的多级导航菜单。
方法概述:签名与返回类型
HasMenuCurrent是定义在Page对象上的方法,用于报告:给定的 Page 对象是否与给定菜单中、给定菜单项下某个子菜单项所关联的 Page 对象匹配。
其方法签名为:
PAGE.HasMenuCurrent MENU MENUENTRY| 参数 | 类型 | 说明 |
|---|---|---|
PAGE | Page | 调用方法的当前页面对象,模板中通常使用$currentPage := .获取 |
MENU | string | 菜单标识符(menu ID),如"main"、"footer",对应site.Menus.main |
MENUENTRY | MenuEntry | 被检查的菜单项对象,来自对site.Menus.main的range迭代 |
返回值类型为bool:当给定菜单项存在子菜单项(child menu entry)与当前页面关联时返回true,否则返回false。
关键语义:章节(section)页面的后代匹配
原文档特别强调了一个重要的语义细节:
如果与菜单项关联的
Page对象是一个章节(section)页面,那么该方法对该章节的任何后代页面(descendant)也都返回true。
这意味着在 Hugo 的页面树中,只要某个菜单项指向一个 section(如/blog),那么/blog下的所有文章页面都会让HasMenuCurrent返回true。这一设计使得在渲染博客类站点时,父级"Blog"菜单项可以自动获得祖先高亮,无需为每篇文章单独配置。
与 IsMenuCurrent 的分工协作
HasMenuCurrent与IsMenuCurrent是一对互补方法:
IsMenuCurrent:判断当前页面是否恰好等于给定菜单项所关联的页面,用于标记"当前激活"(active)状态;HasMenuCurrent:判断当前页面是否位于给定菜单项的子菜单树中(或作为章节的后代),用于标记"祖先"(ancestor)状态。
两个方法的详细对比可参见 IsMenuCurrent 文档,其调用签名与参数形式完全一致。
完整实战模板:菜单祖先级高亮
以下是 HasMenuCurrent 原文档 提供的核心模板示例,它遍历site.Menus.main中的每个菜单项,并根据当前页面与菜单项的关系分别渲染三种状态:
{{ $currentPage := . }} {{ range site.Menus.main }} {{ if $currentPage.IsMenuCurrent .Menu . }} <a class="active" aria-current="page" href="{{ .URL }}">{{ .Name }}</a> {{ else if $currentPage.HasMenuCurrent .Menu . }} <a class="ancestor" aria-current="true" href="{{ .URL }}">{{ .Name }}</a> {{ else }} <a href="{{ .URL }}">{{ .Name }}</a> {{ end }} {{ end }}模板逻辑说明:
- 先用
{{ $currentPage := . }}将当前页面上下文保存为变量,避免在range循环内上下文被覆盖; IsMenuCurrent命中时输出class="active"与aria-current="page",标记当前页;HasMenuCurrent命中时输出class="ancestor"与aria-current="true",标记祖先层级;- 其余情况输出普通链接。
上述两个aria-current属性对屏幕阅读器等辅助技术至关重要,page表示当前页面本身,true表示该链接指向当前页面的祖先级位置。
支持嵌套菜单的递归版模板
原文档进一步指向了 菜单模板文档,其中提供了一份可处理嵌套(多级)菜单的递归 partial 模板,核心高亮判断逻辑与上述示例一致:
{{- $page := .page }} {{- range .menuEntries }} {{- $attrs := dict "href" .URL }} {{- if $page.IsMenuCurrent .Menu . }} {{- $attrs = merge $attrs (dict "class" "active" "aria-current" "page") }} {{- else if $page.HasMenuCurrent .Menu .}} {{- $attrs = merge $attrs (dict "class" "ancestor" "aria-current" "true") }} {{- end }} ... {{- with .Children }} <ul> {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }} </ul> {{- end }} {{- end }}在该模板中,HasMenuCurrent与IsMenuCurrent的返回值通过merge动态合并进<a>标签属性字典,随后在range .Children中递归调用自身,从而实现任意深度的菜单结构都能正确标注激活与祖先状态。
使用前提:front matter 定义或 pageRef 属性
原文档以> [!NOTE]形式给出了使用该方法的一个强制前提:
使用此方法时,你必须在 front matter 中定义菜单项,或者在项目配置中定义菜单项时指定
pageRef属性。
原因在于:HasMenuCurrent(以及IsMenuCurrent)需要将菜单项与具体的Page对象进行比对。若菜单项既没有在页面 front matter 中声明,也没有通过pageRef关联到具体页面,Hugo 无法为菜单项解析出对应的Page对象,方法将永远返回false。
方式一:在 front matter 中定义
在页面 front matter 中声明其归属菜单,页面本身即为菜单项关联的 Page 对象:
title = 'My Post' menu: main: weight: 30方式二:在项目配置中使用 pageRef
在hugo.toml等项目配置中通过pageRef指向页面的逻辑路径:
[[menus.main]] name = 'Products' pageRef = '/products' weight = 10 [[menus.main]] name = 'Hardware' pageRef = '/products/hardware' parent = 'Products' weight = 1pageRef支持指向各种页面类型,其取值规则可参考 菜单配置文档:
| 页面类型 | pageRef 示例 |
|---|---|
| home | / |
| page | /books/book-1 |
| section | /books |
| taxonomy | /tags |
| term | /tags/foo |
当配置中的pageRef无法匹配到任何页面时,HasMenuCurrent与IsMenuCurrent均返回false(详见 MENUENTRY.PageRef 方法文档),这也是配置菜单时必须确保路径正确的原因。
源码级原理:HasMenuCurrent 的判定逻辑
要真正理解该方法的行为边界,可以阅读其底层实现 navigation/pagemenus.go。该方法定义在pageMenus类型上,逻辑可分为三个阶段:
第一阶段:章节祖先判定
func (pm *pageMenus) HasMenuCurrent(menuID string, me *MenuEntry) bool { if !types.IsNil(me.Page) && me.Page.IsSection() { if ok := me.Page.IsAncestor(pm.p); ok { return true } }当菜单项关联的页面存在且为 section 时,直接调用页面树的IsAncestor方法判断当前页面是否为该 section 的后代。这正是原文档中"章节的后代页面也返回 true"这一语义的代码出处。
第二阶段:菜单归属校验
if !me.HasChildren() { return false } menus := pm.pagem.Menus() if m, ok := menus[menuID]; ok { for _, child := range me.Children { if child.isEqual(m) { return true } if pm.HasMenuCurrent(menuID, child) { return true } } }若菜单项没有子项则直接返回false;否则取当前页面声明的菜单集合(Menus()),递归检查子菜单项是否与当前页面相等或匹配。
第三阶段:无菜单声明时的兜底比对
if pm.p == nil { return false } for _, child := range me.Children { if child.isSamePage(pm.p) { return true } if pm.HasMenuCurrent(menuID, child) { return true } } return false }即使页面未在 front matter 中声明菜单,只要子菜单项通过pageRef解析出的页面与当前页面一致,同样返回true。整个方法通过递归遍历子菜单项实现任意层级的匹配,这就是它能支撑深层嵌套菜单的原因。
测试验证:行为被测试用例明确锁定
仓库中的集成测试对HasMenuCurrent的行为进行了断言,可作为权威的行为参考。在 hugolib/menu_test.go 的TestMenusSectionPagesMenu测试中,配置了sectionPagesMenu = "sect"自动生成章节菜单,并断言:
- 渲染
sect1/p1页面时,菜单项/sect1/(Section One)输出HasMenuCurrent标记,因为p1是sect1章节的子页面; - 渲染
sect2/p3页面时,菜单项/sect2/(Sect2s)输出HasMenuCurrent,而其他章节菜单项输出-(即 false); - 只有页面自身声明的菜单项
sect1/p1输出IsMenuCurrent。
另一处TestMenuHasMenuCurrentSection(对应 issue 9846,见 hugolib/menu_test.go)则验证了嵌套菜单场景:菜单项Tests指向 section/tests,其子项Test 1指向/tests/test-1。在渲染/tests列表页时,Tests菜单项得到IsMenuCurrent = true、HasMenuCurrent = false——即使Tests含有子菜单项,只要当前页面恰好是该菜单项自身,就只命中IsMenuCurrent而非HasMenuCurrent。
这些测试共同确认了一个关键区分:IsMenuCurrent命中当前页自身,HasMenuCurrent命中当前页所在的祖先层级,两者互斥且优先级明确——模板中应先判断IsMenuCurrent,再判断HasMenuCurrent。
相关资源
- 完整菜单渲染指南:docs/content/en/templates/menu.md
- 菜单定义方式(自动 / front matter / 项目配置):docs/content/en/content-management/menus.md
- 项目配置中的菜单项属性与嵌套示例:docs/content/en/configuration/menus.md
- 姊妹方法
IsMenuCurrent:docs/content/en/methods/page/IsMenuCurrent.md pageRef的解析行为与失败兜底:docs/content/en/methods/menu-entry/PageRef.md- 方法底层实现:navigation/pagemenus.go
- 行为验证测试:hugolib/menu_test.go
【免费下载链接】hugoThe world’s fastest framework for building websites.项目地址: https://gitcode.com/gh_mirrors/hu/hugo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考