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 metadata object, for routes whose title and description do not depend on data.
  • The generateMetadata function, for routes like app/blog/[slug]/page.tsx where 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 htmlLimitedBots yourself, 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.js is 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 images and videos for image and video sitemaps, and alternates for language versions.
  • For large sites, generateSitemaps splits 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

MistakeWhy it hurtsFix
Main content fetched in a Client Component after loadThe first HTML has a loading state, not your textFetch on the server in the page or layout
Metadata defined in a Client ComponentNot supported; the title never reaches the headExport metadata from a Server Component file
No metadataBaseRelative canonical and Open Graph URLs fail the buildSet it once in the root layout
A noindex removed later by JavaScriptGoogle says it may skip rendering a noindex page, so the change is not seenDecide indexing on the server
Stale sitemapThe sitemap route is cached by defaultMake 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.

Read next

Simon-Kucher pricing strategy, what the firm publishes about its own method and what a Singapore buyer should ask before briefing itThe firm publishes its approach, its scale and four named service lines, and publishes no fee, so the useful work before any call is reading what it claims and deciding which decision you are buying.International pricing strategies for a Singapore company, and the corridor that stops one market undercutting anotherA converted home price is the one answer that is almost always wrong, so set a price per market and keep the gaps narrower than the cost of moving a unit between them.Premium pricing strategy for a Singapore business, and the fence that makes the higher number holdCharging permanently more than the credible alternatives works when the buyer can name what the extra money buys, and the published Singapore writing ladder shows exactly how that is built.Red Antler publishes no price, so here is what its own site does publish and how to cost the same scope from SingaporeRead on 29 October 2026, three pages of redantler.com carry a complete deliverable list, three business stages and not one figure about what any of it costs.Hiring a full service content agency, the five stages a retainer has to cover and the two handoffs where the money leaksMost content retainers buy production and quietly leave strategy and distribution with you, so price the stages rather than the word count.Hiring a branding agency for a Singapore financial services firm, and the three registers to search before you fall in love with a nameIdentity work for a licensed firm starts with a clearance search rather than a mood board, and two of the three registers are free to read in a browser today.

Or browse the whole archive.

See what AI says about you.

The first scan is free, it needs no account, and it asks the questions your buyers ask.

Free. No card. We ask 5 real buyer questions on 2 AI apps.

Already sure you want this? Start free. No card needed, and the free plan stays free. Pay only when you want the work done. Or talk to a person if you have a question first. Or read how we fix what it finds.