The short version
- Google treats a canonical tag as a hint. It weighs it with redirects, sitemaps and HTTPS, and can pick another URL.
- Each indexable page should name itself, with an absolute https URL on the production host, in the HTML the server sends.
- In Next.js, set
metadataBaseonce in the root layout andalternates.canonicalon each page. A canonical set in the root layout is inherited by every page that doesn’t set its own. - URL Inspection in Search Console shows which canonical Google chose. Changes can take up to two weeks to be re-evaluated.
A canonical is a hint
When several URLs show the same or very similar content, Google groups them and picks one to represent the group. That one is the canonical URL, and it’s the one that usually appears in results. Google’s canonicalization overview says that stating your preference “is a hint, not a rule”.
Google ranks the methods by how strongly they influence the choice (source):
- Redirects, a strong signal that the target should become canonical.
rel="canonical", in the HTML head or as an HTTP header, also a strong signal.- Sitemap inclusion, a weak signal.
The basics of the tag are in the indexing guide. The rest of this page is about the ways it goes wrong.
Canonicals that point at the wrong place
Every page points at the homepage
This comes from putting the canonical in a shared layout or template. Every page then tells Google that it is a duplicate of the homepage. Google’s troubleshooting guide lists a CMS or plugin pointing canonicals at undesired URLs among the causes of a wrong canonical (source). The Next.js version of this mistake is described below.
The canonical names a staging or preview host
If the canonical is built from the request’s host or from a deployment URL variable, a page served on a preview hostname such as your-app.vercel.app or staging.yoursite.com names that host as canonical. Google counts a demo site left open to crawlers among the common sources of duplicates (source). Hard-code the production origin, or read it from a variable that has the same value in every environment.
Relative URLs
Google supports relative paths in a canonical but doesn’t recommend them, because they “can cause problems in the long run”, and its example is a testing site that gets crawled by accident (source). With href="/pricing", a copy of the page on any other host declares itself canonical. Write the full URL.
http, https and www
Google prefers HTTPS pages as canonical, with exceptions that include an HTTPS page whose canonical link points to HTTP, an invalid certificate, and HTTPS pages that redirect to HTTP (source). Pick one origin, for example https://yoursite.com, redirect the http and www variants to it, and use that exact origin in every canonical.
Trailing slashes, query strings and case
Trailing slash. Google treats /pricing and /pricing/ as separate URLs. The root is the only exception: https://yoursite.com and https://yoursite.com/ are equivalent. Its advice is to choose one form, redirect the other to it with a 301, and use the chosen form in internal links and the sitemap (source). The canonical has to use it too.
Query strings. /pricing?ref=newsletter is a different URL with the same content, and Google warns that parameters which don’t change the content can create large numbers of them (source). The canonical on that page should be the clean /pricing. The mistake is building the canonical from the full request URL, so each tracking variant names itself.
Case. Google’s URL handling is case sensitive (source), so use lowercase everywhere if your server answers /Pricing and /pricing alike.
Canonicals added by client-side JavaScript
A single-page app usually serves one HTML shell for every route and sets the head in the browser. That produces two problems.
First, if the shell contains a canonical, it’s the same one on every route, and JavaScript then changes it. Google’s JavaScript SEO guide says not to do this: you shouldn’t use JavaScript to change the canonical to something other than the URL in the original HTML, and conflicting or multiple canonical tags “may lead to unexpected results”.
Second, if the shell has no canonical and JavaScript injects one, Google picks it up only when it renders the page, which happens in a separate queue after crawling. Google doesn’t recommend this route, and crawlers that don’t run JavaScript never see the tag.
Google’s guidance comes down to an order of preference:
- Put the correct canonical in the HTML the server sends, and don’t let JavaScript touch it.
- If JavaScript sets one as well, make it identical to the one in the HTML.
- If you can’t put the right value in the HTML, leave it out of the HTML and set it only with JavaScript, as the single canonical tag on the page.
For a client-rendered app, the fix is to render the head on the server, by server-side rendering or prerendering the public pages. Google also says the tag is only accepted when it appears in the <head> (source).
Conflicting signals
Google’s instruction is direct: don’t specify different URLs as canonical for the same page using different techniques (source). The usual conflicts:
- Sitemap. The sitemap lists
/pricing/or an http URL while the tag sayshttps://yoursite.com/pricing. List only canonical URLs. See sitemap and robots.txt. - Redirects. The canonical names a URL that redirects somewhere else. A canonical should name a URL that returns the page.
- Internal links. Google asks you to link to the canonical URL, not a duplicate.
- hreflang. Each language version must list itself and all the others, with fully qualified URLs (source). The canonical on a translated page should be a page in the same language, so don’t point every translation at the English one.
- Two canonical tags. One from the framework and one from a plugin or a hand-written
<link>. Search the page source forrel="canonical"and expect one match.
Setting it in Next.js
This follows the App Router’s metadata API as documented for Next.js 16 (generateMetadata reference). Set metadataBase once in the root layout, then give each page its own alternates.canonical as a path. Next.js joins the two into an absolute URL.
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://yoursite.com"),
};
// app/pricing/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Pricing",
alternates: { canonical: "/pricing" },
};
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
return { alternates: { canonical: "/blog/" + slug } };
}The pricing page outputs <link rel="canonical" href="https://yoursite.com/pricing" />. Things to know, all from the same reference:
- Don’t put a canonical in the root layout. Metadata from layouts and pages is merged shallowly, and a page that doesn’t set a nested field inherits the layout’s. A root layout with
alternates: { canonical: "/" }gives every page without its own canonical a canonical to the homepage. - A page’s
alternatesreplaces the layout’s whole object. If the layout setsalternates.languagesoralternates.typesand a page sets onlycanonical, the others are dropped for that page. Repeat them or share them through a variable. - Keep
metadataBasefixed to production. If it’s computed from the deployment host, preview builds emit preview canonicals. A relative path with nometadataBaseat all is a build error. - Dynamic metadata can be streamed. When
generateMetadatadepends on request-time data, Next.js may send the page first and append the metadata tags to the<body>. The docs say bots that run JavaScript, Googlebot included, interpret this correctly, and that bots that can’t are detected by user agent and get the tags in the<head>. That’s why curl may show the canonical in the body. Google’s own documentation only promises to accept the tag in the head, so if you’d rather not depend on this, sethtmlLimitedBots: /.*/in the Next.js config to turn streaming metadata off. - Match your slash setting. By default Next.js redirects
/about/to/about, andtrailingSlash: truereverses that (source). Write canonicals in the form your config serves.
How to check
- Fetch three pages from the live site (the homepage, one static page, one dynamic page) and read the canonical in the raw HTML:
curl -s https://yoursite.com/pricing | grep -io '<link rel="canonical"[^>]*>' - Confirm each one names itself, starts with your production https origin, has no query string, and uses your chosen slash form. The meta tag checker shows the canonical next to the page’s other tags.
- Fetch the same page with a tracking parameter added, and again on a preview deployment. The canonical should not change.
- In Search Console, inspect the URL and compare the canonical you declared with the one Google selected (Google’s troubleshooting guide).
If Google selected a different URL, it considers the two pages duplicates. Fix the signals above, or make the pages meaningfully different. Google says pages can stay grouped for up to two weeks after a fix. A page that is out of the index for other reasons is covered in crawled, currently not indexed.
More guides
- How to get your site indexed by Google and BingSet up Search Console and Bing Webmaster Tools, submit a sitemap, fix robots.txt, noindex and canonical mistakes, use IndexNow, and check whether your pages are indexed.
- Meta tags that matter on a product landing pageWhich head tags Google supports and which it ignores, how it rewrites titles and snippets, the Open Graph tags to add, and a head block to copy.
- Open Graph images: sizes, caching and debuggingThe og:image tags, the sizes and file limits Meta and LinkedIn document, why an old image keeps showing, how to refresh it, and how to debug a broken card.
- “Crawled – currently not indexed”: causes and fixesWhat “Crawled – currently not indexed” means in Search Console, how it differs from similar statuses, what to check and fix, and when to leave it alone.
- Sitemap and robots.txt for a small SaaSWhat belongs in a sitemap and robots.txt on a small SaaS, what to block and what never to block, with working examples and Next.js code.
- Structured data for a software productWhich schema.org types fit a SaaS landing page, what Google requires for each, a JSON-LD example, how to test it, and the markup that leads to manual actions.
- Alternatives and comparison pages without thin contentHow to publish “X alternatives” and “A vs B” pages that help buyers: Google’s policies, what makes each page distinct, fairness, and upkeep.