Next.js SEO: the Metadata API, sitemap and robots files, and the streaming detail most checklists miss
Next.js renders pages on the server by default; titles, canonicals, the sitemap and when metadata streams are the parts you set.
Published by AI Knows Us · Updated 21 October 2026
Next.js SEO is mostly about using what the framework already gives you, correctly. In the App Router, pages and layouts are Server Components by default, so the HTML a crawler receives already holds your text. Your job is to set a unique title and description for every route with the Metadata API, set metadataBase and a canonical URL once in the root layout, add sitemap.ts and robots.ts files, and know when Next.js streams metadata after the first response. This page walks through each step using the Next.js documentation as read on 22 October 2026, when the npm registry listed Next.js 16.4.2 as the latest release.
What Next.js does for search by default
The Next.js docs on Server and Client Components say that, by default, layouts and pages are Server Components. They render on the server, and the result is sent as HTML. Client Components are not left out either: on the first load, the docs say Client Components and the RSC payload are used to prerender HTML. Only on later navigations inside the app are Client Components rendered entirely in the browser.
That matters because Google's own JavaScript SEO guide, last updated 4 March 2026, says server-side or pre-rendering "is still a great idea because it makes your website faster for users and crawlers, and not all bots can run JavaScript." Next.js gives you that by default. You can lose it by fetching your main content in the browser with useEffect, so keep content fetching on the server. For the wider picture of how Google renders script-heavy sites, see our guide to JavaScript SEO.
Next.js also adds two meta tags to every route even if you define nothing: the charset tag and the viewport tag. Everything else is yours to set.
Step 1: titles and descriptions with the Metadata API
There are two ways to set metadata, and both are exported from a layout.js or page.js file:
- The static
metadataobject, for routes whose title and description do not depend on data. - The
generateMetadatafunction, for routes likeapp/blog/[slug]/page.tsxwhere the title comes from a database or CMS.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { getPost } from '@/app/lib/data' export async function generateMetadata( { params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> { const { slug } = await params const post = await getPost(slug) return { title: post.title, description: post.description, alternates: { canonical: `/blog/${slug}` }, }
}
Two rules from the docs catch people out. First, metadata and generateMetadata are only supported in Server Components. The docs explain why: metadata must be resolved on the server before the page renders, so it can go into the initial HTML. If a page needs client-side features, keep page.tsx as a Server Component and move the interactive part into a separate Client Component file. Second, if the page and its metadata need the same data, wrap the fetch in React's cache function so it runs once, as the docs show.
Title templates
Set title.template in the root layout to add your brand to every page, for example '%s | Acme'. The docs list the rules: title.default is required when you add a template; a template applies to child segments, not the segment it is defined in; and title.absolute lets one page ignore the parent template. A template in a page.js has no effect, because a page has no children.
Step 2: set metadataBase and canonical URLs once
metadataBase sets the base URL for every metadata field that needs a full URL. The docs say it is typically set in the root app/layout.js, and that using a relative path in a URL field without it causes a build error. Once it is set, a canonical can be a short path:
// app/layout.tsx
export const metadata = { metadataBase: new URL('https://www.example.com'), title: { default: 'Acme', template: '%s | Acme' },
}
Then set alternates.canonical on each page, as in the Step 1 example, so every URL names itself. The docs show that a canonical of '/' with this base URL renders as a full link to https://acme.com, which is right for the home page and wrong for any other page. Use the exact host you want indexed. If your live site is on www, the base URL should be too. Google's guide says the best way to set the canonical is in the HTML, and that JavaScript should never change it to a different value from the one in the original HTML. Because Next.js writes metadata on the server, you meet that rule by default. Our page on how Google chooses a canonical URL covers what to do when Google picks a different one.
Step 3: understand streaming metadata
This is the Next.js-specific detail most SEO checklists miss. The docs say that if a page can be prerendered and generateMetadata does not add dynamic behaviour, the metadata is in the page's initial HTML. For dynamically rendered pages, Next.js can instead stream the metadata, injecting it into the HTML once generateMetadata resolves, so the visible content arrives first.
Streaming is turned off for bots that expect metadata in the <head>. Next.js detects them by user agent with the htmlLimitedBots setting. Its default list, per the docs, includes some Google crawlers (for example Mediapartners-Google, AdsBot-Google and Google-PageRenderer), Bingbot, Twitterbot and Slackbot. Two points follow:
- If you set
htmlLimitedBotsyourself, your value replaces the default list rather than adding to it. The docs call this advanced behaviour. - Prerendered pages never stream metadata, because it is resolved at build time. The most reliable way to give every crawler the title in the first bytes is to prerender the pages that matter for search.
Step 4: sitemap.ts and robots.ts
Next.js has file conventions for both. A sitemap.xml placed in the app folder is served as is. For anything that changes, write app/sitemap.ts and return an array of entries:
// app/sitemap.ts
import type { MetadataRoute } from 'next' export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const posts = await getAllPosts() return posts.map((p) => ({ url: `https://www.example.com/blog/${p.slug}`, lastModified: p.updatedAt, }))
}
Facts from the sitemap reference worth knowing:
sitemap.jsis a special Route Handler and is cached by default, unless it uses a request-time API or a dynamic config option. If new posts do not appear, this is usually why.- Entries can carry
imagesandvideosfor image and video sitemaps, andalternatesfor language versions. - For large sites,
generateSitemapssplits the sitemap into several files served at paths like/product/sitemap/1.xml.
app/robots.ts returns a rules object and the sitemap location:
// app/robots.ts
import type { MetadataRoute } from 'next' export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: '*', allow: '/', disallow: '/private/' }, sitemap: 'https://www.example.com/sitemap.xml', }
}
You can pass an array to rules to give named crawlers different rules. Remember Google's warning: a page blocked in robots.txt is never fetched, so Google cannot render it or read any tag on it.
Step 5: Open Graph images and the rest of the head
An opengraph-image.jpg in the app folder sets a site-wide share image; one in a route folder overrides it for that route. An opengraph-image.tsx file can build one per post with ImageResponse from next/og. JSON-LD structured data is rendered as a script tag in the page itself, and you can check it with a schema markup validator.
Mistakes that cost Next.js sites their rankings
| Mistake | Why it hurts | Fix |
|---|---|---|
| Main content fetched in a Client Component after load | The first HTML has a loading state, not your text | Fetch on the server in the page or layout |
| Metadata defined in a Client Component | Not supported; the title never reaches the head | Export metadata from a Server Component file |
No metadataBase | Relative canonical and Open Graph URLs fail the build | Set it once in the root layout |
A noindex removed later by JavaScript | Google says it may skip rendering a noindex page, so the change is not seen | Decide indexing on the server |
| Stale sitemap | The sitemap route is cached by default | Make it dynamic or revalidate it |
If your project uses the older Pages Router, the ideas are the same but the tools differ. For the React side in general, our React SEO guide sets out what React's own docs now recommend.
Questions about Next.js SEO
Is Next.js good for SEO?
Yes. Pages and layouts are Server Components by default, so crawlers get rendered HTML, and the Metadata API, sitemap and robots conventions cover the technical basics. You still have to write a unique title and description for every route.
What is the difference between metadata and generateMetadata?
The metadata object is static. generateMetadata is an async function that can read route parameters and fetch data, so it suits dynamic routes such as blog posts and product pages.
Does Next.js create a sitemap automatically?
No. You add app/sitemap.xml or app/sitemap.ts. The code version can list every post or product from your data source, and generateSitemaps splits very large sites into several files.
Does streaming metadata hurt SEO?
Next.js turns streaming off for the bots in its htmlLimitedBots list, and prerendered pages never stream. For pages that matter most for search, prerendering keeps the title in the first response for every reader.
A clean Next.js head is half of being quoted
Server-rendered HTML with a clear title is also what an AI assistant finds easiest to read when it fetches your page to answer a question. Whether it then names your product when a buyer asks for a recommendation is a separate result, and Search Console does not report it. That newer measurement, which assistants name you and from which sources, is the category AI Knows Us works in.