Oliver Schmid Owl Logo

Hugo Pagination

 -  read

Inspired by Glenn McComb’s Hugo pagination tutorial I decided to write my own custom pager. The specific features I’m interested in for pagination are:
  1. Dedicated Previous and Next links, so the reader can quickly find these 2 common actions at similar locations on each page.
  2. A clearly marked current page number.
  3. Always visible first and last page number. Combined with the current page number, this gives the reader a sense of how many pages they’ve read and how many there are to go.
  4. A configurable number of adjacent pages.
  5. Ellipses when there are more pages than can be displayed.
  6. Bulma CSS styles.

Like so:

Here’s what I came up with:

<!-- Inspired by https://glennmccomb.com/articles/how-to-build-custom-hugo-pagination/

  This paginator prints the first and last page number along with a configurable number of adjacent pages (see
  $adjacent_links).

    e.g. [1] ... [4] [5] [*6*] [7] [8] ... [12]

  Rather than including "First" and "Last" links, including the page number of the first and last pages gives the
  user some context into how far they've gone into the page list. With that info they can make better decisions about
  whether or not to continue.

  "Previous" and "Next" links are included even though the adjacent page numbers seemingly make them redundant. As
  these are the most common paging actions (and often clicked repeatedly) we want them at the same location on each
  page.

  Additionally, if the user is on one of the first few pages (see $lower_limit) more pages are printed after the
  current page.

    e.g. [1] [*2*] [3] [4] [5] ... [12]

  Similarly, if the user is on one of the last few pages (see $upper_limit) more pages are printed before the
  current page.

    e.g. [1] ... [8] [9] [10] [*11*] [12]
-->
{{ $paginator := .Paginator }}

        <!-- Number of links either side of the current page -->
{{ $adjacent_links := 1 }}

        <!-- $max_links = ($adjacent_links * 2) + 1 -->
{{ $max_links := (add (mul $adjacent_links 2) 1) }}

        <!-- Pages to print -->
{{ $lower_limit := 1 }}
{{ $upper_limit := $paginator.TotalPages }}
{{ $include_lower_ellipsis := false }}
{{ $include_upper_ellipsis := false }}

{{ if gt $paginator.TotalPages (add $max_links 2) }}

        <!-- If we have more pages before the current page than we can print -->
  {{ if ge $paginator.PageNumber $adjacent_links }}

    {{ $lower_limit = sub $paginator.PageNumber $adjacent_links }}

        <!-- Show more pages at the end of the range -->
    {{ if lt (sub $paginator.TotalPages $lower_limit) $max_links }}
      {{ $lower_limit = add 1 (sub $paginator.TotalPages $max_links) }}
    {{ end }}

        <!-- Show ellipsis -->
    {{ if gt (sub $lower_limit 1) 1 }}
      {{ $include_lower_ellipsis = true }}
    {{ end }}

  {{ end }}

        <!-- If we have more pages after the current page than we can print -->
  {{ if gt (sub $paginator.TotalPages $paginator.PageNumber) $adjacent_links }}

    {{ $upper_limit = add $paginator.PageNumber $adjacent_links }}

        <!-- Show more pages at the beginning of the range -->
    {{ if le $upper_limit $max_links }}
      {{ $upper_limit = $max_links }}
    {{ end }}

        <!-- Show ellipsis -->
    {{ if gt (sub $paginator.TotalPages $upper_limit) 1 }}
      {{ $include_upper_ellipsis = true }}
    {{ end }}

  {{ end }}

{{ end }}

        <!-- If there's more than one page -->
{{ if gt $paginator.TotalPages 1 }}
<section class="section">
  <nav class="pagination is-centered">

    {{ if $paginator.HasPrev }}
      <a class="pagination-previous" href="{{ $paginator.Prev.URL }}">Previous</a>
    {{ else }}
      <a class="pagination-previous" title="On first page" disabled>Previous</a>
    {{ end }}

    {{ if $paginator.HasNext }}
      <a class="pagination-next button is-link" href="{{ $paginator.Next.URL }}">Next page</a>
    {{ else }}
      <a class="pagination-next" title="On last page" disabled>Next page</a>
    {{ end }}

    <ul class="pagination-list">

    {{ range $paginator.Pagers }}

        <!-- Include first, last, and middle pages -->
      {{ if or (or (eq .PageNumber 1) (eq .PageNumber $paginator.TotalPages)) (and (ge .PageNumber $lower_limit) (le .PageNumber $upper_limit)) }}

        <li><a href="{{ .URL }}" class="pagination-link{{ if eq $paginator.PageNumber .PageNumber }} is-current{{ end }}">{{ .PageNumber }}</a>

            <!-- If we're on the first page and inserting an ellipsis, or just before the last page and inserting an ellipsis -->
        {{ if or (and (eq .PageNumber 1) (eq $include_lower_ellipsis true)) (and (eq .PageNumber $upper_limit) (eq $include_upper_ellipsis true)) }}
          <li><span class="pagination-ellipsis">…</span>
        {{ end }}

      {{ end }}

    {{ end }}

    </ul>
  </nav>
</section>
{{ end }}
    

Note this code is available on github (along with the code for the rest of this site).

The biggest stylistic difference from Glenn McComb’s version is that the adjacent pages are pre-computed outside the loop. I doubt there’s any performance difference as both versions are still O(n) but it did shorten the code somewhat.

I noticed a couple of things about the go template language while developing. For one, go template expressions can’t stretch across multiple lines. This forced the complicated boolean logic to stretch quite wide.

The go template language is also missing slice expressions (i.e. a[2:5]). Had these existed I would have attempted to range only over the adjacent links and reduce the algorithm to O(m) (where m is the number of adjacent links). It’s possible to get around this in regular usage of go templates by adding a custom mkSlice function but Hugo does not allow custom go template functions.

Oh well, the performance implications are minimal. It was still a fun little coding exercise :)