Search engines read your rendered HTML, so Next.js SEO in the App Router comes down to which tags end up in that HTML and which status code the route returns. This checklist covers the metadata baseline, canonical URLs, crawler files, social images, indexing rules, and verification. Examples were checked against Next.js 16.3 with the App Router and the default caching model, meaning the Cache Components flag is off.
Every item below is built into the framework as a file convention or an export. Next.js SEO work rarely needs a third-party package on top of that.
Set the metadata baseline in the root layout
The root layout is the only place where a metadata value applies to every route, so it holds the site-wide defaults. Three of them matter more than the rest: the base URL used to expand relative links, the title template, and the fallback description.
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://acme.com'),
title: { default: 'Acme Store', template: '%s | Acme Store' },
description: 'Tools for small workshops.',
}A page that exports a title of "About" now renders <title>About | Acme Store</title>, and a page that exports no title at all falls back to "Acme Store". The template only applies to child segments, which is why the default is required next to it.
Setting the base URL early prevents a class of bug that is easy to miss. Without it, relative social image paths resolve against http://localhost:3000 during the build, and canonical URLs stay relative instead of becoming absolute.
Give every indexable route a canonical URL
A canonical URL tells search engines which address is the real one when the same content is reachable through several paths, such as query strings, pagination, or a preview domain. Next.js writes it from the alternates field.
// app/about/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'About',
alternates: { canonical: '/about' },
}The rendered head contains <link rel="canonical" href="https://acme.com/about"/> because the relative path is composed with the base URL from the root layout. For a route with a dynamic segment, build the same value inside generateMetadata from the resolved params.
Skipping the canonical tag is not fatal, since Google will pick a canonical itself. It becomes a real problem when the same page is served from a staging domain, a trailing-slash variant, and a tracking-parameter URL at the same time.
Ship a sitemap and a robots file
These two files are the oldest part of any Next.js SEO setup, and the App Router models both as file conventions in the app directory. Each can be a static file or a function that returns data. The generated versions are the useful ones because they stay in sync with your content.
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: 'https://acme.com', lastModified: new Date(), priority: 1 },
{ url: 'https://acme.com/about', lastModified: new Date() },
]
}That file is served at /sitemap.xml as valid sitemap XML, with each entry rendered as a loc element plus whatever optional fields you supplied. In a real project the array comes from your database or content source, and large sites split it into numbered sitemaps.
The robots file is the companion piece. It tells crawlers what to skip and where the sitemap lives.
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/', disallow: '/dashboard/' },
sitemap: 'https://acme.com/sitemap.xml',
}
}Requesting /robots.txt returns the plain-text form of that object, ending with the sitemap line. Both files are Route Handlers under the hood and are cached by default unless they read request-time data.
Add social images before you need them
A link shared without an Open Graph image gets a bare text card, and that is usually noticed on the day of a launch rather than during development. The lowest-effort fix is a file named opengraph-image.png in the app directory, which Next.js turns into the correct tags for every route beneath it.
Placing the same file name deeper in the tree overrides it for that branch, so a blog folder can carry its own card while the rest of the site keeps the default. Next.js reads the file at build time and emits the image URL, type, width, and height for you.
When the image needs to include the page title or other per-route data, generate it with code instead, which the article on dynamic OG images covers. Either way, keep one image around 1200 by 630 pixels so the large summary card renders without cropping.
Control what gets indexed
Two different mechanisms exist and they answer different questions. The robots file controls crawling for the whole site, while the robots metadata field controls indexing for one route.
// app/preview/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Preview',
robots: { index: false, follow: false },
}This renders <meta name="robots" content="noindex, nofollow"/> on that route only. Use it for search result pages, thank-you pages, and anything gated, and remember that a page blocked in the robots file cannot be crawled, so a noindex tag on it is never read.
Preview deployments deserve their own rule. Serving a noindex tag across an entire non-production environment is safer than trusting that nobody links to it, and it costs one conditional in the root layout metadata.
The two mechanisms also fail differently. A wrong disallow rule quietly removes a section from crawling, while a wrong noindex tag removes pages that are already ranking, so review both before a release rather than after.
Make sure the crawler receives real content
Metadata is only half of the story. The body of the response has to contain the content you want ranked, and the response has to carry an honest status code.
Three checks catch most problems here:
- A missing resource should call the not-found helper. Next.js renders the not-found UI and injects a noindex tag, and it sends a real 404 status as long as nothing has streamed yet.
- Content that only appears after a client-side fetch is invisible to crawlers that do not execute JavaScript, so fetch it in the Server Component instead.
- Redirects should use the permanent variant when the move is permanent, so link equity follows the new URL.
Streaming metadata is worth knowing about here. For a dynamically rendered route, Next.js sends the UI first and appends the metadata tags to the body once they resolve, while bots that cannot run JavaScript get blocking metadata in the head. That behavior is automatic and needs no configuration.
Verify in the response, not in your editor
The metadata object you wrote is an input. What matters is the merged output after every parent segment has contributed, and the only reliable way to see it is to read the HTML that leaves the server.
npx next build && npx next start
curl -s http://localhost:3000/about | grep -o '<link rel="canonical"[^>]*>'Running this against a production build shows the resolved canonical tag with the absolute URL. Do the same for the title, the Open Graph tags, and the robots tag on a handful of representative routes, including one dynamic route.
Two external tools finish the job. The Rich Results Test validates structured data, and a social card debugger from the platform you care about shows how the Open Graph tags are actually interpreted. Both belong in the release routine for any Next.js SEO change, because a tag that resolves locally can still break once it is deployed under the real domain.
Common mistakes
The failures below account for most broken Next.js SEO in the App Router, and each one is silent in development.
- Setting metadata in a page that is a Client Component. The export is disallowed there and the build fails, which is the friendly outcome compared to shipping no tags.
- Setting the Open Graph object in a child segment and losing the parent site name and image, because nested metadata objects are replaced rather than deep merged.
- Leaving the base URL unset and shipping social image URLs that point at localhost.
- Adding a canonical tag that points at the same page for every route, which is worse than having none.
- Blocking a path in the robots file and also adding a noindex tag to it, so the tag is never seen.
Rune AI
Key Insights
- Set metadataBase, a title template, and a description once in the root layout.
- Give every indexable route an explicit canonical URL.
- Ship a sitemap and a robots file with the app directory file conventions.
- Keep preview environments and thin routes out of the index with the robots metadata field.
- Confirm the tags in the response HTML, not in your metadata object.
Frequently Asked Questions
Do I need a third-party SEO package in the App Router?
Does Next.js handle SEO automatically?
Is a static page better for SEO than a dynamic one?
Why is my title tag at the bottom of the HTML?
Conclusion
SEO in the App Router is a handful of small, explicit decisions: a metadata baseline in the root layout, one canonical URL per route, a sitemap and robots file, real social images, deliberate noindex rules, and correct status codes. Verify each one in the rendered HTML rather than in the source object.
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.
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.
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.