Pagination SEO: links search engines can follow, Load more and infinite scroll

Short answer

Google's crawlers don't click buttons. They follow <a href> links. If your page numbers, Next button or Load more control only work through JavaScript, everything on page 2 onwards can only be found some other way, such as the sitemap. Give each page its own URL (such as ?page=2) and make every pagination control a real link to it. JavaScript can still load the results in place for visitors.

What Google can follow

These all look the same on screen. Only one of them leads a crawler to page 2:

<a href="?page=2">2</a> Followed. A real link to a real URL.
<button>2</button> Not followed. A button has no address to go to.
<span onclick="…">2</span> Not followed. Crawlers don't click, so the script behind it never runs.
<a href="#">2</a> Not followed. Points at the page it's on.
<a href="#page-2">2</a> Not followed. Google ignores everything after the #.
<a href="javascript:next()"> Not followed. Not a URL at all.

This matters most on category and archive pages. If products or articles are only linked from page 3 of a list, a crawler that can't get past page 1 may never find them, or only find them late through the sitemap.

Load more and infinite scroll

A Load more button or infinite scroll is fine for visitors. The problem is only when it's the only way to reach the next set of results. The fix is to keep the button and give it a real page URL as well:

Button only not crawlable
<button class="load-more">
  Load more
</button>

Results 25+ exist only after a click.
A link, enhanced with JavaScript crawlable
<a class="load-more"
   href="/widgets?page=2">
  Load more
</a>

JavaScript loads page 2 in place on click.
Crawlers follow the href to /widgets?page=2.

Infinite scroll needs the same thing. Google's pagination guide asks you to make sure its crawler can find all of your content, so give each batch of results its own URL and link to it.

The other pagination rules

One URL per page Each page of results needs its own address, such as ?page=2. Google treats each one as a separate page.
Link in order Link each page to the next one, so a crawler can walk through the whole list. Google also suggests linking each page back to page 1.
Its own canonical Each page should name itself as canonical. Don't point pages 2, 3 and 4 at page 1: Google's pagination guide says not to. See canonical tags.
rel="next" isn't enough Google no longer uses <link rel="next"> and rel="prev". Other search engines may still use them, and they do no harm, but they don't replace real links.
Sitemap as a backup List the products or articles in your sitemap too. It helps, but it's a backup for crawlable links, not a replacement.

How to check by hand

1List every pagination control 30 seconds

On a category or archive page, open DevTools (F12), go to the Console and paste:

// Lists page-number, Next and Load more controls, and whether a crawler can follow each one
const here = location.href.split('#')[0];
// SVG links have no innerText (and their title is an object), so fall back to textContent
const label = el => (el.innerText ?? el.textContent ?? '').replace(/\s+/g, ' ').trim() || el.getAttribute('aria-label') || (typeof el.title === 'string' ? el.title : '') || '';
const isPager = text => /^[«‹←<>»›→]{1,2}$/.test(text)
  || /^(go to )?(page )?(\d{1,3}|first|last|prev|previous|next|newer|older)( page| posts)?$/i.test(text.replace(/^[«‹←<>»›→]+\s*|\s*[«‹←<>»›→]+$/g, ''))
  || /^(load|show|see|view) more( [\w-]+)?( \(\d+\))?$/i.test(text);
const rows = [...document.querySelectorAll('a, button, [role="button"], [onclick]')]
  .filter(el => el.checkVisibility() && !el.hasAttribute('aria-expanded') && isPager(label(el)))
  // A clickable wrapper around a real link: the link inside is what counts
  .filter(el => el instanceof HTMLAnchorElement || !el.querySelector('a[href]'))
  .map(el => {
    const raw = el.getAttribute('href');
    const skip = el.disabled || el.getAttribute('aria-disabled') === 'true' ? 'disabled'
      : el.matches('[aria-current="page"], [aria-current="true"]') ? 'current page' : '';
    const problem = !(el instanceof HTMLAnchorElement) ? 'not a link (' + el.localName + ')'
      : raw === null ? 'link with no href'
      : /^\s*javascript:/i.test(raw) ? 'javascript: link'
      : raw.trim().startsWith('#') ? 'only changes the #fragment'
      : el.href.split('#')[0] === here ? 'links back to this page'
      : '';
    return { text: label(el).slice(0, 30), href: raw ?? '(none)', crawlable: skip ? 'n/a (' + skip + ')' : problem ? 'NO: ' + problem : 'yes' };
  });
console.table(rows.length ? rows : [{ result: 'No pagination controls found' }]);

It finds controls that look like page numbers, Previous, Next or Load more, and says whether a crawler can follow each one, and why not.

It goes by the control's text, so carousel arrows and dots, size pickers and other numbered buttons can turn up too. Ignore anything that isn't pagination. A Load more button is fine if a real link to the next page sits next to it. A current page that links to itself with only a class such as "active" will show as "links back to this page".

2Hover and right-click 1 minute

  1. Hover over the page 2 link or the Next button. A real link shows its URL in the bottom corner of the browser.
  2. Right-click it. If you're offered "Open link in new tab", it's a link.
  3. Open it in a new tab. Page 2 should load on its own, at its own URL.

3Turn JavaScript off 2 minutes

In DevTools, press Ctrl+Shift+P, run Disable JavaScript and reload. If you can still reach page 2, the links are in the server HTML, which every crawler can follow. If they disappear, JavaScript adds them: Google can still follow real links after rendering, but other crawlers may not. Run Enable JavaScript when you're done.

How crawlers report this

If a crawl sent you here, these are the labels to look for. Our guide to Screaming Frog and Sitebulb issues covers other common ones.

Screaming FrogSitebulbWhat it means
Pagination: Pagination URL Not In Anchor TagNo direct matchA page in the series isn't linked with a real link, so crawlers may not reach it.

Doing this on every page?

SEOSnip checks pagination for you

Open the extension on a listing page and it finds the pagination blocks and Load more controls, skipping carousels, reviews, galleries and date pickers. You get:

  • Every control a crawler can't follow, with the reason: a button, a click handler, href="#", a javascript: link, a fragment, or page links that all share one URL
  • No flag for a Load more button that has a real next-page link beside it
  • If the page also has <link rel="next">, the ticket explains that Google no longer uses it
  • A ticket listing each control, and a "Check if fixed" button that re-runs the check