How to check canonical tag
Save the page and its headers in one request with curl -s -D head.txt -o page.html <url>, then search both files for a canonical. The HTML tag can be split across lines, so flatten newlines before you grep. A page may also send a Link: header with rel="canonical", and the two values can disagree.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
Run this on staging sign-off and after any template change that touches the document head. A canonical names which URL is the real one, so a wrong value hands the ranking of a live page to a URL that does not exist. The failure it prevents is concrete: a release ships a relative canonical written for production, staging serves the same template, and every staging page now declares a canonical on the staging hostname.
Prerequisites
- curl 7.x or later.
-Dwrites the response headers to a file while-owrites the body, so one request gives you both. - Node 22 for the resolver in step 6 and the local target in step 5.
- The rel=canonical reference on MDN and Google's page on consolidating duplicate URLs, which documents both delivery methods.
Steps
- Step 1.
Fetch the page once and keep the headers and the body.
curl -s -D mdn.head -o mdn.html -w 'code=%{http_code} bytes=%{size_download}\n' https://developer.mozilla.org/en-US/docs/Web/HTTPcode=200 bytes=221672Every later step reads these two files, so the target is fetched once.
- Step 2.
Grep the saved HTML the obvious way.
grep -o -i '<link[^>]*rel="canonical"[^>]*>' mdn.html; echo "exit=$?"exit=1Nothing matched, and the page does have a canonical. Read step 3 before you report the tag as missing.
- Step 3.
Flatten newlines, then grep the same pattern.
tr '\n' ' ' < mdn.html | grep -o -i '<link[^>]*rel="canonical"[^>]*>' | tr -s ' '<link rel="canonical" href="https://developer.mozilla.org/en-US/docs/Web/HTTP" />The tag was there the whole time. This template prints the element across four lines, and
grepworks one line at a time, so the single-line pattern could never match it. The secondtrsqueezes the spaces that flattening produced. - Step 4.
Check the response headers for the other delivery method.
grep -i '^link:' mdn.head; echo "exit=$?"exit=1No
Link:header here, so the element is the only source on this page. A PDF can only use the header, having no head section to put an element in. - Step 5.
Start a target that sends both, and read each source on its own. Save this as
canonical-target.jsand runnode canonical-target.js.const http = require('node:http'); const html = (canon) => `<!doctype html><html><head>\n <link\n rel="canonical"\n href="${canon}"\n />\n <title>Canonical target</title>\n</head><body>page</body></html>`; http .createServer((req, res) => { const h = { 'content-type': 'text/html; charset=utf-8' }; if (req.url === '/both') { h.link = '<http://127.0.0.1:8932/from-header>; rel="canonical"'; res.writeHead(200, h); res.end(html('/from-tag')); } else if (req.url === '/relative') { res.writeHead(200, h); res.end(html('/docs/page')); } else { res.writeHead(200, h); res.end(html('http://127.0.0.1:8932/')); } }) .listen(8932, () => console.log('canonical target on port 8932'));Then read the header source and the HTML source separately.
curl -sI http://127.0.0.1:8932/both | grep -i -E '^HTTP|^link'; curl -s http://127.0.0.1:8932/both | tr '\n' ' ' | grep -o -i '<link[^>]*rel="canonical"[^>]*>' | tr -s ' 'HTTP/1.1 200 OK link: <http://127.0.0.1:8932/from-header>; rel="canonical" <link rel="canonical" href="/from-tag" />Two canonicals, two different URLs, one response. A header-only check sees the first line and passes. An HTML-only check sees the third and passes. Only a check that reads both finds the defect.
- Step 6.
Resolve both sources against the request URL and compare them. Save this as
canonical.mjs.const res = await fetch(process.argv[2], { redirect: 'follow' }); const body = await res.text(); const head = res.headers.get('link'); const hm = head && /<([^>]+)>\s*;[^,]*rel="?canonical"?/i.exec(head); const tm = /<link\b[^>]*rel=["']?canonical["']?[^>]*>/i.exec(body); const th = tm && /href=["']([^"']+)["']/i.exec(tm[0]); const abs = (v) => (v ? new URL(v, res.url).href : null); console.log('requested ', process.argv[2]); console.log('after hops ', res.url); console.log('header raw ', hm ? hm[1] : '(none)'); console.log('header abs ', abs(hm && hm[1])); console.log('tag raw ', th ? th[1] : '(none)'); console.log('tag abs ', abs(th && th[1])); const a = abs(hm && hm[1]), b = abs(th && th[1]); console.log('verdict ', a && b ? (a === b ? 'agree' : 'CONFLICT') : a || b ? 'single source' : 'NO CANONICAL');node canonical.mjs http://127.0.0.1:8932/bothrequested http://127.0.0.1:8932/both after hops http://127.0.0.1:8932/both header raw http://127.0.0.1:8932/from-header header abs http://127.0.0.1:8932/from-header tag raw /from-tag tag abs http://127.0.0.1:8932/from-tag verdict CONFLICTIt resolves each value with
new URL(value, requestUrl), the algorithm a browser uses, so the comparison is between absolute URLs, not attribute strings. - Step 7.
Ask the same relative canonical from two hostnames.
for h in 127.0.0.1 localhost; do node canonical.mjs "http://$h:8932/relative" | grep -E 'requested|tag raw|tag abs'; donerequested http://127.0.0.1:8932/relative tag raw /docs/page tag abs http://127.0.0.1:8932/docs/page requested http://localhost:8932/relative tag raw /docs/page tag abs http://localhost:8932/docs/pageOne byte-identical tag, two different canonical URLs.
/docs/pageresolves against the document that carries it, so the host that served the page decides the answer, not the template. Nobody reading the attribute in isolation can see this. Stop the server afterwards:netstat -ano | grep 8932names the PID to stop. - Step 8.
Confirm the resolution in a browser. Open the page, then paste this in the DevTools Console.
(() => { const l = document.querySelector('link[rel=canonical]'); return { location: location.href, attribute: l.getAttribute('href'), resolved: l.href }; })(){"location":"http://localhost:8932/relative","attribute":"/docs/page","resolved":"http://localhost:8932/docs/page"}getAttribute('href')returns the raw string, thehrefproperty returns the resolved URL. The Elements panel shows you the first one. Compare against the second.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| One element, absolute URL, equal to the request URL | Self-referencing canonical | Nothing. Confirm it with How to check a self-referencing canonical. |
| A single-line grep finds nothing | Usually a tag printed across several lines | Flatten newlines as in step 3 before concluding the tag is absent. |
| link: header and HTML element with different URLs | Two conflicting declarations | Remove one. Google's own guidance is to pick one method, because using both is error prone. |
| A relative href such as /docs/page | Resolves against whatever host served the page | Replace with an absolute URL. Staging otherwise declares staging. |
| No canonical anywhere | The crawler picks a URL for you | Add a self-referencing one unless the page is a deliberate duplicate. |
| Canonical points at a URL that redirects | The declaration and the routing disagree | Follow it with How to check redirect chain and point the canonical at the final URL. |
Common mistakes
What to check next
- How to check a self-referencing canonical: whether the value you found actually points back at the URL you requested.
- How to check www vs non-www canonical URL: the host half of the same question, answered from the redirect chain rather than the tag.
- How to check redirect chain: a canonical that points at a redirecting URL wastes the declaration.
- How to check robots.txt: a canonical on a page a crawler is not allowed to fetch is never read.
- Meta tag checker: reads the title, description, canonical and robots directives of one URL and reports resolved values.
FAQ
What is a canonical URL?
The URL you declare as the real address of a page when several addresses serve the same content. Search engines use it to consolidate signals onto one address instead of splitting them across duplicates. It is a declaration, not an instruction.
How do I check a canonical URL in Chrome?
Open DevTools, Console tab, and read document.querySelector('link[rel=canonical]').href. The href property returns the resolved absolute URL. The Elements panel shows the raw attribute instead, which hides the resolution shown in step 7.
How do I find a canonical issue?
Compare three values: the URL you requested, the canonical after resolution, and the canonical in the response headers. Any disagreement is the issue. Steps 5 and 6 produce all three.
Should the canonical be absolute or relative?
Absolute. Relative values are supported, and Google's documentation recommends against them because a test host that gets crawled then declares itself. Step 7 shows one identical tag producing two different canonical URLs.
Can a page have a canonical in both the header and the HTML?
Yes, and step 5 builds one. Google documents both methods and recommends choosing a single one, because using both at once is error prone. Treat a disagreement as a failing test rather than guessing a winner.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2Chrome 152
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
- Checker: meta-tags title, description, canonical, robots meta, hreflang, Open Graph, Twitter card, viewport
- All crawlability and indexing checks
basic7 minpublished updated Maks Verny