Headless & API
Next.js SEO: metadata, rendering, sitemaps, and structured data
Next.js SEO is the work of making a site built with Next.js, the React framework, easy for search engines to crawl, render and rank. It covers choosing a rendering mode that puts content into the HTML the server sends, and using the framework's tools for titles, canonicals, sitemaps, robots rules, structured data and status codes.
Last reviewed October 12, 2026 · 8 min read
On this page
Next.js is a good foundation for SEO because it can send finished HTML, which a plain React app does not. Its own SEO course puts the requirement in one line: "The most important thing for SEO is that page data and metadata is available on page load without JavaScript." Most Next.js SEO problems come from routes that miss that bar, or from head tags, sitemaps and status codes nobody wired up: the framework gives you the tools and fills in nothing about your content.
Why Next.js does better than a plain React app
A React single-page app sends the browser a nearly empty HTML file and builds the page with JavaScript. Google can index that, but in two passes. Its documentation says "Googlebot queues all pages with a 200 HTTP status code for rendering," and a page "may stay on this queue for a few seconds, but it can take longer than that." Until rendering runs, the content and its links are invisible to Google, and "not all bots can run JavaScript," so many other crawlers only ever see the empty shell.
Next.js removes the wait by rendering on the server, at build time or per request, so the first response already holds the text, links and meta tags. Its SEO course is blunt about the alternative: "in general Client-Side Rendering is not recommended for optimal SEO." The underlying distinction is the one in static vs dynamic websites: what matters is where and when the HTML gets made.
Step 1: Pick a rendering mode for each route
Every route can render differently, which is the biggest SEO lever Next.js gives you.
| Route | Rendering | Why |
|---|---|---|
| Blog posts, docs, landing pages | Static, with incremental static regeneration (ISR) | Full HTML from a CDN, refreshed after an interval or when content changes |
| Blog index, category and tag archives | Static with a short revalidation interval | New posts appear without a full rebuild |
| Search results, filtered listings | Rendered per request, usually noindexed | The content depends on the query |
| Account pages, dashboards | Client-side is fine | Nothing there should be indexed |
For a blog, the usual pattern is generateStaticParams to prerender the known slugs plus a revalidate interval, or on-demand revalidation when the CMS reports an edit. Content a component fetches in the browser after load is not in the server HTML, so fetch indexable content on the server. The Next.js CMS comparison covers which content sources fit this pattern.
Step 2: Set titles, descriptions and canonicals with the Metadata API
In the App Router, a route exports a static metadata object or a generateMetadata function, and Next.js writes the head tags. Both work only in Server Components. For posts, generateMetadata reads the same data the page renders:
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug); // wrap in React's cache() so the page reuses it
return {
title: post.metaTitle,
description: post.metaDescription,
alternates: { canonical: `/blog/${slug}` },
openGraph: { title: post.og.title, images: [post.og.image] },
};
}
Three details decide whether this works for search:
- Set
metadataBaseonce in the root layout. It turns relative paths, like the canonical above, into absolute URLs on your domain. Without it, a relative path in a URL-based metadata field "will cause a build error." - Use a title template.
title: { template: '%s | Brand', default: 'Brand' }in the root layout adds the brand to every child page's title, andtitle.absolutelets one page opt out. - Repeat nested fields. Metadata from layouts and pages is "shallowly merged," so "metadata with nested fields such as openGraph and robots that are defined in an earlier segment are overwritten by the last segment to define them." A post that sets its own
openGraphloses the site-wide Open Graph defaults unless it repeats them.
Rendering mode matters here too. A prerendered page has its metadata in the initial HTML. On a dynamically rendered page, Next.js can stream the tags into the body once generateMetadata resolves, which the docs say works for "bots that execute JavaScript and inspect the full DOM (e.g. Googlebot)"; bots that cannot run JavaScript still get them in the head. Static and ISR routes avoid the question.
Step 3: Generate the sitemap and robots.txt from code
Next.js serves sitemap.xml and robots.txt from special files in the app directory. For a blog, use app/sitemap.ts: it exports a function that returns an array of URLs, so it can read the post list from your CMS and fill lastModified from each post's real update date instead of the build time. The file is cached by default, so give it the same revalidation as the blog itself. The Next.js docs note that "Google's limit is 50,000 URLs per sitemap"; past that, generateSitemaps splits the output into several files.
// app/sitemap.ts
export default async function sitemap() {
const posts = await getAllPosts();
return posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
}));
}
app/robots.ts returns the crawl rules and the sitemap URL. Keep it short and block only what must not be crawled. If you also publish an llms.txt file, a route handler at app/llms.txt/route.ts can build it from the same post list.
Step 4: Add structured data as JSON-LD
Next.js has no structured-data API. Its docs recommend rendering JSON-LD as a script tag of type application/ld+json inside the page or layout component, with the JSON.stringify output escaped by replacing < with \u003c, so content in the data cannot break out of the tag. A native script element is right here, not next/script, because JSON-LD is data rather than code. For a blog post, emit a BlogPosting node with headline, dates, image and author, add FAQPage where the post has a FAQ, and check the result with Google's Rich Results Test.
Step 5: Return real 404s and permanent redirects
Google asks for "a meaningful status code, like a 404 for a page that could not be found." In Next.js, call notFound() when a slug has no post. It renders the 404 page and also injects a noindex tag. The catch is timing: if the response has already started streaming from a Suspense boundary when notFound() runs, it "keeps its 200 status" and only the noindex tag remains. Check that the post exists before any suspended content renders, or prerender the known slugs with dynamicParams set to false so unknown ones 404 before rendering.
For changed slugs, add entries to redirects in next.config.js with permanent: true, which sends a 308 status. A site built with output: 'export' cannot use them, because static exports drop redirects along with ISR; configure redirects at the host instead.
Step 6: Get images and links right
next/image makes alt a required prop. Describe what the image shows, and use an empty alt="" only for decoration. Pass width and height so the layout does not shift. For navigation, next/link renders a normal anchor with an href, which is what Google follows: its crawler "parses the response for other URLs in the href attribute of HTML links." A button that navigates on click is not a link to a crawler.
A Next.js SEO checklist
- Every indexable route renders its content and links on the server.
- The root layout sets
metadataBaseand a title template; every post sets title, description, canonical and Open Graph. app/sitemap.tslists only indexable, canonical URLs with real dates, andapp/robots.tspoints to it.- Posts emit escaped BlogPosting JSON-LD that passes the Rich Results Test.
- Unknown slugs return a 404 status, and changed slugs redirect permanently.
- Images have alt text and dimensions; navigation uses real anchors.
- View the source of one page per template: if the text is missing, Google has to render it first.
The wider checklist for any API-fed front end is in headless CMS SEO, and headless WordPress adds the steps for a WordPress backend.
How Structura handles this
Structura can be the content source for a Next.js blog. A headless space serves published posts from a public, CDN-cached Content API, and each post carries the fields generateMetadata needs: metaTitle, metaDescription and an og object with title, description and image. Images come with width, height and alt text for next/image. The html body is sanitized, with internal links already pointing at your public domain, and a schema array holds JSON-LD: a BlogPosting node with the author as a Person, plus FAQPage and HowTo entries when the post has those sections. publishedAt and updatedAt on every post serve as your sitemap dates and your revalidation signal, and a terms endpoint feeds category and tag routes. The developer docs include an App Router example with a 300-second revalidation.
The steps above stay with your code: rendering mode, head tags including the canonical, sitemap and robots files, 404s and redirects. Structura does not trigger your build or revalidate your routes. Poll the list endpoint on your revalidation interval or, on paid plans, let article delivery send each published post to an HTTPS endpoint of yours that revalidates the affected pages. If you build your own Article node, drop our BlogPosting entry so each page reports one article. The API-first CMS page shows the endpoints and response fields.
FAQ
Which is better for SEO, React or Next.js?
Next.js, for pages you want found. React is a UI library, and a React app built as a client-rendered single-page app makes Google render JavaScript before it sees the content, while some crawlers never render it at all. Next.js renders the same React components on the server and adds metadata, sitemap and robots tooling that plain React leaves to you.
Is React still bad for SEO?
Client-side React is a risk for indexable pages, not a ban. Google renders JavaScript after a queue, and other bots may not render it. For an app behind a login it does not matter. For a blog, documentation or landing pages, render on the server or at build time through a framework such as Next.js.
Does Next.js generate a sitemap automatically?
No. You add a static sitemap.xml file to the app directory or, better for a blog, a sitemap.ts file that builds the list from your content, and Next.js serves the result at /sitemap.xml. The robots.txt file works the same way through robots.ts.