解决 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 }}

问题找到了!

  1. meta/post.html<head> 中先调用了 .Paginate
  2. 但是过滤的是 "blog" section(实际 section 是 "posts"
  3. 所以返回了 0 个页面
  4. Hugo 不允许在同一个页面渲染过程中多次调用 .Paginate
  5. 后续在 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 的内置页面集合,不能传入通过 wherefirst 等过滤后的自定义切片:

{{/* ❌ 错误:传入自定义变量 */}}
{{ $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"

经验总结

  1. 一个页面只能调用一次 .Paginate:包括所有 partial 和 layout
  2. 优先在最早执行的模板中调用:比如 <head> 中的 meta partial
  3. 后续使用 .Paginator 属性获取:不要重复调用 .Paginate
  4. 使用内置页面集合.Pages.RegularPages 等,避免过滤后的变量
  5. 检查 section 名称:确保过滤条件正确
  6. 善用调试信息:在开发时显示分页器状态

参考资料


更新日志:

  • 2025-11-16:初次发布,记录 .Paginate 多次调用导致返回空页面的问题
评论