Guide · 12 min read

Canonical tags: how they work and how to get them right

For developers who want one indexed URL per page. What Google does with rel=canonical, the mistakes that break it, and copy-paste code for four stacks.

Updated · By the Serpkin team

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.

<head>
<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.

  1. 1Duplicates/pricing?ref=x, /pricing/, http://, www.
  2. 2Canonical tagEvery variant points at https://your-site.com/pricing
  3. 3Other signals agreeSitemap, internal links and redirects use the same URL
  4. 4One URL indexedSignals from the variants count for it
Several URLs serve the same pricing page. Each declares the same canonical, internal links and the sitemap use that URL too, and Google consolidates signals onto 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:

Common sources of duplicate URLs
SourceExampleFix
Query parameters/pricing?utm_source=x, ?ref=, ?sort=priceCanonical to the clean URL
Trailing slashes/blog/post and /blog/post/Redirect one style, canonical to the other
www and non-wwwwww.your-site.com vs your-site.com301 at the host level, canonical to the kept host
HTTP and HTTPShttp://your-site.com/301 to HTTPS, HTTPS canonicals
Pagination/blog?page=2Each page canonicalizes to itself, not to page 1
SyndicationYour post republished on Medium or a partner siteAsk the partner for noindex (see below)
Preview deploysyour-app-git-branch.vercel.appCanonical 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.

Which tool to use
rel=canonical301/308 redirectnoindex
What it says“Index that URL instead of this one”“This URL moved, go there”“Don't index this page”
Users seeThe duplicate pageThe target pageThe page itself
StrengthStrong hint; Google can override itStrong signalDirective Google follows once it crawls the page
SignalsConsolidated onto the canonicalConsolidated onto the targetDropped with the page
Use forParameters, filtered views, near-duplicates that must stay reachableRenamed URLs, http to https, www to non-wwwThin or private pages: internal search results, thank-you pages
Don't use forPointing different pages at the homepageVariants 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" or href="//your-site.com/pricing" resolve against whatever host served the page, including mirrors and previews. Use the full https:// URL.
  • Preview URLs leaking to production. A canonical built from VERCEL_URL or the request host can ship https://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:

Canonical statuses in the Page indexing report
StatusWhat it meansAction
Alternate page with proper canonical tagThe page points at another canonical that is indexedNothing, this is working as intended
Duplicate without user-selected canonicalGoogle found duplicates and you declared no canonicalAdd a canonical if Google picked the wrong one
Duplicate, Google chose different canonical than userYou declared a canonical; Google thinks another URL is betterInvestigate (steps below)
  1. Inspect the URL

    Open URL Inspection for an affected page. Compare "User-declared canonical" with "Google-selected canonical".

  2. 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.

  3. 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.

  4. 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

Canonical problems that only show up when you look at the served HTML
SymptomLikely causeFix
Canonical visible in DevTools, missing from curl outputSet 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 outputStreaming metadata: on dynamically rendered pages, generateMetadata output is appended to the body for clients that run JavaScriptNext.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 HTMLLayout and page both set one, or a plugin adds a secondKeep one source. Check with grep -c
Link header and <link> tag disagreeHosting or CDN rule adds a canonical headerUse one method. Google calls mixing them error-prone
Canonical ends in #sectionFragment copied from the current URLDrop the fragment. Google generally doesn't support fragments
Wrong language version shows in resultshreflang URLs point at non-canonical variantsMake every hreflang URL the canonical URL of that language page
Search Console still shows the old choice after a fixGoogle hasn't recrawled both URLs yetWait 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:

Terminal
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/post

Every 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.

app/layout.tsx
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"),
};
app/blog/[slug]/page.tsx
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.

src/layouts/Base.astro
---
// 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.

src/routes/+layout.svelte
<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.

index.html
<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:

Terminal
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:

Terminal
$ 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).
Illustrative output of npx serpkin check on a preview whose layout change broke the canonical.

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.

Check a page nowFree: see what a crawler finds on any URL, with a prompt for your agent.

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

Let your agent do the SEO work.

Serpkin finds the next fix from real search data, hands it to Claude Code, Codex or Cursor, and checks it after you ship.

npx serpkin
Start free