How Hugo's Pagination Works for Listing Pages with Custom Grouping

Hugo's pagination system processes grouped collections by converting them into PagesGroup objects and splitting them via the splitPageGroups function to ensure groups never span multiple pages, delivering paginated results through the Paginator and Pager types defined in resources/page/pagination.go.

Hugo's static site generator provides sophisticated pagination capabilities for list pages, particularly when working with custom groupings like date archives or taxonomies. Understanding how Hugo's pagination works for listing pages with custom grouping requires examining the core implementation in the gohugoio/hugo repository, specifically the interaction between the Paginator type, grouping methods, and the splitting logic that preserves group integrity across page boundaries.

The Core Pagination Types

Paginator and Pager Structures

At the heart of Hugo's pagination lies the page.Paginator type defined in [resources/page/pagination.go](https://github.com/gohugoio/hugo/blob/master/resources/page/pagination.go). This structure maintains a slice of paginated elements—either plain Pages or PagesGroup objects—and generates a corresponding slice of Pager objects, with each Pager representing a single page of results.

When you invoke .Paginate or access .Paginator on a list template, Hugo initializes this system and prepares the collection for segmentation.

Collection Conversion via ToPages and ToPagesGroup

Before splitting occurs, Hugo normalizes the input collection using helper functions ToPages and ToPagesGroup. If your collection has been grouped using methods like .GroupBy, .GroupByDate, or similar grouping functions, the conversion returns a PagesGroup type rather than a standard Pages slice. This distinction determines which splitting algorithm Hugo applies to the data.

Splitting Logic for Grouped Collections

The splitPageGroups Function

For grouped collections, Hugo employs the splitPageGroups function (approximately lines 221‑259 in pagination.go). This function first flattens the grouped structure while preserving group keys, then re-chunks the flattened list into paginator-sized segments. Crucially, it rebuilds groups so that a group never straddles a page boundary—each group remains intact within a single pager.

This differs from the splitPages helper used for ungrouped collections, which simply walks the slice in steps of the configured page size and creates sub-slices without considering logical groupings.

Mutual Exclusivity of Pages and PageGroups

Each Pager object exposes its content through either the .Pages() method or the .PageGroups() method, located around lines 89‑115 in pagination.go. These methods are mutually exclusive—a given pager holds either flat pages or grouped pages, never both simultaneously. When you paginate a grouped collection, you must iterate over .PageGroups in your templates to access the grouped structure.

URL Generation and Performance Optimization

paginationURLFactory

The URL structure for paginated pages is generated by the paginationURLFactory function (lines 998‑1008 in pagination.go). This factory formats paths according to your site's pagination.path configuration setting, ensuring consistent URL patterns like /page/2/ or custom pagination paths.

First-Invocation Caching

The paginator implements caching on first invocation. Once a list page initializes its paginator, Hugo caches the result, meaning subsequent calls on the same list page return identical results even if invoked with different arguments. This optimization prevents redundant processing during template rendering.

Implementation Examples

When working with grouped content in Hugo templates, you pass the grouped collection directly to the .Paginate method.

Paginating Date-Grouped Archives

{{/* Build a collection of all blog posts */}}
{{ $posts := where .Site.RegularPages "Section" "posts" }}

{{/* Group by the month of the date (e.g. “Jan 2006”) */}}
{{ $grouped := $posts.GroupByDate "Jan 2006" }}

{{/* Paginate the grouped collection – 5 groups per pager */}}
{{ $paginator := .Paginate $grouped 5 }}

{{/* Render each group on the current pager */}}
{{ range $paginator.PageGroups }}
  <h2>{{ .Key }}</h2>
  {{ range .Pages }}
    <h3><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h3>
  {{ end }}
{{ end }}

{{/* Navigation links */}}
{{ partial "pagination.html" . }}

Paginating by Front-Matter Weight

{{ $pages := .Site.RegularPages }}
{{ $byWeight := $pages.GroupBy "Weight" "desc" }}

{{ $p := .Paginate $byWeight 4 }}

{{ range $p.PageGroups }}
  <h2>Weight: {{ .Key }}</h2>
  {{ range .Pages }}
    <p>{{ .Title }} ({{ .Weight }})</p>
  {{ end }}
{{ end }}

{{ partial "pagination.html" . }}

Both examples rely on the core machinery in resources/page/pagination.go: the collection transforms into a PagesGroup, splits via splitPageGroups, and exposes results through the Pager API.

Summary

  • The page.Paginator type in resources/page/pagination.go orchestrates pagination by managing Pages or PagesGroup collections and generating Pager objects.
  • Custom grouping integrates through ToPagesGroup conversion, producing PagesGroup objects that preserve logical groupings.
  • The splitPageGroups function ensures grouped collections never split across page boundaries by flattening and re-chunking while rebuilding group structures.
  • Pager.Pages() and Pager.PageGroups() are mutually exclusive—you access one or the other depending on whether you paginated a flat or grouped collection.
  • URL generation uses paginationURLFactory respecting the pagination.path configuration, and paginators are cached on first invocation for performance.

Frequently Asked Questions

What is the difference between Pages and PagesGroup in Hugo pagination?

Pages represents a flat slice of page objects, while PagesGroup represents a slice of groups where each group contains a key (like a date or category) and its associated pages. When you use grouping methods such as .GroupByDate, Hugo returns a PagesGroup, which the paginator processes differently to ensure groups remain intact across pagination boundaries.

How does Hugo prevent grouped content from splitting across pages?

Hugo uses the splitPageGroups function in pagination.go (lines 221‑259) to handle grouped collections. This function flattens the groups while tracking their keys, then re-chunks the content into page-sized segments, carefully rebuilding groups so that no group spans multiple pager objects. This ensures users see complete groups on each paginated page.

Can I use both .Pages and .PageGroups on the same paginator?

No. According to the source code in pagination.go (lines 89‑115), these methods are mutually exclusive. A Pager object contains either a flat slice of pages or a grouped structure, determined by what you passed to .Paginate. If you paginated a grouped collection, use .PageGroups; otherwise, use .Pages.

Why does my paginator return the same results when I call it with different arguments?

Hugo caches the paginator on first invocation per list page. As implemented in the gohugoio/hugo source code, once initialized, the paginator stores its state to prevent redundant processing. Subsequent calls return the cached result regardless of argument changes, so you should configure your pagination parameters on the first call to .Paginate or .Paginator.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →