解决 Hugo 分页器返回空页面的诡异问题:.Paginate 不能被多次调用
目录
问题背景
在将博客的文章列表从 JavaScript 分页迁移到 Hugo 官方分页时,遇到了一个非常诡异的问题:
- ✅ Hugo 构建成功,显示生成了 42 个分页页面
- ✅
.RegularPages有 360 篇文章 - ✅ 直接
range .RegularPages可以正常显示文章 - ❌ 但是
.Paginator.Pages始终返回 0
{{/* 这样可以看到 360 篇文章 */}}
{{ range .RegularPages }}
{{ .Title }}
{{ end }}
{{/* 但分页器是空的! */}}
{{ $paginator := .Paginator }}
分页器中的文章数: {{ len $paginator.Pages }} {{/* 输出:0 */}}
排查过程
第一步:检查配置
首先怀疑是配置问题。Hugo v0.128+ 的分页配置格式发生了变化:
# ❌ 旧版本写法(已废弃)
paginate = 10
paginatePath = "page"
# ✅ 新版本写法
[pagination]
pagerSize = 10
path = "page"
修改配置后问题依旧。
第二步:尝试不同的调用方式
尝试了各种方法调用分页器:
{{/* 方法 1:使用 .Paginator 属性 */}}
{{ $paginator := .Paginator }} {{/* 返回 0 */}}
{{/* 方法 2:使用 .Paginate 方法 */}}
{{ $paginator := .Paginate .RegularPages }} {{/* 返回 0 */}}
{{/* 方法 3:使用 .Pages */}}
{{ $paginator := .Paginate .Pages }} {{/* 返回 0 */}}
{{/* 方法 4:过滤 section */}}
{{ $posts := where site.RegularPages "Section" "posts" }}
{{ $paginator := .Paginate $posts }} {{/* 返回 0 */}}
所有方法都失败了!
第三步:手动实现分页
既然 Hugo 的分页器不工作,尝试手动实现:
{{ $allPages := .RegularPages }}
{{ $pageSize := 10 }}
{{ $offset := mul (sub $currentPage 1) $pageSize }}
{{ $currentPagePosts := after $offset (first (add $offset $pageSize) $allPages) }}
{{ range $currentPagePosts }}
{{/* 可以显示文章 */}}
{{ end }}
手动分页可以工作,但点击分页链接会 404,因为 Hugo 开发服务器只会为使用 .Paginate 的页面生成分页 URL。
真相大白
使用 grep 搜索所有调用 .Paginate 的地方:
grep -r "\.Paginate\|\.Paginator" themes/xiaoten/layouts/
结果发现在 themes/xiaoten/layouts/partials/meta/post.html 中有这样一行:
{{/* meta/post.html - 第 6 行 */}}
{{ if ne .Page.Kind "page" }}
{{ $paginator := .Paginate (where .Pages "Section" "blog") }}
{{/* 生成 SEO 分页链接 */}}
<link rel="first" href="{{ $paginator.First.URL }}" />
<link rel="last" href="{{ $paginator.Last.URL }}" />
{{ end }}
问题找到了!
meta/post.html在<head>中先调用了.Paginate- 但是过滤的是
"blog"section(实际 section 是"posts") - 所以返回了 0 个页面
- Hugo 不允许在同一个页面渲染过程中多次调用
.Paginate - 后续在
list.html中再调用.Paginate时已经失效
Hugo 分页器的重要规则
根据
Hugo 官方文档
,.Paginate 有以下限制:
规则 1:每个页面只能调用一次
{{/* ❌ 错误:多次调用 */}}
{{ $paginator1 := .Paginate .Pages }}
{{ $paginator2 := .Paginate .RegularPages }} {{/* 第二次调用会失败 */}}
{{/* ✅ 正确:只调用一次,然后使用 .Paginator 属性 */}}
{{ $paginator := .Paginate .Pages }}
{{ $samePaginator := .Paginator }} {{/* 获取同一个分页器 */}}
规则 2:在 partial 中调用会影响主模板
如果在 partial 中调用了 .Paginate,主模板中就不能再次调用:
{{/* partials/meta.html */}}
{{ $paginator := .Paginate .Pages }}
{{/* layouts/_default/list.html */}}
{{ partial "meta.html" . }}
{{ $paginator := .Paginate .Pages }} {{/* ❌ 失败!已经在 partial 中调用过了 */}}
规则 3:必须传入内置页面集合
.Paginate 只接受 Hugo 的内置页面集合,不能传入通过 where、first 等过滤后的自定义切片:
{{/* ❌ 错误:传入自定义变量 */}}
{{ $filtered := where .Pages "Type" "posts" }}
{{ $paginator := .Paginate $filtered }} {{/* 可能返回空 */}}
{{/* ✅ 正确:直接传入内置集合 */}}
{{ $paginator := .Paginate .Pages }}
解决方案
修复 meta/post.html
将错误的 section 名称改为正确的,并且使用 .Pages 而不是过滤:
{{/* themes/xiaoten/layouts/partials/meta/post.html */}}
{{ if eq .Section "posts" }}
{{ if ne .Page.Kind "page" }}
{{/* 修复:改为 .Pages,让 Hugo 自动处理 */}}
{{ $paginator := .Paginate .Pages }}
{{ if $paginator }}
<link rel="first" href="{{ $paginator.First.URL }}" />
<link rel="last" href="{{ $paginator.Last.URL }}" />
{{ if $paginator.HasPrev }}
<link rel="prev" href="{{ $paginator.Prev.URL }}" />
{{ end }}
{{ if $paginator.HasNext }}
<link rel="next" href="{{ $paginator.Next.URL }}" />
{{ end }}
{{ end }}
{{ end }}
{{ end }}
修复 list.html
由于 meta/post.html 已经调用了 .Paginate,这里直接使用 .Paginator 属性:
{{/* themes/xiaoten/layouts/_default/list.html */}}
{{- define "main" -}}
<div class="wrapper list-page">
<header class="header">
<h1 class="header-title center">{{ i18n .Title | default .Title }}</h1>
</header>
<main class="page-content" aria-label="Content">
{{/* meta/post.html 已经调用了 .Paginate,这里直接使用 .Paginator */}}
{{ $paginator := .Paginator }}
{{/* 按年份分组显示当前页的文章 */}}
{{ range $index, $page := $paginator.Pages }}
{{ $year := $page.Date.Format "2006年" }}
{{ if eq $index 0 }}
<h2 class="post-year">{{ $year }}</h2>
{{ else }}
{{ $prevPage := index $paginator.Pages (sub $index 1) }}
{{ $prevYear := $prevPage.Date.Format "2006年" }}
{{ if ne $year $prevYear }}
<h2 class="post-year">{{ $year }}</h2>
{{ end }}
{{ end }}
{{ partial "postCard" $page }}
{{ end }}
</main>
{{/* 分页导航 */}}
{{ if gt $paginator.TotalPages 1 }}
{{ partial "pagination.html" . }}
{{ end }}
</div>
{{- end -}}
分页导航组件
创建独立的分页导航 partial:
{{/* themes/xiaoten/layouts/partials/pagination.html */}}
{{ $paginator := .Paginator }}
<div class="pagination-wrap">
<ul class="pagination pagination-default">
{{/* 首页按钮 */}}
<li class="page-item{{ if not $paginator.HasPrev }} disabled{{ end }}">
{{ if $paginator.HasPrev }}
<a class="page-link" href="{{ $paginator.First.URL }}">««</a>
{{ else }}
<a class="page-link" aria-disabled="true" tabindex="-1">««</a>
{{ end }}
</li>
{{/* 上一页 */}}
<li class="page-item{{ if not $paginator.HasPrev }} disabled{{ end }}">
{{ if $paginator.HasPrev }}
<a class="page-link" href="{{ $paginator.Prev.URL }}">«</a>
{{ else }}
<a class="page-link" aria-disabled="true" tabindex="-1">«</a>
{{ end }}
</li>
{{/* 页码 - 带省略号的智能分页 */}}
{{ $adjacent := 2 }}
{{ $lastPrinted := 0 }}
{{ range $paginator.Pagers }}
{{ $showLink := false }}
{{ if or (le $paginator.TotalPages (add (mul $adjacent 2) 3))
(eq .PageNumber 1)
(eq .PageNumber $paginator.TotalPages)
(and (ge .PageNumber (sub $paginator.PageNumber $adjacent))
(le .PageNumber (add $paginator.PageNumber $adjacent))) }}
{{ $showLink = true }}
{{ end }}
{{ if $showLink }}
{{/* 显示省略号 */}}
{{ if gt (sub .PageNumber $lastPrinted) 1 }}
<li class="page-item disabled">
<a class="page-link">...</a>
</li>
{{ end }}
{{/* 页码按钮 */}}
<li class="page-item{{ if eq .PageNumber $paginator.PageNumber }} active{{ end }}">
{{ if eq .PageNumber $paginator.PageNumber }}
<a class="page-link">{{ .PageNumber }}</a>
{{ else }}
<a class="page-link" href="{{ .URL }}">{{ .PageNumber }}</a>
{{ end }}
</li>
{{ $lastPrinted = .PageNumber }}
{{ end }}
{{ end }}
{{/* 下一页 */}}
<li class="page-item{{ if not $paginator.HasNext }} disabled{{ end }}">
{{ if $paginator.HasNext }}
<a class="page-link" href="{{ $paginator.Next.URL }}">»</a>
{{ else }}
<a class="page-link" aria-disabled="true" tabindex="-1">»</a>
{{ end }}
</li>
{{/* 末页 */}}
<li class="page-item{{ if not $paginator.HasNext }} disabled{{ end }}">
{{ if $paginator.HasNext }}
<a class="page-link" href="{{ $paginator.Last.URL }}">»»</a>
{{ else }}
<a class="page-link" aria-disabled="true" tabindex="-1">»»</a>
{{ end }}
</li>
</ul>
</div>
调试技巧
遇到分页器问题时,可以添加这些调试信息:
<div style="background: #ffc; padding: 10px; margin: 10px 0;">
<strong>DEBUG 信息:</strong><br>
页面类型: {{ .Kind }}<br>
Section: {{ .Section }}<br>
.Pages 数量: {{ len .Pages }}<br>
.RegularPages 数量: {{ len .RegularPages }}<br>
.Paginator.Pages 数量: {{ len .Paginator.Pages }}<br>
总页数: {{ .Paginator.TotalPages }}<br>
当前页: {{ .Paginator.PageNumber }}
</div>
使用 grep 查找所有分页器调用:
# Linux/Mac
grep -r "\.Paginate\|\.Paginator" themes/
# Windows PowerShell
Select-String -Path "themes\**\*.html" -Pattern "\.Paginate|\.Paginator"
经验总结
- 一个页面只能调用一次
.Paginate:包括所有 partial 和 layout - 优先在最早执行的模板中调用:比如
<head>中的 meta partial - 后续使用
.Paginator属性获取:不要重复调用.Paginate - 使用内置页面集合:
.Pages、.RegularPages等,避免过滤后的变量 - 检查 section 名称:确保过滤条件正确
- 善用调试信息:在开发时显示分页器状态
参考资料
更新日志:
- 2025-11-16:初次发布,记录
.Paginate多次调用导致返回空页面的问题
© 转载需附带本文链接,依据 CC BY-NC-SA 4.0 发布。
猜你喜欢
评论