NEXT.JS SEOMetadata the App Router wayNext.js apps often launch with the same SEO gaps: every page shares one title, canonical tags are missing, the sitemap is a static file nobody updates, and nobody has checked what crawlers can reach. The App Router has a built-in convention for each of these, so none of it needs a plugin or a hand-written head tag.
Where should metadata live?
Metadata belongs in layout and page files: a static metadata object when it never changes, or a generateMetadata function when it depends on data. Both are only supported in Server Components, and you cannot export both from the same route segment. Next.js turns them into the right tags in the <head>.
Set the defaults once in the root layout: metadataBase with your production URL, and a title object with a template such as %s | Brand and a default, which the template requires. The template applies to child segments, so each page only sets its own short title.
How do you set titles and canonicals for dynamic pages?
Use generateMetadata and read the route params. In current versions params is a promise, so you await it. fetch requests are memoised across generateMetadata and the page, so loading the same record in both does not double the requests.
// app/services/[slug]/page.tsx
import type { Metadata } from 'next'
import { getService } from '@/lib/services'
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const service = await getService(slug)
return {
title: service.seoTitle,
description: service.summary,
alternates: { canonical: `/services/${slug}` },
}
}Because metadataBase is set in the root layout, the relative canonical resolves to a full URL. Without it, a relative path in a field that needs an absolute URL causes a build error, which is a useful safety net.
What catches people out with metadata merging?
Metadata from each segment is merged shallowly, from the root layout down to the page. If a page sets openGraph, it replaces the whole openGraph object from the layout, not just the title, so a shared image or description quietly disappears. Keep shared fields in a small module and spread them into each page's object.
Streaming is the other surprise. Since version 15.2, metadata from generateMetadata can be streamed after the initial UI is sent. Next.js says it has verified that bots which run JavaScript, such as Googlebot, read streamed metadata correctly, while for HTML-limited bots like facebookexternalhit metadata still blocks rendering and lands in the <head>. The htmlLimitedBots option changes that list; the default suits most sites.
How do sitemaps and robots.txt work?
app/sitemap.tsexports a function returning your URLs, with optionallastModified,changeFrequencyandpriority, typed asMetadataRoute.Sitemap. Build it from the same data that creates your pages, so new pages appear without anyone editing a file.- Large sites can split the sitemap with
generateSitemaps, or by placing a sitemap file inside a route segment. app/robots.tsreturns crawl rules and the sitemap URL, typed asMetadataRoute.Robots. Both files are cached by default unless they use request-time APIs or dynamic config.- For pages that should stay out of search, such as thank-you pages or internal search results, use the
robotsmetadata field to outputnoindex. Do not also block them in robots.txt: Google cannot see anoindexon a page it is not allowed to crawl.
How do you make shared links look right?
Add an opengraph-image to a route segment. It can be a plain image file, or an opengraph-image.tsx that returns an ImageResponse from next/og, so each article or service page gets a branded card built from its own title. Generated images are created at build time and cached unless they use request-time APIs or uncached data. Image files have size limits, 8MB for Open Graph and 5MB for Twitter images, and the build fails if a file is larger. File-based metadata takes priority over the metadata object.
What should you check before launch?
- View the page source on three different page types and confirm each has its own title, description and canonical.
- Open
/sitemap.xmland/robots.txton production and check every URL uses the live domain. - Share a page in a messaging app and check the image, title and description.
- Confirm that staging returns
noindexand production does not.
We run this list on every Next.js build, including our own SaaS, AgencyPilotHQ.
Where to start
Run the checklist above on your live app; most gaps are quick to fix once you know where the convention lives. If you want SEO foundations in place from the first commit, see our web app development service, or read about the boring stack behind our SaaS.
FAQ
Frequently asked questions
Does Next.js handle SEO automatically?
It gives you the tools, not the content. Next.js renders your metadata into the head and always adds charset and viewport tags, but titles, descriptions, canonicals, the sitemap and robots rules are yours to define. A page without its own title uses the closest parent's resolved title, which is how duplicate titles happen.
Can a Client Component set page metadata?
No. The metadata object and generateMetadata only work in Server Components, because metadata is resolved on the server before the page renders. Keep the page file as a Server Component that exports metadata, and move the interactive parts into a separate Client Component that the page imports.
Should we use a static sitemap.xml or sitemap.ts?
A static sitemap.xml in the app folder is fine for a small site that rarely changes. For anything driven by a CMS or database, sitemap.ts is better, because it is generated from the same data as the pages, so new and removed pages show up without anyone editing the file by hand.
How do we handle hreflang in the App Router?
Use alternates.languages in your metadata, which outputs rel="alternate" links with hreflang for each language version. Sitemap entries accept the same alternates.languages field for localised sitemaps. Generate both from the locale data your routing already uses, so the head tags and the sitemap never disagree.
Do we need a separate image file for every page's social preview?
No. An opengraph-image.tsx in a dynamic route segment receives the route params, so one file can generate a card for every article or service from its own title. You only need static image files for pages where a designer wants a specific, hand-made image.
