Optimizing Pagination SEO (rel=prev/next, Load More, Infinite Scroll)
Pagination is one of the most underestimated SEO issues. On a catalog with 50 pagination pages, search engines waste crawl budget on hundreds of duplicate-content pages, leaving none with enough link weight for top rankings. Proper pagination means choosing model (numbered, Load More, Infinite Scroll), correct search engine signals, and preserving UX.
After rel=prev/next Deprecation (2019)
Google stopped using rel="prev" and rel="next" as grouping signals. They don't hurt, but you can't rely on them.
Current methods:
- Canonical to first page (if pagination pages have no unique content)
- Self-referencing canonical on each page (if unique content needs indexing)
- robots.txt / GSC parameters to exclude pagination
- View-all page as canonical source
Numbered Pagination
Option A: Indexable pagination pages
When pagination can rank independently:
<!-- On /products/?page=2 -->
<link rel="canonical" href="https://example.com/products/?page=2" />
Each page has unique <title> and <h1>:
<title>Smartphones Catalog — page 2 | Shop</title>
Option B: All pagination → canonical to first
For blogs, archives — where page 2 has no independent value:
<!-- On /blog/?page=3 -->
<link rel="canonical" href="https://example.com/blog/" />
Or noindex:
<meta name="robots" content="noindex, follow" />
follow is important: bot must traverse pagination links to find products/articles.
URL structure:
Path-based (/products/page/2/) preferred over query-string (/products/?page=2) — better perception by crawlers.
Normalize in nginx:
if ($arg_page = "1") {
return 301 $uri;
}
Load More: SEO Without URL Changes
"Load More" button appends content via AJAX without changing URL.
Problem: Google renders JS but doesn't click buttons — content beyond "Load More" is invisible.
Solution 1: History API + pushState
Update URL on click:
function loadMore(page) {
fetch(`/api/products?page=${page}`)
.then(r => r.json())
.then(data => {
appendProducts(data.items);
history.pushState(
{ page },
'',
`/products/?page=${page}`
);
});
}
Solution 2: SSR first batch + dynamic loading
First N items render server-side:
<div id="product-grid">
<!-- first 24 items server-rendered -->
</div>
<button id="load-more" data-page="2">Load More</button>
Infinite Scroll
Worst for SEO by default. Content below fold isn't guaranteed indexing.
Proper implementation: Infinite Scroll + fragment URLs
On scroll threshold (Intersection Observer) update URL:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const page = entry.target.dataset.page;
history.replaceState(null, '', `/products/?page=${page}`);
}
});
}, { rootMargin: '0px 0px -50% 0px' });
SEO compromise:
Add standard numbered pagination in footer, hidden via CSS but visible to search engines:
<nav aria-label="Pagination" class="sr-only">
<a href="/products/?page=1">1</a>
<a href="/products/?page=2">2</a>
</nav>
View-All Page
For small catalogs (up to 200–300 items), single /products/all/ page with all items is optimal. Sets canonical from all pagination pages, gets all link weight.
<!-- On /products/?page=2, /products/?page=3 -->
<link rel="canonical" href="https://example.com/products/all/" />
Verification
# Check pagination pages don't index (if noindex chosen)
curl -s "https://example.com/products/?page=5" | grep -i 'canonical\|noindex'
# JS rendering check
npx puppeteer-core --url "https://example.com/products/" --js-enabled true
Use GSC URL Inspection for pagination pages. Verify canonical is correct and noindex applied.
Timeline
Pagination audit + recommendations — 1–2 days. Technical implementation — 3–6 days.







