Static Website Pagination with Gatsby and Hugo

In my various posts about developing static websites, I’ve pointed out that some critical functions can be tricky to implement. One example is pagination—splitting a long list of posts across several numbered pages. This is a nerdy deep-dive into how pagination works for two popular static site frameworks: Gatsby and Hugo.

Pagination requires figuring out how many total posts you have, and the relation of the current post to the others. That’s easy if you have a database running the background, but static sites don’t have that luxury. So they need to figure all of those relationships out when building the site and generate all the relevant links and infrastructure at build time.

Basic Pagination

Gatsby does that by exposing a createPages function that runs when Gatsby is building your site. That allows you to build a function that figures out how many posts there are run createpages for each of the paginated list pages. The Gatsby documentation provides instructions on how to implement pagination, and there’s a community plugin available (though it hasn’t been updated in some time). I wrote a function based on the example in the Gatsby documentation:

let createPaginatedIndices = (createPage, numPages, postsPerPage, label, templatePath) => {
  Array.from({ length: numPages }).forEach((_, i) => {
    createPage({
      path: i === 0 ? `/${label}` : `/${label}/${i + 1}`,
      component: path.resolve(templatePath),
      context: {
        limit: postsPerPage,
        skip: i * postsPerPage,
        numPages: numPages,
        currentPage: i + 1,
      },
    })
  })
}

Hugo doesn’t expose those internal functions. However, it has a built-in pagination function that allows you to set the number of posts per page and the order of the content. If you’re on a page that gets sent a list of posts (like an archive or tag or category page), you can simply paginate the posts that get sent to that page using the .Pages variable, like so:

{{ range (.Paginator 5).Pages }}

This tells Hugo to paginate all of the content stored in the .Pages variable, with 5 posts per page. Then Hugo has a default template for the previous, next, and numbered links that govern pagination. As an example check out the bottom of the posts page on this website. Those links are generated with the following code:

{{ template "_internal/pagination.html" . }}

These links are customizable, but doing so is tricky, especially given Hugo’s clunky templating system. There are some good guides out there if you’re interested, but the default template with some custom CSS styling worked just fine for me.

Linking to Index Pages

One feature I really wanted to build was for individual posts or photos to link back to the list page on which they appear. For example, if you read any of the posts on the second page of posts on this website, the “← All Posts” link will send you back to the second page of posts, rather the front page. It might seem like a minor feature, but I think it’s really helpful when you’re browsing content. (This works with photos too, by the way). But building it out was a challenge, and I spent way more time on it than it warranted.

The fundamental problem is that an individual post or photo doesn’t have any way to know where it is in relation to all the other content on a website. Hugo provides a way to link to the previous and next posts, and the previous and next posts in a section, but that’s it. The code looks something like this:

{{ if .PrevInSection }}
  <div class="prevNextLink previous">
    <span class="prevNextLabel">Previous</span>
    <a rel="previous" href="{{ .PrevInSection.Permalink | absURL }}"><i class="icon" data-feather="arrow-left"></i> {{ .PrevInSection.Title | truncate 50 "..."}}</a>
  </div>
{{ end }}

I use this code for the previous and next links at the bottom of this page. But we need more information to build the index link. Specifically, we need two numbers: the index of the current page among the all the posts, and the number of posts per page. For example, if there are ten posts per page, posts 1-10 will appear on the first page, and posts 11-20 will appear on the second. So if we’re on the 17th post, we want the link to go that second page.

On Gatsby, I was able to build out this functionality by passing information to each post through the createPages function. That function iterates over all your posts, and it was easy enough to add a counter to that loop. Since the createPages function can pass variables to indvidual pages, I was able to calculate which page each post would appear on, and pass that variable to page template so it could be used to generate links. I used code like this modify the createPages function for each post:

post_query.data.allMarkdownRemark.edges.forEach((post, index) => {
let indexPage = Math.ceil( (post_query)results.data.allMdx.edges.length - index) / postsPerPage )
createPage(getPostPageDetails(
  post,
  `${blogPostTemplate}?__contentFilePath=${post.node.internal.contentFilePath}`,
  indexPage
));

But Hugo doesn’t expose its page creation methods that way. Instead, on each individual page, we need to loop over all the posts, in the same order they appear in pagination, until we get to the current page. The code looks like this:

{{ $counter := 0 }}
{{ range (where .Site.RegularPages "Section" "posts").ByDate.Reverse }}
  {{if eq .Permalink $current_page.Permalink}}
    {{$counter = add (math.Floor (div $counter .Site.Params.Pagination.posts)) 1}}
    {{ break }}
  {{ end }}
  {{ $counter = add $counter 1 }}
{{ end }}

This code fragment selects all of the posts and puts them in reverse chronological order (as they appear in the posts list). It initiates a counter at zero and goes through each post, adding one to the counter, until the permalink of the post in the loop matches the permalink for the current page. At that point, it sets the counter variable to the value of the counter divided by the number of posts per page, rounded down, and adds one. That gets us the number of the list page where the current post is found.

Anchors for Individual Posts

The next step was to add page anchors for each post. To do so I needed to generate a “slug” (a unique, URL-friendly representation of the page title). You can define a post’s slug in the front matter, but I haven’t done that for all my posts (I should, but I haven’t had the chance). So I wrote a function that checks to see if the slug is defined in the frontmatter. If not, it falls back to the file name, which can work as a slug. Here’s the code:

{{ $slug := .Slug }}
{{ if (or (not $slug) (eq $slug ""))}}
  {{$slug = .File.BaseFileName }}
{{end}}
{{ .Scratch.Set "slug" $slug }}

This is saved as a partial page element, in my theme as /layouts/patrials/slug.html. Unlike a true partial, it doesn’t generate any HTML. Instead, it stores the slug in Hugo’s scratchpad, so that the slug can be accessed from the page template. By using a partial, I can make sure the slug is generated the same way every time I use it.

I modified my list template (/layouts/_default/list-item.html) to add anchors to post titles like so:

<h2 class="entryTitle" id="{{.Scratch.Get "slug"}}">
  <a href="{{ .RelPermalink }}">{{ .Title }}</a>
</h2>

And then finally we can write the code to generate the

<p class="allPostsLink">
  <a href="/posts/{{if gt $paginationIndex 1}}page/{{$paginationIndex}}/{{end}}#{{.Scratch.Get "slug"}}">
    <i class="icon" data-feather="arrow-left"></i>
    All Posts
  </a>
</p>

This creates the “← All Posts” link, adjusting the URL according the $paginationIndex variable, and then add a link to the anchor we created on the list page. And there you have it—links on posts that return you to the post list will send you back to the current post’s spot in the list, rather than just the most recent posts. A small but noticeable feature that makes browsing content more pleasant.