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

> Understand Hugo's pagination for listing pages with custom grouping. Learn how Hugo splits grouped collections into Paginator and Pager types to display paginated results effectively.

- Repository: [GoHugo.io/hugo](https://github.com/gohugoio/hugo)
- Tags: deep-dive
- Published: 2026-02-28

---

**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`](https://github.com/gohugoio/hugo/blob/main/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/main/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`](https://github.com/gohugoio/hugo/blob/main/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`](https://github.com/gohugoio/hugo/blob/main/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`](https://github.com/gohugoio/hugo/blob/main/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

```go-html-template
{{/* 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

```go-html-template
{{ $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`](https://github.com/gohugoio/hugo/blob/main/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`](https://github.com/gohugoio/hugo/blob/main/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`](https://github.com/gohugoio/hugo/blob/main/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`](https://github.com/gohugoio/hugo/blob/main/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`.