Add canonical URLs with the Next.js Metadata API
Next.js App Router resolves metadata per route, so each page can declare its own canonical URL instead of inheriting one global value. Set alternates.canonical in the page metadata export or generateMetadata, keep it absolute and self-referencing, and make sure template redirects and trailing-slash handling agree with the URL you declare.
Why this matters
Route-level metadata beats one global tag when URLs multiply.
Before you start
- Next.js App Router project deployed behind a stable canonical host
- A single site URL constant available to metadata exports
How to apply it
Set the canonical URL policy
Choose one absolute form per page: HTTPS, lowercase host and a fixed trailing-slash rule. Store the production origin in a constant such as siteUrl so metadata, links and redirects all read the same value.
Check: The siteUrl constant resolves to the production origin, and the http, www and apex variants all redirect to it.
Keep app/layout.tsx free of a canonical
Export a metadata object from app/layout.tsx for shared fields only. Do not put a route-specific alternates.canonical there, because every route that forgets to override it inherits the homepage URL.
Check: grep -n 'alternates' app/layout.tsx shows no route-specific canonical value.
Add alternates.canonical per static page
In each page.tsx export metadata with alternates: { canonical: siteUrl + '/pricing' }. Keep the path lowercase and identical to the deployed route, then rebuild so the tag is rendered in the initial HTML.
Check: curl -s your production domain/pricing | grep -c 'rel="canonical"' returns 1 and the href equals the pricing URL.
Use generateMetadata for dynamic routes
In app/blog/[slug]/page.tsx await params, load the post and return { alternates: { canonical: siteUrl + '/blog/' + slug } } from generateMetadata so each record gets its own URL instead of the index canonical.
Check: curl -s your production domain/blog/<slug> | grep -o 'rel="canonical"[^>]*' prints the exact post URL.
Remove duplicate sources and confirm
Delete hardcoded rel=canonical tags in components, CMS widgets and SEO plugins. After deploy, fetch one page per route type, then request validation in the Search Console URL Inspection tool.
Check: curl -s your production domain/<path> | grep -c 'rel="canonical"' returns 1 for each sampled route.
Source: seo-mcp playbook · curated entry · report an error
Official references
Framework notes
nextjs-app
Set `alternates: { canonical: siteUrl + path }` in the route `metadata` export or in `generateMetadata`; keep `app/layout.tsx` free of a route-specific canonical.
nextjs-pages
In the Pages Router render `<link rel="canonical" href={absoluteUrl} />` inside `next/head` per page, or in a shared header component used by `_app.tsx`; `next-seo` also accepts `canonical`.