Pagination, and why load-more is not a substitute

Applies to colibrity.pagination-in-url, colibrity.pagination-crawlable.

Covers colibrity.pagination-in-url, colibrity.pagination-crawlable, colibrity.paged-canonical.

The problem this rule is really about

"Each pagination page should have the page number reflected in the URL" sounds like adding a parameter. It is not. A listing whose only navigation is an offset load-more button has no pages at all - there is nothing to put a number on.

That is the shape most of these listings currently have: a button, a REST call with an offset, and cards appended to a grid. It works well for a visitor with JavaScript who keeps scrolling. It has three costs.

Everything past the first batch is invisible. A crawler loads the page, sees twelve of two hundred items, and never presses the button. The other 188 detail pages are only discoverable if something else links to them.

There is no address for item 150. No way to link to it, no way to come back to it, no way to resume after opening a result and pressing Back.

A <button type="button"> is not a link. It cannot be middle-clicked, cannot be opened in a new tab, has no href to follow, and does not appear in any link graph. Accessible pagination markup around a set of buttons is still a dead end.

What to build

Real pagination underneath. Load-more, if the design wants it, layered on top.

/realisations/            page 1
/realisations/page/2/     page 2, server-rendered, 200 with JavaScript disabled
  1. The server renders each page. Requesting /page/2/ returns page two's items in the HTML. Turn JavaScript off and the listing still works.
  2. Page controls are anchors with real hrefs. Keep the accessible markup that is already there
    • a nav with an accessible name, an ordered list, aria-current="page" on the active page, the disabled state on the ends - and change <button> to <a href>.
  3. Load-more is an enhancement. It intercepts the "next page" link, fetches, appends, and advances the URL as it goes. Without JavaScript the same link is a normal page load.
  4. Each paged view is canonical to itself, and carries its page number in the title. See below.

Canonicals on paged views

The one that goes wrong most often. Page two must canonicalise to page two.

<!-- on /realisations/page/2/ -->
<link rel="canonical" href="https://example.com/realisations/page/2/">
<title>Realisations - Page 2 - Brand</title>

Pointing every paged view back at page one tells the crawler that pages two and up do not exist, so it stops following them, and the items only reachable from those pages drop out of the index. This is a silent failure: nothing looks wrong, the pages just quietly stop being crawled.

rel="next" and rel="prev" are no longer used as an indexing signal, so they are optional. The self-referencing canonical is not.

History behaviour

Pagination is the opposite of filtering. Moving to page two is a new view, so it takes a real history entry and Back should return to page one. Filters use replaceState; pagination uses a normal navigation or pushState.

When load-more is layered on top, advance the URL to the page whose first item is at the top of the viewport, and restore that scroll position when the visitor returns.

How it is checked

  1. /page/2/ returns 200 and contains page two's items with JavaScript disabled.
  2. Every page control in the pagination nav is an a with an href resolving to 200.
  3. Middle-click page two - it opens in a new tab.
  4. The canonical on page two equals page two's URL, and its title differs from page one's.
  5. With load-more, the address bar shows the page number after loading a batch.