Next.js SEO Done Right: Metadata, Sitemaps and Structured Data
Next.js gives you everything you need for solid technical SEO, but only if it is wired up properly. Here is how I handle metadata, canonicals, sitemaps and structured data on every build.

Next.js has a reputation for being good for SEO. That is half true. The framework gives you server rendering, a proper metadata system and file-based sitemaps, but none of it happens by itself. I regularly audit Next.js sites with missing canonicals, identical titles on every page and Open Graph images pointing at localhost. Here is the setup I use on every build, and the mistakes I check for first.
Start with rendering, not tags
Before any meta tag, make sure crawlers can see your content. With the App Router, pages are server components by default, so the HTML that arrives already contains the text. That is the single biggest SEO advantage Next.js has over a client-rendered single-page app. It is lost the moment someone fetches content inside a useEffect in a client component: the HTML ships empty and the words appear only after JavaScript runs.
A quick test: view the page source (not the element inspector) and search for a sentence from the body copy. If it is not there, fix that before anything else. For the background on why server rendering matters, see React Server Components explained without the jargon.
The Metadata API: static and dynamic
For pages whose metadata never changes, export a metadata object. For pages driven by a CMS or database — blog posts, products, locations — export an async generateMetadata function that fetches the record and returns the tags.
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.metaTitle,
description: post.metaDescription,
alternates: { canonical: "/" + post.slug },
openGraph: { type: "article", images: [post.image] },
};
}
Next.js de-duplicates identical fetch requests during a render, so the page and its metadata can ask for the same post without hitting your CMS twice. If you use a database client rather than fetch, wrap the query in React's cache function to get the same effect.
In the root layout, set a title template such as "%s | Your Brand" so every page gets a consistent suffix without anyone typing it by hand.
metadataBase and canonical URLs
Set metadataBase once in the root layout to your production domain. Without it, relative URLs in canonicals and Open Graph images have nothing reliable to resolve against, and Next.js falls back to a default with a warning in the build log. That is how preview or localhost URLs end up in shared links.
Then give every indexable page a canonical through alternates.canonical. Canonicals tell Google which version of a URL is the real one when the same content is reachable through query strings, trailing slashes or tracking parameters. For filtered listings, point the canonical at the clean URL unless the filtered view is genuinely worth ranking on its own. Multi-language sites also need alternates.languages for hreflang; I cover that properly in the multilingual SEO guide.
Open Graph images that do not break
Every page that might be shared should have an Open Graph image, ideally 1200 by 630 pixels. You can set one through the openGraph.images field, or add an opengraph-image file to a route folder — either a static image or an opengraph-image.tsx that generates one with ImageResponse from next/og. Generated images are ideal for blogs: title, author and brand colours, rendered automatically for every post.
Check the output with a social sharing debugger before launch. A broken OG image is invisible until a client shares a link on LinkedIn and gets a grey box.
sitemap.ts and robots.ts
An app/sitemap.ts file that returns an array of URLs generates /sitemap.xml for you. Pull the URLs from the same source as your pages — the CMS or database — so new content appears automatically and deleted content disappears. Use lastModified dates that reflect real edits, not the build time; a sitemap where every page changed today teaches Google to ignore the field. Very large sites can split into several sitemaps with generateSitemaps.
Alongside it, app/robots.ts generates robots.txt and should point to the sitemap. The classic mistake is shipping a staging rule that disallows everything to production. Make the rules depend on an environment variable, and check them as part of your launch checklist.
JSON-LD structured data
Structured data tells search engines what a page is: an article with an author and a date, a business with an address, a breadcrumb trail. In Next.js the recommended approach is to render a JSON-LD script tag from the page component itself:
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
datePublished: post.createdAt,
author: { "@type": "Person", name: "Danish Sohail" },
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
On most business sites I add three types: Organization (or LocalBusiness) once in the root layout, Article on blog posts, and BreadcrumbList on anything more than one level deep. If values come from editors, escape < characters before injecting them so a stray tag in the CMS cannot break out of the script. Only mark up what is visible on the page, and validate it with Google's Rich Results Test. Structured data describes your content; it does not replace it.
Pagination and indexing problems
Paginated blog and product listings cause more quiet indexing issues than anything else I see. A few rules:
- Use real URLs for each page (
/blogs?page=2or/blogs/page/2), rendered on the server with plain links. Infinite scroll with no crawlable fallback hides everything past the first batch. - Give each paginated page a self-referencing canonical. Pointing page 2 at page 1 tells Google to ignore the posts that only appear on page 2.
- Keep thin, near-duplicate pages — empty tag archives, internal search results — out of the index with
robots: { index: false }in their metadata.
Internal linking does as much work as pagination here. An SEO-friendly site structure means important pages are never more than a few clicks from the homepage.
Common mistakes I find in audits
- No
metadataBase. Relative OG and canonical URLs resolve to the wrong host. - Duplicate titles. A layout sets a title and no page overrides it, so every page shares the same one. Search Console will happily list them for you.
- Metadata in client components. The
metadataexport only works in server components. Move it to the page or layout file. - No trailing-slash or www redirects. Pick one version of each URL and redirect the rest to it.
- Slow pages. Speed feeds into rankings and decides whether visitors stay. Heavy hero images are the usual culprit; see getting LCP under 2.5 seconds with next/image and the wider Core Web Vitals guide.
Where it fits
None of this is exotic. It is a few files and a handful of conventions, set up once in the root layout and repeated per route. If you are starting a new project, it slots naturally into the structure I describe in the App Router for business websites. Get it right at the start and it costs almost nothing. Retrofit it after launch and you are also cleaning up whatever Google indexed in the meantime.
Think your Next.js site is leaving search traffic on the table? Send me the URL and I will tell you what I would fix first.




