Crawlable Pagination Without rel=next/prev in Headless
Now that Google no longer uses rel=next/prev as an indexing signal, crawlable pagination in a headless stack depends entirely on clean per-page URLs, self-referencing canonicals, and server-rendered links.
When to Use This Approach
Apply this pattern when your headless build exhibits any of the following:
- You operate a large paginated archive (blog index, category listing, news feed) where deeper pages hold content that must remain discoverable to Googlebot.
- Your pagination previously relied on
rel=next/prevlink elements to communicate sequence, and you need a strategy that works now that the signal is retired. - Your listing renders pagination controls as JavaScript-only buttons, so the links to page 2 and beyond never appear in the server HTML that crawlers parse first.
Implementation Steps
Step 1: Route Pages Through Path Segments
Serve every page of a listing from a dedicated path-based route (/archive/page/2) rather than a query parameter (?page=2). Path segments produce a single unambiguous URL per page, cache cleanly at the edge, and sidestep the duplicate-variant problem that query strings invite.
// app/archive/page/[page]/page.tsx — Next.js App Router
import { notFound } from 'next/navigation';
const PER_PAGE = 20;
export async function generateStaticParams() {
const total = await getPostCount();
const pages = Math.ceil(total / PER_PAGE);
// Skip page 1 — that lives at /archive
return Array.from({ length: pages - 1 }, (_, i) => ({
page: String(i + 2),
}));
}
export default async function ArchivePage({
params,
}: {
params: { page: string };
}) {
const page = Number(params.page);
if (!Number.isInteger(page) || page < 2) notFound();
const posts = await getPosts({ page, perPage: PER_PAGE });
if (posts.length === 0) notFound();
return <PostList posts={posts} page={page} />;
}
Validation: curl -s -o /dev/null -w "%{http_code}\n" https://staging.yourdomain.com/archive/page/2 must return 200, and a non-existent page such as /archive/page/9999 must return 404.
Step 2: Emit a Self-Referencing Canonical Per Page
Each paginated URL must declare a canonical that points to itself. A common mistake is canonicalizing every page back to page one, which tells Google to drop pages 2+ from the index and, with them, the links to your deeper content.
// app/archive/page/[page]/page.tsx — metadata
import type { Metadata } from 'next';
const BASE = process.env.NEXT_PUBLIC_SITE_URL!; // e.g. https://yourdomain.com
export async function generateMetadata({
params,
}: {
params: { page: string };
}): Promise<Metadata> {
const page = Number(params.page);
return {
alternates: {
canonical: `${BASE}/archive/page/${page}/`,
},
};
}
This is the same discipline covered in canonical URL enforcement: the canonical must match the served URL exactly, including trailing-slash policy.
Validation:
curl -s https://staging.yourdomain.com/archive/page/2/ \
| grep -oP '(?<=rel="canonical" href=")[^"]+'
# Expected: https://yourdomain.com/archive/page/2/ (points to itself)
Step 3: Server-Render Real Anchor Pagination Links
Googlebot discovers deeper pages by following <a href> links in the initial HTML. Render pagination controls as real anchors on the server, not as click handlers that fetch the next page via JavaScript.
// components/Pagination.tsx
export function Pagination({
page,
totalPages,
}: {
page: number;
totalPages: number;
}) {
const href = (n: number) => (n === 1 ? '/archive' : `/archive/page/${n}`);
return (
<nav aria-label="Pagination">
{page > 1 && <a href={href(page - 1)} rel="prev">Previous</a>}
{page < totalPages && <a href={href(page + 1)} rel="next">Next</a>}
</nav>
);
}
Keeping rel="prev" and rel="next" is harmless for other user agents, but it is the crawlable href that does the real work now that Google ignores those relations for indexing.
Validation:
# Confirm the links exist in raw HTML with JS disabled (curl never runs JS)
curl -s https://staging.yourdomain.com/archive \
| grep -oP '<a[^>]+href="/archive/page/\d+"'
# Must list the next-page anchor(s)
Step 4: Give Each Page a Unique Title and Description
If pages 2, 3, and 4 share the exact title and meta description of page one, Google may group them as duplicates. Append the page number so each URL is self-evidently distinct.
// app/archive/page/[page]/page.tsx — extend generateMetadata
export async function generateMetadata({
params,
}: {
params: { page: string };
}): Promise<Metadata> {
const page = Number(params.page);
return {
title: `Archive — Page ${page}`,
description: `Browse page ${page} of our full article archive.`,
alternates: { canonical: `${BASE}/archive/page/${page}/` },
};
}
Validation:
curl -s https://staging.yourdomain.com/archive/page/2/ \
| grep -oP '<title>[^<]+</title>'
# Expected: <title>Archive — Page 2</title>
Step 5: Decide a noindex,follow Threshold for Deep Pages
Very deep archives can accumulate thousands of thin listing pages. Where a page offers no standalone value, apply noindex, follow beyond a threshold so the URL drops from the index while its links stay crawlable. This protects crawl budget without stranding deeper content.
// app/archive/page/[page]/page.tsx — conditional robots directive
const INDEX_THRESHOLD = 10;
export async function generateMetadata({
params,
}: {
params: { page: string };
}): Promise<Metadata> {
const page = Number(params.page);
return {
alternates: { canonical: `${BASE}/archive/page/${page}/` },
robots:
page > INDEX_THRESHOLD
? { index: false, follow: true }
: { index: true, follow: true },
};
}
Validation:
curl -s https://staging.yourdomain.com/archive/page/25/ \
| grep -oP '<meta name="robots" content="[^"]+"'
# Expected beyond threshold: <meta name="robots" content="noindex, follow">
SEO Impact Summary
| Signal | What improves | What breaks if misconfigured |
|---|---|---|
| Discovery | Server-rendered anchors let Googlebot reach every page without JS | JS-only pagination hides deep content from the crawl |
| Canonical accuracy | Self-referencing canonicals keep each page indexable | Canonicalizing all pages to page 1 drops pages 2+ and their outbound links |
| Duplicate handling | Per-page titles/descriptions signal distinct documents | Shared titles collapse paginated URLs into one duplicate group |
| Crawl efficiency | Path-based routes cache cleanly and avoid parameter variants | Query strings spawn ?page=2&sort= duplicates that waste crawl budget |
Measurable signals to watch:
- GSC Pages report: paginated URLs should move to “Indexed” (for pages under your threshold) rather than “Crawled - currently not indexed”.
- GSC Crawl stats: total crawled URLs should not balloon with parameter permutations after launch.
- Log analysis: Googlebot hit depth should reach your deepest indexable page within a few crawl cycles.
Edge Cases and Gotchas
Preview environments and staging pagination
A preview deployment that exposes the same /archive/page/N routes can leak into discovery if it is linked or listed anywhere. Block preview subdomains at the edge and ensure your sitemap only emits production paginated URLs.
Sort and filter parameters on top of pagination
Faceted controls layered over pagination (/archive/page/2?sort=new) multiply URL variants fast. Canonicalize filtered views to their unfiltered path-based equivalent, or noindex the parameterized combinations, so the crawler concentrates on the canonical page sequence. See pagination SEO best practices for headless APIs for API-side patterns that keep offsets stable.
Incremental builds and shifting page boundaries With static or incremental generation, publishing new items shifts which post lands on which page. An item at the bottom of page 2 can migrate to page 3 after a publish. Trigger a rebuild or revalidation of affected pagination routes on publish so cached page contents stay accurate.
Off-by-one on the first page
Page one usually lives at the bare listing path (/archive), not /archive/page/1. If both resolve, pick one and 301-redirect the other, otherwise you create a duplicate pair. Keep generateStaticParams starting at page 2 as shown above.
Frequently Asked Questions
Does Google still use rel=next/prev?
No. Google confirmed in 2019 that it stopped using rel=next/prev as an indexing signal. The markup is harmless to keep for browsers and other user agents, but it no longer helps Google understand a paginated sequence. Crawlability now comes entirely from real server-rendered anchor links and clean, self-canonical per-page URLs.
Should page 2+ be noindex?
Not by default. Leaving shallow pages indexable preserves the discovery of their linked content and any standalone relevance the listing carries. Apply noindex, follow only to very deep pages that offer no unique value, and never remove the crawlable links themselves — follow keeps discovery intact even when the page is dropped from the index.
Path segments or query params for pages?
Prefer path segments such as /archive/page/2. Path-based URLs are unambiguous, cache cleanly at the edge, and avoid the parameter-handling quirks that let variants like ?page=2&sort=new fragment crawl budget. Query parameters still function, but they demand extra canonical and parameter-handling discipline to stay clean.
What about a “view all” page? A single view-all page can work for moderately sized listings and consolidates ranking signals onto one URL. For large archives it becomes too heavy to render and hurts Core Web Vitals, so keep paginated URLs as the crawlable path and reserve view-all for smaller sets.
Part of: Pagination Handling in Headless
Related
- Pagination SEO Best Practices for Headless APIs — API-side offset, cursor, and caching patterns that keep paginated data crawlable
- Canonical URL Enforcement — ensure every paginated URL declares a self-referencing canonical that matches the served path
- Dynamic Routing & Indexation Workflows — the parent reference covering routing, indexation, and URL architecture for headless stacks