16.6 Pagination: paginate, .Paginator, and paginator Template
Now, let’s talk about pagination. You didn’t think you’d just dump 10,000 blog posts onto a single page, did you? Your server would burst into flames, your users would flee, and Google would personally send a strongly worded letter. Pagination is the art of slicing that massive content salami into manageable, bite-sized pieces. Hugo handles this with a trio of concepts: the paginate setting, the .Paginator object, and the paginator template. It’s powerful, but it has a few… idiosyncrasies.
The paginate setting: Your first mistake
You don’t just “turn on” pagination. You configure it in your template, telling Hugo exactly which pages to slice up. This is done with the paginate function in your list template, usually list.html. The most common use is for your section pages.
{{ define "main" }}
{{ $paginator := .Paginate (where .Pages "Type" "posts") }}
<h1>{{ .Title }}</h1>
{{ range $paginator.Pages }}
<article>
<h2><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
{{ .Summary }}
</article>
{{ end }}
<!-- We'll get to this part next -->
{{ template "_internal/pagination.html" . }}
{{ end }}
Here, {{ $paginator := .Paginate (where .Pages "Type" "posts") }} is the engine. You pass .Paginate a collection of pages—in this case, we’re filtering for only pages of type “posts”. The default number of pages per section is 10, defined in your site’s config (paginate). You can override this by passing a second argument: .Paginate (where .Pages "Type" "posts") 5.
Crucial Gotcha: The paginate setting in your config file (config.toml) is a global default. The number you pass directly to the .Paginate function in the template overrides that default for this specific template. This is a common point of confusion. Check your config first if things are paginating strangely.
The .Paginator object: Your new best friend
Once you’ve called .Paginate, you get a $paginator object. This isn’t just your sliced pages; it’s a Swiss Army knife of pagination data. You’ll use .Pages from this object ($paginator.Pages) in your range loop, not the original .Pages.
The real magic is in the other properties of .Paginator:
$paginator.PageNumber: The current page you’re on (e.g., 2).$paginator.URL: A function to generate the URL for a given page number.$paginator.URL 3gives you the link to page 3.$paginator.Pages: The slice of pages for the current page of the pagination.$paginator.TotalPages: The total number of pages.$paginator.TotalNumberOfElements: The total number of posts across all pages.$paginator.HasPrev/$paginator.HasNext: Booleans to check if there’s a previous or next page.$paginator.Prev/$paginator.Next: The actual page objects for the previous and next pages.
These are what you use to build your own custom navigation, which you will almost certainly want to do because…
The paginator template: Fine, we’ll do it ourselves
Hugo provides a built-in partial, _internal/pagination.html, which is what I used in the first example. It’s… functional. It gets the job done with basic next/prev links and page numbers. It’s also about as exciting as a paperclip. For anything that looks like it was designed after 2005, you’ll need to roll your own.
Here’s how you build a much more common and useful pagination bar using the .Paginator object’s properties.
{{ if or $paginator.HasPrev $paginator.HasNext }}
<nav aria-label="Page navigation">
<div>
{{ if $paginator.HasPrev }}
<a href="{{ $paginator.Prev.URL }}" aria-label="Previous Page">← Previous</a>
{{ end }}
<span>Page {{ $paginator.PageNumber }} of {{ $paginator.TotalPages }}</span>
{{ if $paginator.HasNext }}
<a href="{{ $paginator.Next.URL }}" aria-label="Next Page">Next →</a>
{{ end }}
</div>
</nav>
{{ end }}
This gives you a simple “Previous / Page X of Y / Next” setup. For a more advanced one with numbered page links, you’d use $paginator.URL in a loop. It’s more code, but the control is worth it.
The home page is a special, fussy snowflake
Here’s the part the documentation often mumbles quietly: the home page is a list template. Specifically, it uses the layouts/index.html template. This means you can, and probably should, paginate it if you have more than a handful of posts.
The code is nearly identical, but you’re typically paginating the site’s regular pages, often from the “posts” section.
{{ define "main" }}
{{ $paginator := .Paginate (where site.RegularPages "Type" "in" site.Params.mainSections) }}
{{ range $paginator.Pages }}
... display your posts ...
{{ end }}
{{ partial "my-pagination-partial.html" $paginator }}
{{ end }}
The key difference is the page set: (where site.RegularPages "Type" "in" site.Params.mainSections). This looks in your config for a mainSections parameter (e.g., mainSections = ["posts"]) and grabs the regular pages from those sections. It’s the most future-proof way to handle it.
The Big Pitfall: The home page’s paginator must be handled in index.html. You cannot control it from another list template. It plays by its own rules because it’s the boss. Remember that, and you’ll save yourself an hour of frustrated debugging.
So there you have it. Pagination isn’t magic, it’s just a bit of configuration and knowing which object to poke. Build your own navigation, be kind to your home page, and for the love of all that is holy, don’t set your paginate value to 5,000.