Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Canonical URLs in Next.js tell search engines which address is the authoritative one for a page. In the App Router you declare canonical URLs with the canonical field inside alternates, and you make them absolute by setting a base URL once in the root layout. Both live in the Metadata API, so no manual head markup is involved.
The behavior below was verified against Next.js 16.3 with the App Router, using a production build rather than the development server, because a few of these values differ between the two.
Set the base URL once
The base URL is a convenience: it lets every URL-based metadata field below it use a relative path. Put it in the root layout so it applies to every route.
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://acme.com'),
alternates: { canonical: '/' },
openGraph: { images: '/og-default.png' },
}The home page now renders a canonical link of https://acme.com and an Open Graph image URL of https://acme.com/og-default.png. Both started as relative paths and were expanded against the base.
In a real project the value comes from an environment variable so that staging and production differ, with a local fallback for development.
// app/lib/site.ts
export const siteUrl = new URL(
process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000'
)Import that value in the root layout instead of hardcoding the domain. Reading the variable at module scope keeps it out of request handling, and the public prefix is safe here because the site URL is not a secret.
Add a canonical path per route
Setting the base URL does not create canonical tags. Each route still declares its own, and a relative path is the practical form because it composes with the base.
// app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About',
alternates: { canonical: '/about' },
}The response for /about contains a canonical link pointing at https://acme.com/about. If you omit the field, no canonical tag is rendered for that route and search engines pick one themselves, which is a coin flip on sites with duplicate paths.
Note that the canonical value does not inherit usefully from a parent. A layout that sets a canonical path passes the same literal value to every child, which is why per-route values are the norm.
Query strings are the usual reason a route needs one at all. A listing reachable at /about, /about?ref=newsletter, and /about?utm_source=twitter is three URLs to a crawler and one page to a reader, and the canonical tag is what collapses them back into one.
Canonical URLs on dynamic routes
A dynamic segment needs the same treatment, built from the resolved params. That means doing it inside the metadata function rather than in a static object.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { getPost } from '@/app/lib/posts'
export async function generateMetadata({
params,
}: PageProps<'/blog/[slug]'>): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return { title: post.title, alternates: { canonical: `/blog/${slug}` } }
}Every post now points at its own URL, so a request to /blog/hello-world renders a canonical link ending in that slug. Build the path from the params rather than from a value inside the record, unless the record holds the true path and the segment is only an alias.
Filtered listings are the other common case. A route that accepts sorting or pagination query strings should usually canonicalize to the clean path, which you can do by returning the path without the search parameters.
How relative paths are resolved
Composition here favors intent over strict URL semantics, so a leading slash does not wipe out a base path. These are the resolutions for a base of https://acme.com.
| Field value | Resolved URL |
|---|---|
| / | https://acme.com |
| payments | https://acme.com/payments |
| /payments | https://acme.com/payments |
| https://beta.acme.com/payments | https://beta.acme.com/payments |
An absolute URL in the field ignores the base entirely, which is what you want when a page canonicalizes to another domain.
One case behaves differently from the table. Passing a URL instance to the canonical field composes it with the current pathname instead of using it as-is, so a value of new URL('https://shop.acme.com') on the /shop route renders as https://shop.acme.com/shop. That is useful for moving a section to another host, and surprising if you expected the bare origin.
What a missing base URL actually does
Leaving the base unset does not stop the build. It prints a warning during the build and keeps going, which is why the problem often ships.
⚠ metadataBase property in metadata export is not set for resolving social open graph or twitter images, using "http://localhost:3000". See https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadatabaseThe consequences split in two. Social image URLs get an origin from that fallback, so a local build emits image tags pointing at localhost, while canonical links are left relative, rendering as <link rel="canonical" href="/about"/>.
A relative canonical tag resolves against whatever host served the page, so a preview deployment declares itself canonical. Setting the base URL is the fix, and it takes one line in the root layout.
Deployment platforms can supply their own fallback origin for social images, which hides the warning's effect in production. Treat that as a safety net rather than as configuration, since it does nothing for canonical links.
One line in the official reference disagrees with this. It says a relative path in a URL-based field without a base URL causes a build error, and that is not the behavior of Next.js 16.3, where the build completes with the warning above and falls back to a base. The framework source carries the same fallback, so trust your build output over that sentence.
Trailing slashes and localized alternates
If your project sets the trailing slash option in the Next.js config, canonical and alternate URLs for your own origin get the slash too. A page at /about renders a canonical of https://acme.com/about/, matching the URL the site actually serves.
Language alternates live next to the canonical field and follow the same resolution rules.
// app/about/page.tsx
export const metadata = {
alternates: {
canonical: '/about',
languages: { 'en-US': '/en-US/about', 'de-DE': '/de-DE/about' },
},
}This renders one canonical link plus one alternate link per locale, each expanded to an absolute URL. Keeping them consistent with your routing is the hard part, which the article on internationalized routing covers, and the trailing slash option itself is explained in the trailingSlash guide.
Verify the tag, then move on
Canonical bugs are invisible in the browser, so check the rendered HTML of a production build rather than the metadata object you wrote.
npx next build && npx next start
curl -s http://localhost:3000/blog/hello-world | grep -o '<link rel="canonical"[^>]*>'An absolute URL with your production domain means the base URL and the route value are both correct. A relative path means the base URL is missing, and a localhost origin means the environment variable did not reach the build.
Do this for one static route and one dynamic route. Those two cover the paths where the Metadata API resolves values differently, since one is computed during the build and the other while the request is being served.
Once the tags are right, the remaining work is keeping them right. Canonical URLs in Next.js break most often when a domain changes or a route is renamed, so add these two commands to whatever check runs before a release.
Rune AI
Key Insights
- metadataBase turns relative metadata paths into absolute URLs for the segment it is set in and everything below.
- Without it, the build warns and social image URLs fall back to a host such as localhost.
- alternates.canonical accepts a relative path and resolves against the base URL.
- On dynamic routes, build the canonical path from the awaited params inside generateMetadata.
- Passing a URL instance composes it with the current pathname instead of using it verbatim.
Frequently Asked Questions
What happens if I never set metadataBase?
Should the canonical URL include query strings?
Does trailingSlash affect the canonical tag?
Can I set metadataBase per section instead of once?
Conclusion
Set metadataBase once in the root layout so relative metadata paths become absolute URLs, then give each route an explicit canonical path. On dynamic routes, build the path from the awaited params so every record points at itself. Verify the tag in the rendered HTML, because a missing base URL fails quietly rather than loudly.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.
Next.js SEO Guide for the App Router: Complete Checklist
A practical Next.js SEO checklist for the App Router: metadata baseline, canonical URLs, sitemap and robots files, social images, indexing control, and how to verify the rendered HTML.