Pagination SEO: links search engines can follow, Load more and infinite scroll
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:
#.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 class="load-more"> Load more </button> Results 25+ exist only after a click.
<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
?page=2. Google treats each one as a separate page.<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.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
- Hover over the page 2 link or the Next button. A real link shows its URL in the bottom corner of the browser.
- Right-click it. If you're offered "Open link in new tab", it's a link.
- 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 Frog | Sitebulb | What it means |
|---|---|---|
| Pagination: Pagination URL Not In Anchor Tag | No direct match | A 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="#", ajavascript: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
Pagination uses JavaScript instead of links
| Control | Found at | Problem |
|---|---|---|
<button class="page">2</button> | main > nav > button:nth-of-type(2) | is a button, not a link |
<a href="#">Next</a> | main > nav > a:nth-of-type(3) | has href="#" |
<button>Load more</button> | main > section > div > button | is a button, not a link |