侧边栏这活儿,不干不知道,一干全是细节。这个系列更新到第92期,前六部分我们先后把Ubuntu上的Hugo环境搭好、主题跑起来、内容组件填满,博客的基本盘算是稳了。但每次打开自己的博客,总感觉少了点啥——访客进来之后除了顺着正文往下滚,没有第二种探索路径。这不就是侧边栏缺席嘛。于是我把主题改造的重点放到侧边导航栏上,这一篇就是完整的实操记录。
先给个定心丸:这篇文章不是从零教你写CSS,而是把"Hugo主题里侧边导航栏到底怎么改、菜单数据怎么配、移动端怎么适配、哪些坑我替你们踩过"讲透。适合已经把Hugo基础环境搭好、接下来要精修博客门面的同学。按我实测的经验,跟着这篇走,一下午就能把侧边栏从"能看"做到"能用"。
1. 侧边栏到底该放什么:先把信息架构想明白
动代码之前,我先停下来问了问自己一个问题:侧边栏的作用是什么?答案其实很简单,它是整个站点的常驻导航层。页面滚动时它不动,无论访客看到哪篇文章,视野里始终有这一列。这么珍贵的"固定曝光位",放什么不放什么,优先级怎么排,直接决定了博客好不好用,而不只是好不好看。
我见过不少博客的侧边栏是"大杂烩":友情链接二十个、标签云铺满一整屏、热门文章十大篇、还有一堆统计图标。访客进来第一眼是懵的,不知道先点哪儿。信息架构的原则是分层,侧边栏从功能上大体可以拆成三层,想清楚这三层,后面每个模块的去留就好决定了。
1.1 三个层次的导航需求
第一层是全局导航,解决"这个博客有什么"的问题。首页、归档、分类、标签、关于我,这类条目是主菜单,必须放最上面,且永远保持清晰可点。这一层是侧边栏存在的根本理由,砍谁的优先级都不能砍它。
第二层是发现工具,解决"我能不能快速找到想看的文章"的问题。最近更新、分类聚合、关键词搜索,都归在这一层。它们不要求列表完整,但是要快、要准,访客不用开新页就能判断"这里值不值得点进去"。
第三层是辅助信息,解决"这个博客是谁、怎么联系"的问题。博主头像卡片、订阅入口、社交链接、友情链接,属于加分项。这些模块有当然好,但绝对不该喧宾夺主,更不该挤占前两层的空间。
这三层放在一起,侧边栏的信息优先级就出来了:主菜单永远第一,发现工具次之,辅助信息垫底。我自己的站点最后定下的结构就是按照这个顺序排的,后面不管是大改样式还是新增模块,都先过一遍这个框架再动手,省了好多回头返工的功夫。
1.2 我的最终模块方案
这是我在自己博客里实际采用的模块清单,先给你一个整体视野:
| 模块 | 优先级 | 说明 | 渲染方式 |
|---|---|---|---|
| 主菜单 | P0 | 首页、归档、分类、标签、关于 | Hugo菜单遍历输出 |
| 搜索框 | P0 | 站内内容检索入口 | 独立partial |
| 最近文章 | P1 | 最新5篇文章带日期 | Hugo内置method遍历 |
| 分类列表 | P1 | 按文章数排序的分类聚合 | 数据模板 + weight |
| 博主卡片 | P2 | 头像+一句话简介 | 静态HTML |
模块定下来之后,再去动布局文件和样式就有的放矢了。很多人一上来就纠结CSS颜色、字体、圆角大小,其实都是次要的。先解决"上面有什么",再解决"长什么样"。这一节看着像废话,但真的能帮你省掉后面反复改样式的时间——我最初就是没想清楚,结果一个标签云模块来回调了三版,最后干脆砍掉了。
2. Hugo内置的菜单系统:从配置文件到模板渲染的完整链路
Hugo的菜单系统,是侧边栏最核心的基础设施。很多人一开始会想:侧边栏导航嘛,直接在模板里写死几行<a href>不就行了?不行,因为主题要改、博客侧重得要调、页面位置要挪、激活状态要自动高亮——写死的HTML没法维护。正确的做法是使用Hugo的菜单API,把导航数据从模板里抽出来,做成配置驱动。这一章我们把数据流完整走一遍。
2.1 在配置文件里声明菜单
Hugo支持在站点的配置文件中声明菜单,我这里用的是hugo.toml(老版本叫config.toml,后面坑里会细说)。最常见的主菜单是这样定义的:
[menu.main] [[menu.main]] name = "首页" url = "/" weight = 10 [[menu.main]] name = "归档" url = "/archive/" weight = 20 [[menu.main]] name = "分类" url = "/categories/" weight = 30 [[menu.main]] name = "标签" url = "/tags/" weight = 40 [[menu.main]] name = "关于" url = "/about/" weight = 50这里有几个字段值得说一下。name是菜单项显示文字,url是链接地址,weight是排序权重——Hugo按weight从小到大排序,所以我习惯用10、20、30这样的间隔,中间留出余量,以后插入新菜单项不用改后面所有数字。identifier也是常用字段,后面讲激活状态的时候它很关键。
2.2 也可以把菜单放在文章front matter里
菜单不一定要全部写在配置文件里。如果某个页面希望单独出现在导航栏,可以直接在那一篇内容的front matter里声明:
--- title: "关于我" date: 2025-01-01 menu: main: name: "关于" weight: 50 ---这样做的优势是"内容跟着页面走":以后删掉这个页面,菜单项也随之消失,不会出现配置文件里还挂着一条死链的尴尬。缺点是菜单分散在各文件里,全局调整的时候要翻很多文件。我的习惯是:全局主菜单写死在配置文件,个别特殊入口用front matter补充,两边靠weight合并排序,互不冲突。如果你跟我一样只维护一个个人博客,这个组合基本够用了。
2.3 模板里怎么把菜单渲染出来
配置好菜单之后,模板端通过.Site.Menus.main拿到所有菜单项。一个最直接的单层菜单渲染是这样的:
{{ range .Site.Menus.main }} <a class="sidebar-nav-item" href="{{ .URL }}">{{ .Name }}</a> {{ end }}如果要处理多级菜单,就要遍历Children:
{{ range .Site.Menus.main }} <a class="sidebar-nav-item" href="{{ .URL }}">{{ .Name }}</a> {{ if .HasChildren }} <ul class="sidebar-nav-children"> {{ range .Children }} <li><a href="{{ .URL }}">{{ .Name }}</a></li> {{ end }} </ul> {{ end }} {{ end }}Hugo菜单项本身是一个结构体,Children是它的子菜单数组,HasChildren是判断方法。这些都属于很基础但是绕不开的东西,建议真的打开编辑器敲一遍,不要只是复制粘贴看完就完。菜单这块你亲手改一次,比读十篇文档都管用。
3. 侧边栏布局文件改造:从主题的partials层切入
Hugo主题的布局都在layouts目录下,按用途分成了partials、_default、taxonomy等子目录。侧边导航栏属于可复用组件,正常应该放在layouts/partials/sidebar.html这类文件里。很多主题会把侧边栏渲染逻辑拆得很细,比如有header.html、widgets.html、footer.html之类的partial组合。改造的第一步,是搞清楚自己用的主题到底在哪个文件渲染侧边栏。
3.1 找到sidebar对应文件与override机制
如果你用的是别人开发的主题,主题代码一般在themes/主题名/layouts/下面。但我不建议直接改themes/里的文件,因为以后主题一升级,你改的东西全被覆盖。Hugo提供了override机制:在站点根的layouts/目录里放同名文件,会优先于主题内的文件生效。
站点根目录/ ├── themes/ │ └── mytheme/ │ └── layouts/ │ └── partials/ │ └── sidebar.html ← 主题自带的原版 └── layouts/ └── partials/ └── sidebar.html ← 你自己的override版本我自己就是这么做的:从主题里复制一份sidebar.html到layouts/partials/,然后在副本上改。这样升级主题不慌,改坏了也随时能回退。如果你实在找不到侧边栏对应的partial,就在主题目录里grep -r "sidebar" layouts/搜一下关键词,比一个一个文件翻快得多。
3.2 手写一个带激活状态高亮的菜单渲染
侧边栏菜单和普通页脚链接最大的区别,就是要有"当前页面高亮"。访客浏览文章时,侧边栏里"归档"要亮;浏览分类页时,"分类"要亮。实现这个效果需要用IsMenuCurrent或HasMenuCurrent方法。
这里有个容易出错的地方:range会把当前上下文切换成菜单项,这时候要拿到"当前正在渲染的页面对象",必须用$。所以我的模板是这样写的:
<nav class="sidebar-menu"> {{ $currentPage := . }} {{ range .Site.Menus.main }} <a class="sidebar-menu-link{{ if or ($currentPage.IsMenuCurrent "main" .) ($currentPage.HasMenuCurrent "main" .) }} sidebar-menu-link-active{{ end }}" href="{{ .URL }}"> {{ .Name }} </a> {{ end }} </nav>关键点就两行:先把.赋给$currentPage,再用or组合IsMenuCurrent和HasMenuCurrent。前者精确匹配当前URL和菜单项,后者用于上级或分类页被匹配的情况,比如你在某篇文章里,同时希望"归档"或"分类"处于激活态,就可以用HasMenuCurrent配合identifier做包含关系判断。没有这个高亮,导航栏就少了一半的可用性,访客永远不知道自己在站点的哪个位置。
3.3 把常用小部件一起补上
菜单搞定了,侧边栏还剩搜索框和最近文章。搜索框我用的是一个最简单可靠的方式,直接把搜索动作指向站点内建的搜索落地页:
<form class="sidebar-search" action="/search/" method="get"> <input type="search" name="q" placeholder="搜索文章..." /> </form>/search/对应的落地页需要你另外做,Hugo本身没有内建搜索功能,它是纯静态站点。如果不想搭搜索页面,可以退而求其次用站点地图,或者干脆把搜索框这一节跳过,不丢人。
最近文章模块用Hugo内置的集合方法就能实现,不用额外依赖:
<h3>最近文章</h3> <ul class="sidebar-recent"> {{ range first 5 (where .Site.RegularPages "Type" "in" (slice "post" "posts")) }} <li> <a href="{{ .RelPermalink }}">{{ .Title }}</a> <span class="sidebar-recent-date">{{ .Date.Format "2006-01-02" }}</span> </li> {{ end }} </ul>注意我用的是RegularPages而不是Pages。RegularPages过滤掉了分类页、标签页这类归档列表页,只保留真正的内容页面,否则你的"最近文章"里会出现一串"分类""标签"这种不是文章的条目,这个坑我踩过,必须提前给你提个醒。
4. 响应式适配:桌面固定侧栏与移动端抽屉的切换
侧边栏的样式实现,我推荐用CSS Grid来做整页布局,简单、可控,不用写复杂的浮动和清除浮动。桌面端是经典的"侧栏+正文"两栏,移动端则要把侧栏藏起来,通过按钮滑出。两个形态分开处理,各自都清爽。
4.1 用Grid搭出页面骨架
以我的博客为例,页面最外层包了一个类名page的容器:
.page { display: grid; grid-template-columns: 280px 1fr; gap: 2rem; max-width: 1200px; margin: 0 auto; padding: 1rem; }左边280px是侧栏固定宽度,右边1fr是正文自适应剩余空间。如果你的主题有更复杂的结构,比如右侧还有"相关文章"栏,可以再加一列,但别贪多,两栏已经够清爽了。
我还给侧栏本身加了一个sticky定位,让它在正文滚动时能吸在视口顶部,长文浏览时始终能看到导航入口:
.sidebar { position: -webkit-sticky; position: sticky; top: 1rem; align-self: start; max-height: calc(100vh - 2rem); overflow-y: auto; }align-self: start很关键,不然Grid和Sticky组合下侧栏会被拉满整个网格高度,sticky就不生效了。overflow-y: auto则防止侧栏内容本身就比视口高的时候把屏幕卡死,出现滚动不出去的烦人问题。
4.2 移动端抽屉式导航
窄屏上两栏布局放不下,标准做法是把侧栏变成抽屉,默认藏在屏幕左边,点按钮再滑出来。我用了最朴素的方案:一个汉堡按钮控制一个类名。
@media (max-width: 768px) { .page { grid-template-columns: 1fr; } .sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 260px; z-index: 1000; transform: translateX(-100%); transition: transform 0.25s ease-in-out; } .sidebar.is-open { transform: translateX(0); } }配合一小段JavaScript做类名切换:
const toggleButton = document.querySelector('.sidebar-toggle'); const sidebar = document.querySelector('.sidebar'); const overlay = document.querySelector('.sidebar-overlay'); toggleButton.addEventListener('click', function () { sidebar.classList.toggle('is-open'); overlay.classList.toggle('is-visible'); }); overlay.addEventListener('click', function () { sidebar.classList.remove('is-open'); overlay.classList.remove('is-visible'); });transition配合transform是在移动端性能上比较稳的方案,千万不要用left: -260px到left: 0这样去直接变更位置属性,动画会掉帧,看着特别廉价。overlay遮罩记得给个半透明背景,点遮罩关闭抽屉的操作习惯也得保留,不然用户体验会大打折扣。
4.3 不同屏幕下模块的取舍
响应式不只是"能用",而是"在不同设备上给最合适的体验"。我实测下来,手机端侧栏弹出空间有限,标签云这种几十个标签的模块,在260px宽的抽屉里会排到崩溃。所以我加了一条规则,小屏下隐藏低优先级模块:
@media (max-width: 768px) { .sidebar-widgets, .sidebar-tags { display: none; } }搜索框则保留,因为移动端站内搜索的需求一点不比桌面端低。说白了,display:none隐藏的只是"锦上添花",导航这种"刚需"永远要保留。做响应式之前先排优先级,比调一堆断点参数更有用。断点数值也不要闭着眼用别人的,先看看自己站点在哪些宽度下布局开始"别扭",再去设断点,这样才是真正适合自己的方案。
5. 实测踩坑记录:四个最容易翻车的点
这一章是全篇最有价值的部分,因为都是我自己改侧边导航栏时真的碰到过的问题,每一个都折腾了半小时以上。你提前看过,至少能少走一半弯路。
5.1 配置文件命名:config.toml还是hugo.toml
Hugo早期版本默认配置文件是config.toml,后来官方把默认名改成了hugo.toml。如果你照着老教程的路径手动建了config.toml,又在新版本里另外建了一份hugo.toml,就会遇到"为什么菜单没生效"的初级疑问——因为站点只读其中一份,你改的是另一份。建议统一用hugo.toml,新装环境默认就是它。
还有个隐藏问题:如果两份文件都存在,有些Hugo版本会提示配置冲突。我遇到过一次,侧边栏明明配了菜单却不渲染,查了半天才发现是两份配置文件在打架。解决方式很简单,留hugo.toml,删掉或重命名另一个,清理干净再启动。
5.2 菜单项URL失效与404
菜单写死URL的坏处是:如果目标页面后来改动了permalinks或者文章被删除,菜单链接就会变成404。新版Hugo里,我更推荐用PageRef字段指向内容路径,而不是裸写url:
[[menu.main]] name = "归档" pageRef = "/archive" weight = 20PageRef相当于告诉Hugo"去内容库里找这个路径的页面",由Hugo解析出真实的最终URL。如果页面不存在,菜单项可能干脆不渲染,或者构建时就给出警告,至少不会把404留到线上给访客。这个改动很小,但对长期维护博客的人来说特别值得。
5.3 激活状态死活不亮
菜单能正常显示,但"当前页面高亮"始终不生效,这是被问得最多的情况之一。原因几乎都是模板上下文问题:range循环里上下文被切换成菜单项,此时直接调用IsMenuCurrent,拿到的"当前页面"根本不对。
我一开始也是放着{{ if .IsMenuCurrent }}直接判断,结果永远不亮。正确的姿势就是第3章那个代码:先用$currentPage := .在当前上下文里存下当前页面对象,再去调IsMenuCurrent方法。另外,如果页面类型是分类聚合页,要记得用HasMenuCurrent配合identifier做范围匹配,不然分类下的文章详情页永远不会点亮"分类"菜单。
5.4 中文菜单项乱码或渲染异常
中文内容写进TOML,最可能的坑是文件编码。Ubuntu下用不同编辑器保存文件时,偶尔会带着BOM或错误的字符集。确保你的配置文件以UTF-8无BOM格式保存,一般就不会乱码。VSCode、Vim这些主流工具默认都没问题,反而是从网页复制粘贴配置的时候,容易带进隐藏字符。
另一个容易被忽略的是TOML字符串引号的规范。name = "分类"这种双引号字符串没问题;但如果你写成了单引号字面量字符串,某些Hugo版本的解析器对特殊字符的处理会更严格。我的建议:菜单相关的字符串统一用双引号包起来,不要为了少敲一个Shift键用单引号,真出问题排查起来你都不知道从哪儿查起。
Debug也要给一个标准姿势:启动hugo server --logLevel info看构建日志,模板里犹豫不决的地方用{{ printf "%#v" . }}把结构体内容打印出来直接看。侧边栏看着是个小功能,但踩坑时照着这个思路排查,基本都能在半小时内定位到问题根源。
我在实际改侧边栏的过程里,还有一个体会特别深:别在样式细节上过度纠结。什么圆角度数、字色饱和度、间距是3px还是5px,渲染出来看两眼觉得顺眼就过,别为一个像素折腾到半夜。真正值得花时间的是菜单数据结构、激活状态和响应式边界这几个"逻辑层"的东西,它们决定侧边栏的质量下限。样式不满意,后面哪天真想重新设计,框架稳了,改起来也就是换个CSS的事。这个系列走到这里,侧边栏这关算是过了,下一篇我准备折腾博客的部署和自动化更新,到时候再继续聊。