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.

highIn codeimpact effort

Why this matters

Route-level metadata beats one global tag when URLs multiply.

Get the full breakdown

The extended brief explains when this works, when it does not, and the trade-offs. Join the waitlist and it unlocks for every tip when sign-in opens; MCP access is the paid part.

Join the waitlist

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

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

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

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

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

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

Get the prompt for your agent

A ready-to-paste prompt for your agent that implements this tip and verifies the result in your repo.

Join the waitlist

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

Let your agent apply this + 161 more tips.

Pre-order — $19