这个系列写到第三篇,前两篇我们把 Hugo 站点的基本目录、内容模型和 single 模板理顺了:站点能跑起来,文章能正常渲染,内容也能正常输出了。但打开首页一看,很多人会愣住——首页要么是一片空白,要么是 Hugo 默认那句简单的无样式文本。原因其实不复杂:在 Hugo 里,首页本身就是一个列表页,它走的不是 single 模板,而是 list 模板体系。这个认知没转过来,首页板块配置就总绕不过弯。这篇文章就把“首页板块配置”这件事彻底讲透,从 list 模板的查找顺序、板块拆分思路,到 Ubuntu 环境下的完整实操,再到我踩过的几个坑,一次性给齐。
1. 首页板块配置的整体思路:先理解首页为什么是 list 模板
1.1 首页本质上就是一个特殊的列表页
Hugo 里的页面类型分好几种:普通文章页(page)、栏目页(section)、首页(home)、分类页(taxonomy)、标签术语页(term)。很多新手只盯着 single 模板,因为写了一篇文章之后,最先要解决的就是“文章页怎么排版”。但首页和栏目页走的是另一套逻辑:它们不是展示“单个内容”,而是展示“一批内容的聚合结果”。
想通这一点非常关键。首页要干的事情,无非是“把全站最新文章列出来、把重点分类列出来、把归档入口放出来”,这些动作全都是列表操作。所以 Hugo 干脆把首页设计成一种特殊类型的列表页,它有一个独立的 kind,叫 home。它也参与模板查找,而且查找方式和普通栏目页非常接近。
打个比方,single 模板是酒店的客房,list 模板是酒店的前台和大堂。客房各有各的布置,但大堂的作用是把所有客人都引到该去的地方。首页就是这个大堂,它不需要自己写死哪篇文章放哪里,它只需要告诉 Hugo:把哪些内容聚合过来、按什么顺序排、每个条目渲染成什么样。这个“聚合规则”就是靠 list 模板里的 range、where、first 这些语句来实现的。
1.2 模板查找顺序:index.html、home.html、list.html 的优先级
配置首页板块之前,必须搞懂 Hugo 的模板查找顺序。我给一个常见优先级的简化版本:
| 优先级 | 模板路径 | 作用 |
|---|---|---|
| 1 | layouts/index.html | 首页专用模板,优先级最高 |
| 2 | layouts/home.html | 首页模板的另一命名 |
| 3 | layouts/_default/home.html | 默认目录下的首页模板 |
| 4 | layouts/_default/list.html | 默认 list 模板,会兜底所有列表页 |
| 5 | 主题目录下的同名模板 | 主题自带模板,优先级最低 |
换句话说,如果你在根目录layouts/下没有创建任何首页相关模板,Hugo 最终会用layouts/_default/list.html或者主题里自带的 list 模板来渲染首页。这也是为什么一个刚hugo new site出来的新站点,跑起来之后首页不是完全空白,而是有一个最简单的页面——因为系统兜底用了 list 模板。
这个查找顺序同时也是一个排障入口。很多“我改了模板但首页没变化”的情况,都是因为同时存在多个候选模板,而你改的那个优先级太低。比如你在layouts/_default/list.html里改了首页样式,但站点里其实已经有layouts/home.html,那你的改动就会完全被忽略。所以动手前,先检查layouts/下到底有哪些文件,再决定改哪个。
1.3 板块化方案选型:全写一个文件还是拆 partial
把首页拆成多个板块,在 Hugo 里大体有三种做法。
第一种是把所有区块全写进index.html。适合首页非常固定、一年到头不怎么会动的站点。缺点是只要想调整顺序或增删区块,就要翻一个长模板文件,改起来容易误伤。
第二种是把每个板块拆成独立的 partial 文件,放在layouts/partials/home/下,index.html只做骨骼拼接。这是我最推荐的做法。板块之间互不干扰,某个板块出问题可以直接单看那个文件;以后想在归档页或关于页复用同一个板块,也可以直接{{ partial "home/recent.html" . }}调过来。
第三种是配置驱动,把板块列表写在hugo.yaml或config.toml的params里,模板里用一个循环去遍历配置项渲染板块。适合多语言站点、多站点共用同一套主题的场景,也适合站点的维护者不是技术人员、需要频繁调整首页板块顺序的情况。
三种方案并不互斥。我实际项目里经常是“index 骨架 + partial 板块 + 少量配置参数”混合使用。首页的固定区域用 partial,需要动态启用的区域用配置驱动。这个后面实操部分会给出完整示例。
2. 核心模板语法与板块的常见构成
2.1 高频语法:range、where、first、GroupByDate 怎么配合
Hugo 模板使用的是 Go template 语法,刚开始接触的人会觉得有点别扭,尤其是{{ }}里嵌套各种管道符和括号。但首页板块真正高频用到的语句其实就那几个:range负责遍历,where负责过滤,first负责取前几条,GroupByDate负责按日期分组。
先看一个最容易踩的坑:range .Pages和range .Site.RegularPages的区别。.Pages返回的是当前上下文下的所有页面,但在首页这个场景里,它会把分类页、标签页、栏目页这些分支页面也算进来。如果你直接遍历.Pages,首页列表里很可能混进去一个叫 “Posts” 的栏目链接或者 “Tags” 的聚合链接。而.Site.RegularPages返回的是全站所有普通文章页,不含分支页面,是首页“最新文章列表”这种板块最稳妥的数据源。
再看where。它的典型用法是where .Site.RegularPages "Section" "posts",意思是把全站普通文章里属于posts栏目下的内容筛出来。如果你网站里既有博客文章、又有读书笔记、还有碎碎念,你可以通过多个where分别取不同栏目的文章,然后对应渲染成不同板块。
first更简单,first 5 $posts就是取前 5 条。注意它接的是一个变量,所以一般先$posts := where ...把筛选结果存起来,再用first截断。
GroupByDate是归档板块的利器,range .Site.RegularPages.GroupByDate "2006-01"会把所有文章按年月分组,每一组的.Key就是类似2025-06这样的字符串。配合.Pages再遍历,就能做出一个按月份折叠的归档列表。
2.2 最新文章板块:最常用的首页板块怎么写
最新文章区几乎是每个博客首页的标配。用 partial 拆出来大概是这样的结构,文件放layouts/partials/home/recent.html:
{{ $posts := where .Site.RegularPages "Section" "posts" }} {{ $recent := first 6 $posts }} <section class="home-section home-recent"> <h2 class="section-title">最新文章</h2> <ul class="post-list"> {{ range $recent }} <li class="post-item"> <a class="post-title" href="{{ .Permalink }}">{{ .Title }}</a> <time class="post-date" datetime="{{ .Date.Format "2006-01-02" }}"> {{ .Date.Format "2006年01月02日" }} </time> <p class="post-summary">{{ .Summary }}</p> </li> {{ end }} </ul> </section>这里有几个细节值得单独说明。
第一个是.Date.Format "2006年01月02日"。Go template 的时间格式化模板是固定的2006-01-02 15:04:05,这个日期就是 Go 语言的“参考时间”,不是随便写的占位符。很多从 PHP 或 Python 转过来的朋友第一反应是写Y-m-d或者%Y-%m-%d,结果页面上一片乱码。这是 Hugo 模板新手最容易犯的错,没有之一。
第二个是.Summary。Hugo 会自动从文章正文里截取摘要,截取长度由配置文件里的summaryLength控制,默认大概 70 个词。截取位置不一定符合你的预期,后面问题排查部分我会专门讲怎么控制摘要。
第三个是where .Site.RegularPages "Section" "posts"。如果你的博客文章不只放在posts一个栏目下,这个过滤条件会把其他栏目的文章全部过滤掉。这时候可以直接用.Site.RegularPages不加过滤,让首页展示全站最新文章;或者根据你的栏目规划,写多个板块分别展示。
2.3 分类、标签、归档板块:用列表页模板扩展首页信息密度
除了最新文章,首页通常还需要几个入口型板块,帮助访客快速找到感兴趣的内容。分类板块的写法在 Hugo 里非常简洁:
{{ $categories := .Site.Taxonomies.categories.ByCount }} {{ $topCategories := first 12 $categories }} <section class="home-section home-categories"> <h2 class="section-title">文章分类</h2> <ul class="category-list"> {{ range $topCategories }} <li> <a href="{{ .Page.Permalink }}">{{ .Page.Title }} ({{ .Count }})</a> </li> {{ end }} </ul> </section>这里要注意的是.Page.Permalink。在分类板块中,遍历得到的每一项是一个 taxonomy entry,它自身有.Permalink,但更规范的写法是拿到它对应的.Page对象,再取.Page.Permalink。.Count则代表这个分类下有多少篇文章。
标签云板块也是同样的套路,只是把categories换成tags。如果你觉得标签太多,可以用first 12限制数量,或者配合.Alphabetical、.ByCount做排序。ByCount是按文章数量从多到少排序,适合做“热门标签”展示。
归档板块则用GroupByDate:
{{ range .Site.RegularPages.GroupByDate "2006-01" }} <h3>{{ .Key }}</h3> <ul> {{ range .Pages }} <li><a href="{{ .Permalink }}">{{ .Title }}</a></li> {{ end }} </ul> {{ end }}不过我要提醒一句:分类、标签、归档、最新、推荐全部堆到首页,首页会变得特别长,首屏加载和信息密度都会失控。我自己的经验是首页最多保留三四个板块,分类和标签只展示 Top N,归档入口用一个“查看全部文章”的链接引导到独立归档页,而不是把完整归档直接铺在首页。这个取舍后面还会再提。
2.4 摘要与时间格式:两个容易出细节问题的点
摘要和时间是首页板块配置里最容易“看着差不多、实际差很多”的两个地方。
关于摘要,Hugo 的自动摘要确实省事,但有两个隐患。第一个是自动摘要可能截在某个 HTML 标签中间,导致页面样式错乱。第二个是摘要位置不是你想要的,比如文章开头几行刚好是图片说明或者短代码,截出来的摘要就很奇怪。
最稳妥的控制方式是在文章 front matter 里手动指定摘要字段,或者直接在正文里插入<!--more-->标记。Hugo 会把标记之前的内容作为摘要,而且不会把自动截断的 HTML 残留带进来。在模板里的写法依然是.Summary,Hugo 会自动区分是手动摘要还是自动摘要。
关于时间,除了上一节提到的2006格式化问题,还有一个时区问题。Hugo 默认使用本地时间,如果你在 Ubuntu 服务器上构建站点,而服务器时区和你的本地时区不一致,文章显示的日期可能比实际发布时间早一天或晚一天。解决办法是在站点配置里显式设置timeZone,比如写timeZone = "Asia/Shanghai",这样无论服务器在哪个时区,渲染出的日期都会一致。这个坑在配置了自动构建之后尤其明显。
3. Ubuntu 环境下实操:从新建站点到多板块首页跑起来
3.1 环境准备:Hugo 版本、站点目录、示例内容
先说版本问题。Ubuntu 的 apt 源里确实有 hugo,但版本可能非常老,老版本对很多新模板语法支持不完整。我建议优先用官方二进制或者 snap 安装。下面这张表是我在不同环境里的对比:
| 安装方式 | 优点 | 缺点 |
|---|---|---|
| apt install hugo | 一条命令 | 版本可能偏旧,extended 特性不一定有 |
| snap install hugo --classic | 安装方便,自动更新 | snap 版本更新策略不一定可控 |
| GitHub Releases 下载二进制 | 版本完全可控,extended 版功能全 | 手动更新稍微麻烦一点 |
如果你要用的主题依赖 SCSS/SASS 编译,那就必须装 extended 版 Hugo。普通版和 extended 版的区别就是是否内置了 SCSS 编译相关功能,很多商业主题和热门开源主题都默认要求 extended。检查版本的命令是hugo version,看到输出里有extended字样就说明没问题。
环境准备好之后,建一个测试站点:
hugo new site hugo-home-demo cd hugo-home-demo hugo new posts/first-post.md注意默认新建的文章是 draft 草稿状态,直接hugo server启动时你不会在首页看到它。本地预览草稿要用hugo server -D。这也是新手经常“页面空白”的原因之一:不是模板问题,而是内容全被标记成了草稿。
在 Ubuntu 下还有一个很容易被忽略的权限问题。如果你是从别的地方拷贝来的站点目录,或者用sudo执行过hugo new,目录里的文件属主可能是 root,导致后续写入public/时报permission denied。稳妥做法是先把站点目录权限归到当前用户:
sudo chown -R $USER:$USER ~/hugo-home-demo3.2 首页骨架与 partial 拆分:把“板块”拆出来
现在开始正式配置首页。第一步是创建首页骨架layouts/index.html。这里要根据你的主题情况决定写法:如果主题里有baseof.html,那么首页模板只需要重写main区块;如果没有 baseof,那你需要自己写完整的 HTML 结构。判断方法很简单,去看看你的主题layouts/_default/下有没有baseof.html,或者站点根目录layouts/_default/下有没有。
假设你的主题自带 baseof,那么首页骨架可以写成:
{{ define "main" }} <main class="home"> {{ partial "home/recent.html" . }} {{ partial "home/categories.html" . }} {{ partial "home/tags.html" . }} </main> {{ end }}如果你没有 baseof,就需要把<html>、<head>、<body>这些标签都写出来,然后同样在主体部分引入 partial。
第二步是创建layouts/partials/home/目录,然后把最新文章、分类、标签这几个板块分别写入对应的 partial 文件。结构大概是这样:
layouts/ ├── index.html └── partials/ └── home/ ├── recent.html ├── categories.html └── tags.html为什么要单独建一个home/子目录?因为一个站点的 partial 可能非常多,如果全部平铺在partials/下,很快就分不清哪个是给首页用的、哪个是给单页用的、哪个是给导航用的。按功能建子目录,是让模板目录变整洁的好习惯。
第三步是调整站点配置。在hugo.toml或hugo.yaml里设置必要的参数:
baseURL = "https://example.com/" title = "我的 Hugo 博客" languageCode = "zh-cn" timeZone = "Asia/Shanghai" summaryLength = 80 paginate = 10 [params] description = "记录技术、生活与思考"这里timeZone就是前面说的日期时区问题,summaryLength控制自动摘要长度,paginate虽然首页板块不一定用到,但归档页和列表页会用到。
3.3 用站点配置驱动板块:改配置就能调首页
如果你想让首页板块的顺序、数量、展示条数都可以方便调整,那就用配置驱动的方式。先在hugo.toml里增加一个板块列表:
[params] [[params.homeSections]] name = "最新文章" type = "posts" limit = 5 showDate = true [[params.homeSections]] name = "随想" type = "notes" limit = 3 showDate = false然后写一个通用板块 partial,文件放layouts/partials/home/dynamic.html:
{{ if .Site.Params.homeSections }} {{ range .Site.Params.homeSections }} {{ $posts := where $.Site.RegularPages "Section" .type }} {{ $limit := default 5 .limit }} {{ $showDate := default true .showDate }} <section class="home-section"> <h2 class="section-title">{{ .name }}</h2> <ul class="post-list"> {{ range first $limit $posts }} <li> <a href="{{ .Permalink }}">{{ .Title }}</a> {{ if $showDate }} <time datetime="{{ .Date.Format "2006-01-02" }}"> {{ .Date.Format "2006年01月02日" }} </time> {{ end }} </li> {{ end }} </ul> </section> {{ end }} {{ end }}注意这里在range里面,上下文已经从站点变成了配置项,访问站点对象时必须用$加前缀,所以写的是$.Site.RegularPages而不是.Site.RegularPages。这个细节很容易漏,漏了就报错或者页面空白。
然后在首页骨架里加一行:
{{ define "main" }} <main class="home"> {{ partial "home/recent.html" . }} {{ partial "home/dynamic.html" . }} {{ partial "home/categories.html" . }} </main> {{ end }}这样你以后想调整首页最新文章之外的板块,只需要改配置文件的params.homeSections列表,完全不用碰模板。对于多语言站点,甚至可以在不同语言配置里写不同的板块列表,同一个主题渲染出不同的首页结构。
3.4 本地调试与构建:hugo server 的几个实用参数
配置完成后,在 Ubuntu 终端里启动本地服务:
hugo server -D -p 8080-D表示把草稿也渲染出来,-p 8080指定端口,避免默认 1313 被占用。启动后终端会输出一个访问地址,一般是http://localhost:8080。你每保存一次模板或内容,页面会自动热重载。
如果页面结构复杂、模板渲染慢,可以用--templateMetrics参数查看每个模板的渲染耗时:
hugo server -D --templateMetrics这个参数会输出一张表格,列出每个模板渲染了多少次、花了多长时间。首页板块多的时候,可以用它定位到哪个 partial 是性能瓶颈。实际导出的静态站点也一样,用hugo --templateMetrics可以针对生产构建做分析。
最终生成静态文件用:
hugo默认输出到public/目录,把整个public/上传到服务器或部署平台即可。构建时如果发现旧文件残留很奇怪,可以加--gc参数做一次垃圾回收,清理掉不再使用的缓存文件。
4. 常见问题与排查技巧实录
4.1 首页空白:先查这三个位置
首页空白是最常见的问题,但排查路径其实很固定。先检查有没有内容:content/目录下有没有文章?文章是不是全是 draft 状态?用hugo server -D启动后能不能看到?
接着检查模板路径:layouts/index.html是放在了layouts/根目录,而不是layouts/partials/下。这个错误我见过太多次,模板文件放错了目录,改了半天都不生效。
最后检查终端日志:Hugo 启动时如果模板有语法错误,终端会直接报错,并把出错文件和行号标出来。很多空白页面不是“没有模板”,而是“模板坏了”。
给你一个排障顺序列表:
hugo version确认版本find layouts -type f查看模板文件实际位置hugo server -D看终端输出有没有报错- 浏览器强制刷新(Ctrl+Shift+R),排除缓存
- 用
curl http://localhost:1313/看返回的 HTML 是否正常
4.2 .Pages 和 .RegularPages 混用导致栏目页混进列表
症状很典型:首页文章列表里出现 “Posts”“About”“Tags” 这种不像文章的链接。原因是模板里用了.Pages而不是.RegularPages。
.Pages是“当前上下文下的所有页面”,包括栏目页、分类页这些分支页面。首页作为一个特殊列表页,它的.Pages范围其实是整个站点的所有页面集合,所以会混入各种非文章页面。而.RegularPages只包含普通内容页,分支页面一律排除。
如果你实在不小心用了.Pages,补救方式有两种。一种是换成.RegularPages,另一种是在where里过滤 kind:
{{ range where .Site.RegularPages "Kind" "page" }} ... {{ end }}顺便记住 Hugo 的几种 kind 值:home是首页,section是栏目页,page是普通文章页,taxonomy是分类聚合页,term是具体某个标签或分类的术语页。搞清楚这几个值,过滤逻辑就不会写错。
4.3 摘要截断和 HTML 标签残留怎么破
如果你发现首页列表里某些摘要带着半个<p>标签,或者排版突然乱了,大概率是自动摘要截断在了 HTML 标签中间。Hugo 的自动摘要虽然会尽量保证标签闭合,但在复杂正文里依然可能出现奇怪结果。
最可靠的解决方案是使用手动摘要标记<!--more-->。在文章正文想截断的位置插入一行:
这里是摘要部分。 <!--more--> 这里是正文剩余部分。Hugo 会把<!--more-->之前的内容作为摘要,模板里依然用.Summary输出,不需要改模板。这个方法的好处是完全可控,摘要一定是你想展示的那段话。
另外一个方案是在 front matter 里手动写summary字段:
--- title: "我的文章" summary: "这是一段手写的摘要内容。" date: 2025-06-01 ---如果模板里输出摘要后还想去掉所有格式,可以加plainify过滤器:{{ .Summary | plainify }}。但要注意这样会把摘要里的加粗、链接全部变成纯文本,适合卡片式布局,不适合富文本摘要。
4.4 模板不生效与缓存问题
“我改了模板,页面就是不变”这个问题,通常不是 Hugo 的锅,而是下面几类原因。
第一类是文件名或路径问题。比如你把index.html写成了indexs.html,或者放进了partials/目录。Hugo 不会管你的文件名是否合理,它只会按约定去查找。多一个字母少一个字母,它就直接跳过,用兜底模板渲染。
第二类是优先级冲突。前面 1.2 节讲过,Hugo 会按顺序查找首页模板。如果你站点里同时存在layouts/home.html和layouts/index.html,但你改的是低优先级的那个,自然看不出来变化。
第三类是缓存。本地hugo server一般会热重载,但你改hugo.toml这类配置文件时偶尔不会自动刷新。遇到这种情况,手动重启一下hugo server。浏览器端也可能缓存旧的 CSS 和 JS,按 Ctrl+Shift+R 强制刷新,或者开着开发者工具的 Disable cache 选项。
第四类是 partialCached 使用不当。如果你给某个 partial 加了缓存,但又没有传足够多的缓存 key,内容更新后可能还渲染旧数据。这种情况下,检查你的partialCached调用,确保把该参与变化的变量都作为参数传进去。
4.5 Ubuntu 相关的权限、版本和端口问题
最后整理几个 Ubuntu 环境下的特别问题。
权限问题前面提过,最主要的症状是hugo构建时报permission denied,或者hugo new创建文件后你没法编辑。用ls -l查看文件属主,然后sudo chown -R $USER:$USER 站点目录解决。
版本问题主要表现为某些模板语法不支持。老版本 Hugo 对.Site.RegularPages支持是没问题的,但新版支持的hugo.yaml配置文件、resources处理、图片滤镜等特性在老版本上会直接报错。所以装完 Hugo 之后第一件事是hugo version,确认版本,别等部署到服务器上跑 production build 才发现问题。
端口问题比较直接:hugo server默认监听 1313,如果这个端口被其他进程占用,终端会提示地址已被使用。用下面命令找到占用进程:
lsof -i :1313然后按 PIDs 决定是 kill 掉旧进程,还是换一个端口启动hugo server -p 8080。
还有一个不算坑但容易被忽略的小细节:Ubuntu 上如果你用nohup或 systemd 做自动部署,环境变量里的 PATH 可能跟手动执行终端时不一样,导致 cron 或 systemd 找不到hugo命令。解决办法是用绝对路径运行hugo,比如/usr/local/bin/hugo,或者在 systemd service 文件里显式指定Environment=PATH=/usr/local/bin:/usr/bin:/bin。
我自己在实际折腾首页板块时最大的体会是:板块不是越多越好,信息层级比信息数量重要。刚开始我恨不得把最新文章、分类、标签、归档、推荐全放首页,结果首屏特别长,访客反而不知道先看什么。后来砍到只剩“最新文章 + 重点分类 + 归档入口”三个板块,打开速度和浏览体验都好了。另外一个小技巧:把首页的模板改动用git管理起来,每次改完先hugo --gc清理再本地预览,确认没问题再部署,能省掉很多“线上和本地不一样”的麻烦。如果你现在正卡在首页空白或者列表混入栏目页的坑里,按上面 4.1 和 4.2 的顺序查一遍,基本都能解决。