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.

8 min read

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.tsxApp.tsx
// 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.

typescripttypescript
// 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.tsxApp.tsx
// 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.tsxApp.tsx
// 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.

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.

texttext
⚠ 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#metadatabase

The 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"/>.

Relative canonical links are ambiguous

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.tsxApp.tsx
// 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.

bashbash
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What happens if I never set metadataBase?

The build prints a warning and falls back to a base URL for social images, which is localhost during a local build. Canonical links stay relative instead of absolute, so the tag depends on whichever host served the page.

Should the canonical URL include query strings?

Usually not. Point filtered, sorted, and tracked variants at the clean path so search engines consolidate them, and only keep a parameter when it produces genuinely different content.

Does trailingSlash affect the canonical tag?

Yes. With the trailing slash option enabled, Next.js appends a slash to same-origin canonical and alternate URLs, so the tag matches the URL your site actually serves.

Can I set metadataBase per section instead of once?

You can set it in any layout, and it applies to that segment and everything below. Most sites set it once in the root layout because a second value is only useful when a section lives on another domain.

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.