In short
- A canonical tag tells search engines which URL to index among duplicates. Google treats it as a strong hint, not a rule, and weighs it with redirects, sitemaps and internal links.
- Put an absolute, self-referencing canonical on every indexable page, built from the route and your production origin, never from the request host.
- The target must return 200, be indexable and canonicalize to itself. Canonicals to redirects, 404s, noindex pages or the homepage are the usual bugs.
- Redirect duplicates users don't need, canonical the ones they do, and noindex pages that shouldn't be in search at all. Don't combine noindex with a canonical elsewhere.
A canonical tag is a <link rel="canonical" href="…"> in the page head that tells search engines which URL is the main version of a page when the same content is reachable at more than one address. Google treats it as a strong signal, not a command: in its own words, indicating a canonical preference "is a hint, not a rule". Put a self-referencing, absolute canonical on every indexable page, point duplicates at the version you want indexed, and make sure that target returns 200, is indexable and canonicalizes to itself. Most canonical bugs are one of those three rules broken.
What a canonical tag does
Search engines group URLs with the same or very similar content into a cluster and pick one URL to show in results. That URL is the canonical. Links and other signals pointing at the duplicates are consolidated onto it. Your canonical tag is your vote in that choice. Google weighs it together with redirects, whether the URL is HTTPS, whether it is in your sitemap and how your internal links point.
<link rel="canonical" href="https://your-site.com/pricing" />Google ranks the signals it lets you send by strength: redirects are a strong signal, rel="canonical" is a strong signal, and a sitemap entry is a weak one. They stack, so the best result comes from all of them agreeing. If you send none, Google still picks a canonical. It just may not be the one you want.
- 1Duplicates/pricing?ref=x, /pricing/, http://, www.
- 2Canonical tagEvery variant points at https://your-site.com/pricing
- 3Other signals agreeSitemap, internal links and redirects use the same URL
- 4One URL indexedSignals from the variants count for it
When you need one
On a modern site you almost always have duplicates, even if you never wrote the same page twice. The usual sources:
| Source | Example | Fix |
|---|---|---|
| Query parameters | /pricing?utm_source=x, ?ref=, ?sort=price | Canonical to the clean URL |
| Trailing slashes | /blog/post and /blog/post/ | Redirect one style, canonical to the other |
| www and non-www | www.your-site.com vs your-site.com | 301 at the host level, canonical to the kept host |
| HTTP and HTTPS | http://your-site.com/ | 301 to HTTPS, HTTPS canonicals |
| Pagination | /blog?page=2 | Each page canonicalizes to itself, not to page 1 |
| Syndication | Your post republished on Medium or a partner site | Ask the partner for noindex (see below) |
| Preview deploys | your-app-git-branch.vercel.app | Canonical always uses the production origin |
Pagination
Google is explicit here: don't use the first page of a paginated sequence as the canonical, give each page its own. Page 2 lists different items than page 1, so it isn't a duplicate. Google also no longer uses rel="next" and rel="prev", though other search engines may.
Syndication
Cross-domain canonicals used to be the standard answer for syndicated articles. Google's current troubleshooting guide says the canonical link element is not recommended for avoiding duplication by syndication partners, because the pages are often very different. The recommended fix is for the partner to block indexing of their copy with noindex. If you can't get that, a canonical back to your original plus a visible link to it is still better than nothing, but expect the partner copy to compete.
Self-referencing canonicals
A self-referencing canonical points at the page's own clean URL. It looks redundant, but it does real work: when someone links to /pricing?ref=newsletter, the tag on that response still says /pricing, so the parameter version folds into the clean one. Put one on every indexable page.
Build it from the route, not from the incoming request:
- Use the production origin, never the host header. Otherwise previews and staging copies canonicalize to themselves.
- Drop tracking and sort parameters. Keep a parameter only if it changes the main content (for example
?page=2). - Match your trailing-slash style and the URLs in your sitemap exactly.
- Use absolute URLs. Google's guidance says to use absolute paths rather than relative ones.
rel=canonical vs 301 vs noindex
These three get mixed up because they all "remove" a URL from results. They do different things, and picking the wrong one is behind many indexing reports.
| rel=canonical | 301/308 redirect | noindex | |
|---|---|---|---|
| What it says | “Index that URL instead of this one” | “This URL moved, go there” | “Don't index this page” |
| Users see | The duplicate page | The target page | The page itself |
| Strength | Strong hint; Google can override it | Strong signal | Directive Google follows once it crawls the page |
| Signals | Consolidated onto the canonical | Consolidated onto the target | Dropped with the page |
| Use for | Parameters, filtered views, near-duplicates that must stay reachable | Renamed URLs, http to https, www to non-www | Thin or private pages: internal search results, thank-you pages |
| Don't use for | Pointing different pages at the homepage | Variants users still need (filters, tracking links) | Choosing a canonical between duplicates |
Rule of thumb: if the duplicate doesn't need to exist for users, redirect it. If it does, canonical it. If the page shouldn't be in search at all, noindex it and don't combine that with a canonical to another page. And don't use robots.txt or the URL removal tool for canonicalization: Google says so directly. A blocked URL can still be indexed without its content, and Google never sees its canonical tag.
Common canonical mistakes
- Canonical to a redirect or a 404. The target must return 200 directly. A canonical that points at an old slug, which now redirects, sends two conflicting signals.
- Canonical to a noindex page. You ask Google to index URL B instead of A, and B says "don't index me". The likely result is that neither is indexed the way you want.
- Canonical chains. A points to B, B points to C. Point every canonical at the final URL, and make the final URL canonicalize to itself.
- Every page canonicalized to the homepage. A layout-level canonical of
/is a classic copy-paste bug. Google ignores canonicals to pages that aren't duplicates, and your inner pages get reported as duplicates or excluded. - Relative or protocol-relative URLs.
href="/pricing"orhref="//your-site.com/pricing"resolve against whatever host served the page, including mirrors and previews. Use the fullhttps://URL. - Preview URLs leaking to production. A canonical built from
VERCEL_URLor the request host can shiphttps://your-app-abc123.vercel.app/…as the canonical of your live site. - Conflicting signals. The sitemap lists
/blog/post/, the canonical says/blog/post, internal links use a third variant. Make them all the same URL. - Canonical outside the head. Google only accepts
rel="canonical"in the<head>. Invalid markup that closes the head early (a<div>or<iframe>placed before the tag) pushes it into the body. - Canonical across languages. With hreflang, each language version canonicalizes to itself (Google says to specify a canonical in the same language). Pointing
/de/at/en/tells Google the German page is a duplicate. - Two canonical tags. A plugin or layout adds one and the page adds another. When they disagree, Google may ignore both. Check the server HTML for exactly one.
Reading “Google chose different canonical than user”
In Search Console's Page indexing report, three statuses relate to canonicals. They mean different things:
| Status | What it means | Action |
|---|---|---|
| Alternate page with proper canonical tag | The page points at another canonical that is indexed | Nothing, this is working as intended |
| Duplicate without user-selected canonical | Google found duplicates and you declared no canonical | Add a canonical if Google picked the wrong one |
| Duplicate, Google chose different canonical than user | You declared a canonical; Google thinks another URL is better | Investigate (steps below) |
- Inspect the URL
Open URL Inspection for an affected page. Compare "User-declared canonical" with "Google-selected canonical".
- Ask whether they really are duplicates
Google's help says that if the URL you declared isn't similar to the page, Google won't choose it. If the pages differ (different products, different articles), you have a canonical bug: fix the tag.
- Look for conflicting signals
Check what the sitemap lists, where internal links point, whether your canonical target redirects, and whether the HTTP or www variant still answers with 200. Google tends to pick the URL most of your signals agree on.
- Align everything, then wait
Make the canonical, sitemap, internal links and redirects agree on one URL. Google updates its choice the next time it crawls both URLs. Request indexing on the canonical to speed that up.
If Google picks a duplicate you consider equivalent (say, the trailing-slash variant), it is usually harmless. Fix the inconsistency anyway, so signals stop splitting. Expect a lag: Google's troubleshooting guide says pages can stay in the same duplicate cluster for up to two weeks after you make them meaningfully different.
Troubleshooting: symptom, cause, fix
| Symptom | Likely cause | Fix |
|---|---|---|
| Canonical visible in DevTools, missing from curl output | Set on the client (useEffect, a client-only head component) | Render it in server metadata. Google says if JavaScript sets it, it must not change one already in the HTML |
| Next.js: canonical appears after <body> in curl output | Streaming metadata: on dynamically rendered pages, generateMetadata output is appended to the body for clients that run JavaScript | Next.js says Googlebot reads it correctly. To force it into <head> for everyone, set htmlLimitedBots: /.*/ (slower first byte). Prerendered pages aren't affected |
| Two canonical tags in the HTML | Layout and page both set one, or a plugin adds a second | Keep one source. Check with grep -c |
| Link header and <link> tag disagree | Hosting or CDN rule adds a canonical header | Use one method. Google calls mixing them error-prone |
| Canonical ends in #section | Fragment copied from the current URL | Drop the fragment. Google generally doesn't support fragments |
| Wrong language version shows in results | hreflang URLs point at non-canonical variants | Make every hreflang URL the canonical URL of that language page |
| Search Console still shows the old choice after a fix | Google hasn't recrawled both URLs yet | Wait up to about two weeks, then request indexing on the canonical |
To check a whole site, compare each sitemap URL with the canonical it serves:
SITE=https://your-site.com
curl -s "$SITE/sitemap.xml" | grep -o '<loc>[^<]*' | sed 's/<loc>//' | while read -r url; do
canon=$(curl -s "$url" | grep -io '<link[^>]*rel="canonical"[^>]*>' | grep -o 'href="[^"]*"' | cut -d'"' -f2)
[ "$canon" = "$url" ] || echo "$url -> ${canon:-no canonical}"
done
# https://your-site.com/blog/post/ -> https://your-site.com/blog/postEvery line it prints is a sitemap URL whose canonical disagrees with it, which is exactly the "conflicting signals" case above. Two canonicals on one page also show up, as two URLs on one line. This is also a good task to hand a coding agent: give it the output and ask it to fix the route that builds each canonical, not the individual pages.
Setting canonicals in Next.js, Astro, SvelteKit and HTML
Next.js (App Router)
Next.js doesn't add a canonical for you. Set metadataBase once in the root layout, then return alternates.canonical from each page. Relative paths are joined to metadataBase, so the output is absolute. Using a relative URL field without metadataBase is a build error.
import type { Metadata } from "next";
// Hard-code production, or read a variable you set only for production.
// Don't use VERCEL_URL: it is the deployment's own URL, previews included.
export const metadata: Metadata = {
metadataBase: new URL("https://your-site.com"),
};import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
// Built from the route, so ?utm_ and ?ref= variants share it.
alternates: { canonical: `/blog/${post.slug}` },
};
}If you rename a slug, use permanentRedirect() or a redirects() entry for the old URL and build the canonical from the new one. The Next.js SEO checklist covers the rest of the metadata setup.
Astro
Set site in astro.config.mjs, then resolve the current path against it. This is the pattern from Astro's own docs; Astro.site is undefined if site is not set.
---
// astro.config.mjs: export default defineConfig({ site: "https://your-site.com" })
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<link rel="canonical" href={canonicalURL} />SvelteKit
Use the path from page.url but your production origin, not page.url.origin, which is whatever host served the request.
<script>
import { page } from "$app/state";
import { PUBLIC_SITE_URL } from "$env/static/public"; // https://your-site.com
</script>
<svelte:head>
<link rel="canonical" href={new URL(page.url.pathname, PUBLIC_SITE_URL).href} />
</svelte:head>Plain HTML
One tag in the <head>, absolute URL, exactly one per page. For non-HTML files such as PDFs, Google also accepts a Link HTTP header.
<head>
<link rel="canonical" href="https://your-site.com/docs/install" />
</head>
<!-- HTTP header alternative, e.g. for a PDF -->
Link: <https://your-site.com/whitepaper.pdf>; rel="canonical"Then confirm what the server actually sends:
curl -s "https://your-site.com/pricing?utm_source=test" | grep -io '<link[^>]*rel="canonical"[^>]*>'Catching canonical bugs automatically
Canonical bugs rarely show up in the browser, so they ship easily, especially when an agent edits a layout. Serpkin checks them in two places.
Site scans check that each page has an absolute canonical URL, then load the canonical targets and flag four problems as missions: canonicals pointing to noindex pages, to missing or redirected pages, to a page that canonicalizes somewhere else (a chain), and to another domain. Each mission tells your agent the file and the fix, for example returning the final URL in alternates.canonical.
Preview checks run before you merge:
$ npx serpkin check https://your-branch.vercel.app Checking https://your-branch.vercel.app against production… ✗ /pricing The page has no canonical link. ✗ /blog/launch The canonical link points to a different site than production or this preview. Found regressions on the preview (11 pages checked).
The check fails when a preview page's canonical is missing, invalid, or points at a host that is neither production nor the preview itself. It also loads canonical targets and fails when one returns an error, is noindex or canonicalizes again. Problems already in production show as warnings, and the command exits with code 1 on a regression, so it works in CI. For a one-off look at a single URL, the free SEO checker shows its canonical without an account.
Questions
Should every page have a self-referencing canonical?
Yes, every indexable page. It makes parameter and tracking variants of the URL fold into the clean one, and it protects you when your content is copied or served on another host.
Canonical tag or 301 redirect: which is better?
Use a 301 (or 308) when the old URL doesn't need to exist, such as a renamed page or http to https. Use a canonical when the duplicate must stay reachable for users, such as filtered or tracked URLs.
How do I check which canonical Google chose?
Open URL Inspection in Search Console for the page. It shows the user-declared canonical (yours) and the Google-selected canonical side by side. To see what your server sends, curl the page and grep for rel="canonical".
Can a canonical tag point to another domain?
Yes, Google supports cross-domain canonicals. For syndicated content, though, Google now recommends that partners noindex their copy instead, because syndicated pages often differ too much for a canonical to be reliable.
Why does Search Console say Google chose a different canonical than user?
Google decided another URL is the better canonical. Common causes are a canonical pointing at a page that isn't really a duplicate, or a sitemap, internal links and redirects that point at a different variant than the tag.
Sources
- Google: How to specify a canonical URL (checked October 27, 2026)
- Google: What is canonicalization (hint, not a rule) (checked October 27, 2026)
- Google: Fix canonicalization issues (syndication) (checked October 27, 2026)
- Google: Pagination and incremental page loading (checked October 27, 2026)
- Search Console Help: URL Inspection tool (checked October 27, 2026)
- Search Console Help: Page indexing report (checked October 27, 2026)
- Next.js docs: generateMetadata (metadataBase, streaming metadata) (checked October 27, 2026)
- Next.js docs: htmlLimitedBots (checked October 27, 2026)
- Astro docs: Astro.site canonical example (checked October 27, 2026)
- SvelteKit docs: $app/state (checked October 27, 2026)