Paginated Posts With Hugo

March 9, 2023

Setting It Up

The simplest solution is to create a custom layout to handle multipage documents. For example, I created a multipage layout in my theme as layouts/blog/multipage.html. You can put it wherever it makes sense for your structure.

First, though, you need a way to tell which page is which, because it won’t be obvious to Hugo.

I’m still chewing on the filename thing I mentioned earlier – I might actually try that later – but for now, we’re going to use the front matter in each page. The front matter for the page you’re reading right now looks like this:

page_number: 2

And that’s it. That’s all each page gets: its own page number. The only thing this is used for in my implementation is as a sort key to order the pages by. Simple enough; we can order them nicely that way.

Now we just need to make sure the front matter is set up properly in _index.html such that it activates our multipage layout:

date: 2023-03-09T00:00:00-06:00
slug: paginated-posts-with-hugo
title: "Paginated Posts With Hugo"
featured_image: library/documentation.png
url: /2023/03/paginated-posts-with-hugo

categories: ["Blogging"]
tags: ["Hugo", "HOWTO", "Tip"]
flags: ["featured"]

layout: multipage
  render: always
    render: never

There are a few important things here, but for now the highlighted line is what we’re interested in: it selects the page template and ensures that the page is displayed just the way we want it. Unfortunately I don’t know of a way to do that automatically, so manual layout selection it is.

Laying It Out

Now we have the thing using our layout, but how does the layout work? How do we make it put all these pages in the right place? It’s actually surprisingy simple in the end.

List pages are intended to list things. That much is obvious. How you represent those things is entirely up to you. In the case of a multipage article, we’re just going to display each item, in full, one per page. We’ll use Hugo’s Paginator to do it.

For example, multipage.html might look something like this:

{{- define "main" -}}
{{- $pages := .RegularPages.ByParam "page_number" -}}
{{- $paginator := .Paginate $pages 1 -}}
<div class="page-content">
  {{- range $paginator.Pages -}}
  {{-   partial "page-content" . -}}
  {{- end -}}
{{- if gt $paginator.TotalPages 1 -}}
<div class="paginator">
  {{- template "_internal/pagination.html" . -}}
{{- end -}}
{{- end -}}

The first highlighted section here gathers up the pages, ordered by the page_number front matter field we talked about earlier, and then begins the pagination. You can order your pages however you want here; Hugo has simply handed you a slice of all the subpages in the document. This is really a branch bundle, so that’s all handled automatically.

The second highlighted section loops through the list of pages and provides the unique content associated with each one. The result will be however many copies of this template, each with the content from one page inserted where the loop is. How Hugo does that internally, I have no idea (and I’m rather curious). Note that a partial is used here; with good design, you can reuse that in your single template and only have to write it once.

Within that partial, your context is the target page, so you can access .Content and so forth as needed. Note, however, that some properties will not be available (such as any permalink-related properties; these pages don’t have proper permalinks!).

And finally, the last highlighted section adds the page controls if needed. The template used in this example (and on is internal, provided by Hugo. You can certainly implement your own pager, but this is much easier if you’re using bootstrap or something with CSS that happens to be compatible with the bootstrap pager component.

And that’s it. Once you’ve got your template built, you can compile your site, navigate to your post, and see your nicely paged content!

That was too easy, right? So what’s the catch?