这是《Ubuntu上用Hugo搭建私人博客》系列文章的第六篇。前五篇把环境、站点初始化、主题选型、内容组织这些地基活都干完了这一篇专门聊侧边导航栏。很多人觉得侧边栏就是个装饰放两个链接完事实际折腾下来发现里面门道不少菜单层级怎么设计、高亮状态怎么判断、窄屏下怎么处理、多语言站点怎么办每一环都有坑。这篇文章我按实战顺序来写先讲清楚侧边导航栏在Hugo里的几种实现思路再给出一套可以直接抄的配置方案然后是样式定制和响应式处理最后整理我踩过的那些问题。不管你是照着主题文档配菜单还是打算自己撸一个侧边栏模板这篇都能给你省下不少时间。1. 侧边导航栏到底该放什么内容1.1 先想清楚导航架构再做配置很多人搭博客上来就急着写代码结果菜单改来改去越改越乱。我建议先花10分钟想清楚一个问题你的侧边栏要给访问者提供什么博客侧边栏通常承担两类职责一类是站内导航告诉读者这个博客里有什么比如首页、文章归档、分类、标签、关于页面另一类是信息聚合把最近更新、热门文章、订阅入口这些动态内容放在显眼位置。这两类内容混在一起侧边栏就会显得杂乱。我的习惯是优先保证导航的清晰度。侧边栏顶部放站点名称和一句简介下面紧跟主导航菜单再往下才考虑放标签云、归档列表这类辅助信息。如果你的博客刚起步内容还不多侧边栏甚至可以只保留导航和搜索框留白也是一种设计。1.2 菜单层级的取舍与Hugo的菜单模型Hugo的菜单系统相比纯HTML写死的导航有个明显优势菜单结构可以灵活配置而且和页面内容解耦。你把菜单定义写在配置文件里模板里只需要循环输出之后加菜单项、调顺序都不需要碰HTML。不过Hugo的菜单模型非常简单就是扁平化的菜单项加一个可选的父子关系。它没有那种复杂的多级嵌套权限控制但对付博客导航完全够用。我的建议是菜单层级控制在两层以内顶层放首页文章分类这种大项需要细分的地方比如文章下面挂全部文章归档年份最多两层就行了。超过两层用户找东西的成本会上升移动端展示也麻烦。这里要注意一个概念区别Hugo的菜单menu和内容组织section/taxonomy不是一回事。菜单是你主动指定的一组链接可以指向任意URL而分类、标签这些是内容本身的属性Hugo会自动生成对应的列表页。侧边导航栏里的分类和标签入口本质上是指向这些自动生成的列表页但入口顺序、显示名称是你在菜单配置里控制的。2. 三条实现路线从躺平到全定制2.1 路线一直接用主题自带的侧边栏配置现在很多Hugo主题都把侧边栏做成了可配置模块。你在配置文件里打开sidebar开关然后指定要显示哪些区块比如作者介绍、分类列表、最近文章、标签云主题会自动渲染。这种方案对不想碰代码的朋友最友好缺点也很明显你能配置的只是主题作者预设的选项想调整区块顺序、改样式细节往往得去翻主题源码。2.2 路线二用Hugo的Menu系统驱动侧边栏这套方案是我今天重点要讲的。思路是你把菜单项定义在配置或者页面前置参数里然后写一个sidebar的partial模板让菜单数据自动生成导航HTML。它的好处是导航内容集中管理改菜单项不用去翻模板文件同时借助Hugo模板里的菜单上下文方法可以比较精确地判断当前页面属于哪个菜单项从而实现高亮定位。2.3 路线三完全手写侧边栏模板不依赖主题自己写死整个侧边栏的HTML。这样做自由度最高想怎么布局就怎么布局但后续维护成本也最高每加一个导航链接都要改模板文件而且很容易出现一个细节忘了同步。我自己平时不推荐这种写法除非你想练手或者做个高度定制的首页。把三条路线放一起对比更直观方案上手难度定制自由度后续维护适合场景主题自带配置低低低快速起站不想碰代码MenuPartial模板中中高低大多数个人博客全手写模板高高高深度定制想完全掌控我最终选了路线二核心原因就一条让数据驱动布局而不是让布局绑定数据。文章会越来越多分类会调整菜单项跟着变如果每次调整都要去翻HTML维护负担太重了。3. 实操从零配置一套侧边导航栏3.1 在config里定义菜单项Hugo的菜单定义可以放在站点配置文件里也可以放在单个页面的前置参数里。对于侧边栏这种全站统一的导航放站点配置更合适。以toml格式为例# config.toml [menu] [[menu.sidebar]] name 主页 url / weight 1 [[menu.sidebar]] name 文章 url /posts/ weight 2 [[menu.sidebar]] parent 文章 name 全部文章 url /posts/ weight 21 [[menu.sidebar]] parent 文章 name 分类 url /categories/ weight 22 [[menu.sidebar]] parent 文章 name 标签 url /tags/ weight 23 [[menu.sidebar]] name 关于 url /about/ weight 3几个关键字段说明一下name菜单项显示的文字。url链接地址。这里有个容易踩的坑url写相对路径还是绝对路径取决于你有没有把博客部署到子路径。如果你的站点将来要跑在类似https://example.com/blog/这种子目录下url要以baseURL里的子路径开头或者直接用相对路径。我就遇到过博客部署到子目录后侧边栏链接全部多了一截目录的情况后面排查半天才发现是url写法和baseURL配置不匹配。weight排序权重数值越小越靠前。父子菜单之间我习惯让子菜单的权重跟在父菜单后面留出余量比如父菜单是10子菜单从11开始这样以后插入新菜单项不用大改。parent父菜单项的identifier或name。这里需要注意如果父菜单项的name在网站其他地方被改了子菜单会找不到父级所以更稳妥的做法是给父菜单项加上identifier字段然后子菜单的parent指向这个identifier。3.2 在页面级配置里挂菜单项除了站点全局菜单页面前置参数里也可以单独定义菜单项。这个常用于把某个独立页面加入导航。比如关于页面你可以在它的front matter里写--- title: 关于 menu: sidebar: name: 关于我 weight: 3 ---这样配置的效果和全局配置差不多区别在于菜单项的归属页面级菜单跟着内容页面走如果哪天删了这个页面菜单项自动消失不会在配置文件里留下死链。我的习惯是全局菜单管架构、页面级菜单管特殊入口两者结合用。不过要注意一个坑如果同一个菜单名在页面前置参数和站点配置里都定义过Hugo会合并而不是覆盖具体行为取决于你的Hugo版本。想精确控制的话我建议主要用站点配置统一管理侧边栏页面级菜单只在少数场景用避免出现两个同名菜单项的尴尬局面。3.3 编写sidebar模板有了菜单数据接下来写模板。在layouts/partials/sidebar.html里放以下内容{{ $currentPage : . }} aside classsidebar div classsidebar-header a href{{ .Site.BaseURL | relURL }} classsidebar-title{{ .Site.Title }}/a {{ with .Site.Params.description }} p classsidebar-desc{{ . }}/p {{ end }} /div nav classsidebar-nav ul classsidebar-menu {{ range .Site.Menus.sidebar }} {{ if .HasChildren }} li classmenu-item menu-item-has-children {{ if $currentPage.HasMenuCurrent sidebar . }}active{{ end }} a href{{ .URL | relURL }}{{ .Name }}/a ul classsub-menu {{ range .Children }} li classsub-menu-item {{ if $currentPage.IsMenuCurrent sidebar . }}active{{ end }} a href{{ .URL | relURL }}{{ .Name }}/a /li {{ end }} /ul /li {{ else }} li classmenu-item {{ if $currentPage.IsMenuCurrent sidebar . }}active{{ end }} a href{{ .URL | relURL }}{{ .Name }}/a /li {{ end }} {{ end }} /ul /nav {{ if .Site.Params.sidebarSearch }} div classsidebar-search form action{{ search/ | relURL }} methodget input typetext nameq placeholder搜索文章… /form /div {{ end }} /aside这段模板里最关键的是两个上下文方法IsMenuCurrent判断当前页面是否就是某个菜单项指向的页面。比如你在文章归档页对应菜单项就会被标为高亮。HasMenuCurrent判断当前页面是否在某个菜单项的子树范围内。比如你正在看某篇文章这篇文章属于文章这个大分类下的内容那么文章这个父菜单项会被标为高亮。这一对方法配合使用能让导航高亮状态显得很智能父级、子级、叶子页面都能正确命中对应的菜单项。很多刚开始接触Hugo模板的人会忽略这两个方法自己用eq .Permalink .URL去判断结果文章页、分类页、标签页的高亮逻辑写出一堆if else还容易判断错。能用内置方法解决的就别自己造轮子。3.4 把侧边栏挂到页面布局里模板文件写好之后要让它出现在页面上。Hugo主题的布局文件一般都有一个基础模板baseof.html页面内容都嵌入到它里面。你需要在baseof或对应的single/list模板里引用刚才的partial!-- layouts/_default/baseof.html -- !DOCTYPE html html lang{{ .Site.Language.Lang }} head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title{{ .Title }} | {{ .Site.Title }}/title {{ block head . }}{{ end }} /head body div classlayout {{ partial sidebar.html . }} main classcontent {{ block main . }}{{ end }} /main /div /body /html布局结构上我用一个.layout容器包住侧边栏和主内容区接下来样式部分会让它们左右排布。如果你用的是某个现成主题不要直接改主题的baseof.html而是用Hugo的模板覆盖机制在站点根目录的layouts目录下放同名文件Hugo会优先使用你放的文件这样主题升级时你的定制不会被冲掉。我在这里吃过一次亏刚接触Hugo时直接改了安装目录下的主题模板后来主题一更新我所有改动全部被覆盖文章里引用的class也全部失效。从那以后我所有定制只放在站点根目录的layouts和assets里主题本身保持原样。4. 样式定制与响应式布局4.1 用Sass组织侧边栏样式Hugo在资源处理方面做得比较成熟你可以用assets/scss目录放样式源码然后在模板里用resources.Get加toCSS处理。下面是一个侧边栏样式的示例// assets/scss/sidebar.scss .sidebar { width: 280px; padding: 24px; border-right: 1px solid #e8e8e8; position: sticky; top: 0; height: 100vh; overflow-y: auto; background: #fff; } .sidebar-header { margin-bottom: 24px; } .sidebar-title { font-size: 1.5rem; font-weight: 700; color: #222; text-decoration: none; } .sidebar-desc { margin-top: 8px; font-size: 0.875rem; color: #777; } .sidebar-menu { list-style: none; margin: 0; padding: 0; .menu-item { margin-bottom: 4px; a { display: block; padding: 8px 12px; border-radius: 6px; color: #444; text-decoration: none; transition: background 0.2s; :hover { background: #f5f5f5; } } .active a { background: #f0f0f0; color: #111; font-weight: 600; } } .sub-menu { list-style: none; margin: 4px 0 4px 16px; padding: 0; .sub-menu-item { a { display: block; padding: 6px 10px; font-size: 0.9rem; color: #666; text-decoration: none; border-radius: 4px; :hover { background: #fafafa; } } .active a { color: #333; font-weight: 600; background: #f5f5f5; } } } }样式上我遵循几个原则导航项的点击热区要够大所以用了块级展示加内边距高亮状态和hover状态要有区分度hover用浅灰当前页高亮用深一点的底色加粗字体子菜单相对父级缩进视觉上形成层级感。4.2 响应式处理窄屏下的侧边栏侧边栏的响应式处理是实操中最容易翻车的部分。桌面端280px宽的栏很舒服但手机屏幕上如果硬塞一个280px的固定栏内容区几乎没地方放。处理思路通常是两种一种是侧边栏折叠窄屏下把侧边栏藏起来点击菜单按钮后从左侧滑出遮罩层覆盖内容区。这种方案适合侧边栏内容比较丰富的情况。另一种是侧边栏降级窄屏下直接把侧边栏挪到主内容下方变成普通的信息区块。这种方案实现简单但用户在页面上找导航的路径变长了。我选了折叠方案用一小段原生JavaScript控制展开和收起button classsidebar-toggle aria-label切换导航 span/spanspan/spanspan/span /button// assets/js/sidebar.js (function () { const toggle document.querySelector(.sidebar-toggle); const sidebar document.querySelector(.sidebar); const overlay document.querySelector(.sidebar-overlay); if (!toggle || !sidebar) return; toggle.addEventListener(click, function () { sidebar.classList.toggle(open); if (overlay) overlay.classList.toggle(visible); document.body.classList.toggle(sidebar-open); }); })();在Hugo里挂载JavaScript资源时用resources.Get加js.Build处理能自动做压缩和bundle。样式方面断点我选在768px这也是大多数框架的常见平板竖屏断点media (max-width: 768px) { .layout { position: relative; } .sidebar { position: fixed; left: 0; top: 0; transform: translateX(-100%); transition: transform 0.3s ease; z-index: 100; width: 280px; height: 100vh; } .sidebar.open { transform: translateX(0); } .sidebar-overlay { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); z-index: 99; } .sidebar-overlay.visible { display: block; } .sidebar-toggle { display: inline-flex; flex-direction: column; gap: 4px; padding: 10px; span { display: block; width: 22px; height: 2px; background: #333; } } }这里有个细节桌面端按钮默认隐藏只有窄屏下才显示所以要在媒体查询里把display从none改成inline-flex。如果忘了写桌面端初始状态按钮会在宽屏上露出来很影响观感。5. 常见问题与排查记录5.1 菜单不显示的几类原因症状是侧边栏模板引用了.Site.Menus.sidebar但页面渲染出来菜单区域是空的。遇到这种情况第一反应不是去查模板而是先在命令行跑一下hugo看构建过程有没有报提示。如果构建正常但菜单还是空的最可能的原因就是配置格式不对。特别注意配置文件里[menu]的层级关系。toml格式下每个菜单项是[[menu.sidebar]]方括号必须是两层写成[menu.sidebar]的话后面的键值会被解析成menu.sidebar这个单一值后面再写一项就会覆盖前面的。这个问题在Hugo新手里出现频率极高往往检查几遍都看不出来。5.2 高亮状态不生效模板里用了IsMenuCurrent和HasMenuCurrent但当前页面并没有高亮。大部分原因是菜单项的url和页面实际生成的路径不一致。比如文章列表页长这样https://example.com/posts/你的菜单url写成了/posts少了结尾斜杠Hugo在做字符串比较时不认为它们相等。另外Hugo有给URL做relURL处理的逻辑如果你在模板里对.URL做了额外包装也可能破坏比较结果。判断这类问题最有效的方法是右键页面选择查看源文件看看生成的HTML里菜单项的href值再对照浏览器地址栏的URL。两边不一致问题基本出在url写法上两边一致但还是不高亮才需要考虑模板逻辑。5.3 子菜单层级显示异常子菜单项没挂在父菜单下面全部平铺在侧边栏里。排查思路是看配置里父菜单项的identifier和子菜单的parent是否对得上。注意Hugo有个隐蔽行为如果父菜单项没有显式写identifier这个值默认等于name。子菜单的parent写的是父级name的时候正常情况能匹配上但中文场景下name里有空格、全半角符号差异就很容易失配。使用identifier是更稳妥的做法。5.4 常见问题速查现象可能原因解决办法菜单整体不显示menu配置层级不对检查toml里[[menu.sidebar]]是否两层方括号菜单项顺序不对忘记设置weight给所有同级菜单项设置weight数值越小越靠前当前页不高亮url与页面路径不一致对比生成HTML和浏览器地址统一斜杠子菜单跑平parent指向的name或identifier不存在给父菜单加identifier子菜单parent指向它点击菜单后页面404站点部署在子路径url没带子路径或没加relURL模板中用relURL处理.URL或配置里写对完整路径中文菜单样式间断某些浏览器字体渲染问题在字体栈中把中文常用字体放前面比如Noto Sans SC, PingFang SC, Microsoft YaHei5.5 调试侧边栏的几个实用命令排查菜单问题我常常开两个终端。一个跑hugo server另一个用来查配置# 查看当前站点激活的所有菜单定义 hugo config menus # 检测配置与资源问题带verbose输出 hugo --verbose --debug # 查看渲染后的HTML中菜单部分快速确认url hugo serverhugo config menus这个命令很多人不知道它在调试菜单场景下特别好用直接输出当前所有菜单的键值结构。输出结构里看到sidebar菜单不存在那就是配置没被正确加载看到菜单项存在但url值异常那就是你的url写法有问题。6. 还有一个被忽略的细节多语言站点的菜单处理如果你的博客以后要开双语甚至多语言侧边栏菜单的name字段会变成一个麻烦。Hugo内置了i18n机制菜单项的name字段支持从语言包取翻译值。写法是name menu.about这种形式Hugo会用它作为i18n的key去查询翻译查不到时才把原字符串原样显示。这个机制虽然好用但有个陷阱一旦菜单项name是这种带点号的伪翻译keyparent字段的匹配也会按翻译key而不是显示文本来算。也就是说子菜单的parent要填menu.articles而不是文章。所以从一开始我就强调用identifier管理父子关系不仅是为了防止名字改动导致失配也是为多语言留的后路。如果你只有单一语言i18n这个特性可以先不管但建议从第一天起就养成写identifier的习惯。等真到了要多语言的那天你会发现改起来只是一行配置的事。7. 侧边栏之外别忘了页面内导航侧边导航栏解决的是从任何页面快速跳转到站内主要区域的问题但它替代不了文章页内部的目录导航。如果你写的文章很长侧边栏里最好再放一个本页目录区块把文章内的小标题链接进去。Hugo里可以通过.TableOfContents变量直接输出目录配合CSS渲染成树形结构。我的做法是侧边栏分为上下两段上段是全站导航菜单下段是当前文章的目录。全站导航无论在哪一页都是固定的目录区域只在文章页显示。这样读者的视线不必来回切换侧边栏的信息承载效率也更高。具体实现上目录区用{{ if .TableOfContents }}{{ .TableOfContents }}{{ end }}判断是否存在目录有则输出没有就不渲染。不过这里要注意性能和缓存问题如果侧边栏里渲染了文章目录那么每个页面的侧边栏内容都不一样构建缓存时就不能把partial输出整个缓存下来。站点文章多了以后这个影响不算大但我见过有人为了节省构建时间把侧边栏全部缓存结果目录一直显示上一篇文章的。省那几秒构建时间不值得。侧边导航栏做到这个程度已经能满足大多数博客的日常使用了。从配置菜单、编写模板、定制样式、处理响应式到排查问题几个环节都走通之后你会对Hugo模板化的内容组织方式有更深的理解。后面如果想让侧边栏更智能化可以考虑按section自动生成菜单项或者结合阅读数据和标签热度动态调整展示内容这些都是在现有骨架上的扩展核心思路不会变。
阅读完成 · 觉得有帮助?