How to check rel next and rel prev pagination tags
Read them with curl -sS URL | grep '<link'. They are valid HTML link relations, and Google stopped using them as an indexing signal in 2019, so finding them proves little. Spend the time on the three facts that still decide whether a paginated set is crawlable: served without JavaScript, self-canonical, and not orphaned.
Why check this
Run this when pagination is added or rebuilt, after a framework upgrade that changes how navigation renders, and on release sign-off for any listing that runs past one page. It takes one request per page.
The failure it prevents is a catalogue whose later pages exist and are unreachable. Page one is linked from everywhere, pages two onward are behind a script, and every product that never appears on page one drops out of search. The tags say the pages are there, the server serves them on request, and no crawler arrives.
Checking the tags alone misses that entirely, which is why this page checks four things and treats the tags as the least of them.
Prerequisites
- curl 7.0 or later. No HTTP/2 build needed.
- Node 18 or later for the crawler, which uses
fetch. - Google's pagination documentation for the current status of the tags and of the canonical.
- A free port.
netstat -ano | grep 9137prints nothing when 9137 is free.
Save this as paged-server.js. It serves two sets of four pages: one built correctly, one carrying the three defects this procedure looks for.
// node paged-server.js listens on http://127.0.0.1:9137
// /good/page/1..4 is built correctly. /bad/page/1..4 carries three real defects.
const http = require('http');
const BASE = 'http://127.0.0.1:9137';
const LAST = 4;
const items = (n) => [1, 2, 3].map((i) => `<li><a href="/item/${n}-${i}">Item ${n}-${i}</a></li>`).join('');
const page = (set, n, { canonical, body, links }) => `<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Catalogue page ${n}</title>
<link rel="canonical" href="${canonical}">
${n > 1 ? `<link rel="prev" href="${BASE}/${set}/page/${n - 1}">` : ''}
${n < LAST ? `<link rel="next" href="${BASE}/${set}/page/${n + 1}">` : ''}
</head><body><h1>Catalogue page ${n}</h1>
<ul>${body}</ul>
<nav>${links}</nav>
</body></html>
`;
const good = (n) => page('good', n, {
canonical: `${BASE}/good/page/${n}`,
body: items(n),
links: [n > 1 ? `<a href="/good/page/${n - 1}">Previous</a>` : '',
n < LAST ? `<a href="/good/page/${n + 1}">Next</a>` : ''].join(' ')
});
const bad = (n) => page('bad', n, {
canonical: `${BASE}/bad/page/1`, // defect 1: canonical to page one
body: n === 3 ? '' : items(n), // defect 2: page 3 needs JS for its items
links: n === 2
? '<a href="/bad/page/1">Previous</a> <button onclick="location=\'/bad/page/3\'">Next</button>'
: [n > 1 ? `<a href="/bad/page/${n - 1}">Previous</a>` : '', // defect 3: no href
n < LAST ? `<a href="/bad/page/${n + 1}">Next</a>` : ''].join(' ')
});
http.createServer((req, res) => {
const m = /^\/(good|bad)\/page\/([1-4])$/.exec(req.url);
res.writeHead(m ? 200 : 404, { 'content-type': 'text/html; charset=utf-8' });
res.end(m ? (m[1] === 'good' ? good(+m[2]) : bad(+m[2])) : '<h1>Not found</h1>');
}).listen(9137, '127.0.0.1', () => console.log('listening on 127.0.0.1:9137'));
Save this as pagecheck.mjs. It walks the set twice, once by link and once by tag, and compares the two.
// node pagecheck.mjs <url-of-page-one>
// Reads a paginated set the way a crawler without JavaScript reads it.
const start = process.argv[2];
const origin = new URL(start).origin;
const seen = new Map();
const get = async (url) => {
if (seen.has(url)) return seen.get(url);
const res = await fetch(url);
const html = await res.text();
const attr = (rel) => new RegExp(`<link[^>]*rel="${rel}"[^>]*href="([^"]+)"`, 'i').exec(html)?.[1] ?? null;
const page = {
url, status: res.status, bytes: html.length,
items: (html.match(/<li>/g) ?? []).length,
canonical: attr('canonical'), next: attr('next'), prev: attr('prev'),
anchors: [...html.matchAll(/<a\s[^>]*href="([^"]+)"/gi)].map((m) => new URL(m[1], url).href)
.filter((u) => u.startsWith(origin) && /\/page\/\d+$/.test(u))
};
seen.set(url, page);
return page;
};
// Pass A: what a crawler reaches by following <a href> only.
const reached = new Set([start]);
for (const url of reached) for (const a of (await get(url)).anchors) reached.add(a);
// Pass B: what the rel=next chain declares.
const chain = [];
for (let url = start; url; url = (await get(url)).next) { chain.push(url); if (chain.length > 50) break; }
const path = (u) => new URL(u).pathname;
console.log('page status items self-canonical reached-by-href');
for (const url of chain) {
const p = await get(url);
console.log(`${path(url).padEnd(20)} ${p.status} ${String(p.items).padStart(2)} ` +
`${(p.canonical === url ? 'yes' : 'no -> ' + path(p.canonical)).padEnd(18)} ${reached.has(url) ? 'yes' : 'NO'}`);
}
console.log(`rel=next chain: ${chain.map(path).join(' -> ')}`);
console.log(`href graph: ${[...reached].map(path).sort().join(', ')}`);
const orphans = chain.filter((u) => !reached.has(u)).map(path);
console.log(`orphaned: ${orphans.length ? orphans.join(', ') : 'none'}`);
Start it and keep the Windows PID, which step 7 needs.
node paged-server.js & netstat -ano | grep 9137
Steps
- Step 1.
Read every link relation in the head of one page.
curl -sS http://127.0.0.1:9137/good/page/2 | grep -i '<link'<link rel="canonical" href="http://127.0.0.1:9137/good/page/2"> <link rel="prev" href="http://127.0.0.1:9137/good/page/1"> <link rel="next" href="http://127.0.0.1:9137/good/page/3">Three relations, and only the first one decides anything. Google's pagination documentation, read on 2026-09-11, says of the other two:
In the past, Google used
<link rel="next" href="...">and<link rel="prev" href="...">to identify next page and previous page relationships. Google no longer uses these tags, although these links may still be used by other search engines.The announcement was made in 2019. Keeping the tags is correct HTML and costs nothing. Treating their presence as the pagination check is what this page is against.
- Step 2.
Count what each page serves to a client that runs no JavaScript.
for n in 1 2 3 4; do printf "bad page %s: " "$n" curl -sS "http://127.0.0.1:9137/bad/page/$n" | grep -o '<li>' | wc -l donebad page 1: 3 bad page 2: 3 bad page 3: 0 bad page 4: 3Page 3 answers 200 and carries no items. In a browser it looks complete, because the list arrives after the script runs. To curl, and to anything that indexes the served HTML, page 3 is an empty list with a heading over it.
- Step 3.
Read the canonical of every page in the set.
for n in 1 2 3 4; do curl -sS "http://127.0.0.1:9137/bad/page/$n" | grep -i 'rel="canonical"'; done<link rel="canonical" href="http://127.0.0.1:9137/bad/page/1"> <link rel="canonical" href="http://127.0.0.1:9137/bad/page/1"> <link rel="canonical" href="http://127.0.0.1:9137/bad/page/1"> <link rel="canonical" href="http://127.0.0.1:9137/bad/page/1">Four pages, one canonical URL. Each page is declaring that page 1 is the real version of itself, which is a claim about content that is not true: the items differ on every page. The same documentation is direct about it, saying not to use the first page of a sequence as the canonical page and to give each page its own canonical URL.
- Step 4.
List the navigation links a crawler can follow, page by page.
for n in 1 2 3 4; do printf "bad page %s: " "$n" curl -sS "http://127.0.0.1:9137/bad/page/$n" | grep -o 'href="/bad/page/[0-9]"' | tr '\n' ' ' echo donebad page 1: href="/bad/page/2" bad page 2: href="/bad/page/1" bad page 3: href="/bad/page/2" href="/bad/page/4" bad page 4: href="/bad/page/3"Page 2 links backwards and not forwards. The forward control is there, and it is not a link.
curl -sS http://127.0.0.1:9137/bad/page/2 | grep -o '<nav>.*</nav>'<nav><a href="/bad/page/1">Previous</a> <button onclick="location='/bad/page/3'">Next</button></nav>A button with an
onclickworks for anyone with a mouse and a running script. It is not in the document's link graph, so nothing that walks links follows it. - Step 5.
Walk the set both ways and compare. The crawler follows
<a href>for one pass and therel=nextchain for the other.node pagecheck.mjs http://127.0.0.1:9137/bad/page/1page status items self-canonical reached-by-href /bad/page/1 200 3 yes yes /bad/page/2 200 3 no -> /bad/page/1 yes /bad/page/3 200 0 no -> /bad/page/1 NO /bad/page/4 200 3 no -> /bad/page/1 NO rel=next chain: /bad/page/1 -> /bad/page/2 -> /bad/page/3 -> /bad/page/4 href graph: /bad/page/1, /bad/page/2 orphaned: /bad/page/3, /bad/page/4The two passes disagree, and that gap is the finding. The tags declare four pages. The links reach two. Page 4 is orphaned by a defect on page 2, one page away from it, which no per-page check would attribute correctly. Now run the same command against the set that is built right.
node pagecheck.mjs http://127.0.0.1:9137/good/page/1page status items self-canonical reached-by-href /good/page/1 200 3 yes yes /good/page/2 200 3 yes yes /good/page/3 200 3 yes yes /good/page/4 200 3 yes yes rel=next chain: /good/page/1 -> /good/page/2 -> /good/page/3 -> /good/page/4 href graph: /good/page/1, /good/page/2, /good/page/3, /good/page/4 orphaned: noneFour pages, four self-canonicals, twelve items served without a script, and the two passes agree.
- Step 6.
Ask for a page past the last one.
for p in 4 5 9999; do curl -sS -o /dev/null -w "%{http_code} %{size_download} bytes %{url_effective}\n" "http://127.0.0.1:9137/good/page/$p" done200 451 bytes http://127.0.0.1:9137/good/page/4 404 18 bytes http://127.0.0.1:9137/good/page/5 404 18 bytes http://127.0.0.1:9137/good/page/9999The set ends. A listing that answers 200 with an empty result for any page number generates an unbounded supply of soft 404s, each one a URL a crawler can find and none of them worth having.
- Step 7.
Stop the server by the PID
netstatreported.taskkill //PID 15916 //F; netstat -ano | grep LISTENING | grep 9137SUCCESS: The process with PID 15916 has been terminated.No line after it means the port is free. The doubled slashes are for Git Bash. Kill the PID, never the image name.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| orphaned: none and every row self-canonical | The set is crawlable as served | Nothing |
| A page in the rel=next chain and not in the href graph | It is reachable only through a tag most crawlers ignore | Add a real <a href> to it from the page before |
| no -> /page/1 in the canonical column | Every page claims to be a duplicate of page one | Give each page its own canonical. See How to check a self referencing canonical |
| items 0 on a page that answers 200 | The list needs JavaScript that a crawler may not run | Serve the items in the HTML, then re-run step 2 |
| A page number past the end answering 200 | Unbounded crawl space made of soft 404s | Answer 404 past the last page. See How to check soft 404 |
| rel=next absent | Nothing on its own | Check the href graph. The tags are not the crawl path |
Common mistakes
What to check next
- How to check a self referencing canonical: the canonical column in step 5, on any page.
- How to check if googlebot can render javascript: what happens to page 3 when the script does run.
- How to check soft 404: for the empty page past the end of the set.
- How to validate sitemap xml: the other place paginated URLs are declared.
- How to check if a url is blocked by robots.txt: confirm the later pages are allowed before blaming the link graph.
FAQ
Should I include paginated results in my sitemap.xml?
You can. Pages two onward are distinct URLs with distinct content, so listing them is valid. It does not replace linking to them: a crawler that walks links reaches only what the href graph reaches, which is why step 5 checks the graph and not the sitemap.
Are rel=next and rel=prev worth keeping?
Keeping them is cheap and harmless. They are valid HTML relations, and Google's own note says other search engines may still use them. Removing them fixes nothing, and adding them fixes nothing either. Treat them as documentation of the sequence.
Should page 2 canonicalise to page 1?
No. Page 2 holds different items, so it is not a duplicate of page 1. Google's pagination documentation says to give each page in the sequence its own canonical URL. Step 3 shows the defect and step 5 flags it per page.
How do I check pagination that loads on scroll?
The same way. Fetch each page URL and count what arrives without a script, as in step 2. If the items are absent from the served HTML, the set depends on JavaScript running. Then confirm that every batch has a URL of its own and that something links to it.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate7 minpublished updated Maks Verny