Adios
BlogNext.js SEO

Next.js SEO

How to Configure Next.js SEO for Production: Rendering, Metadata, and Indexing

Configure Next.js SEO with server rendering, metadata, canonical URLs, sitemaps, JSON-LD, Core Web Vitals, and production release checks.

Adios teamUpdated July 17, 202613 min read

Production Next.js SEO is a route contract: each public URL needs a useful answer, server-rendered content, consistent metadata, a clear indexability rule, and a release check that proves those signals reached production.

Give every indexable route one job

Next.js supplies rendering and metadata APIs; it does not decide which pages deserve search traffic. Start by naming the reader, query, answer, canonical URL, and next action for every public route. That decision separates a useful page from a technically indexable duplicate.

Map one durable URL to each distinct intent. A product overview, implementation guide, pricing page, and API reference can discuss the same product because they solve different jobs. Two pages that repeat the same answer with different headings create a weaker choice for readers and another page for the team to maintain.

Record the decision in a route inventory before writing metadata. Include the route pattern, data source, rendering mode, canonical rule, indexability, and freshness trigger. The inventory becomes the shared contract for content, engineering, and release checks.

  • Index pages that provide a complete, standalone answer.
  • Keep private, empty, internal-search, preview, and low-value filter pages out of the index.
  • Link to the canonical URL internally instead of relying on a canonical tag to repair inconsistent navigation.
  • Assign content updates to real events such as a product change, editorial revision, or inventory update.

Put the primary answer in the first HTML response

Pages and layouts in the App Router are Server Components by default. Use that default for the title, introduction, body copy, product facts, breadcrumbs, and contextual links. The initial response then contains the page's main answer without waiting for a browser-side request.

A Client Component is appropriate when the feature needs state, an event handler, a lifecycle hook, or a browser API. Keep that boundary close to the interaction. A pricing calculator can be interactive without turning the surrounding product page into a client-rendered shell.

Rendering is also a freshness decision. Static generation fits content that changes with a release. Server rendering fits request-specific public data. Cached rendering fits shared data with a defined invalidation event. Choose the narrowest model that keeps the visible answer correct; do not make an entire route dynamic to support one small control.

Build a metadata hierarchy that survives real routes

Set metadataBase, a title template, the default description, and shared social fields in the root layout. Override values at the narrowest layout or page that owns them. Static routes can export metadata; data-backed routes can use generateMetadata. Both APIs are server-only.

Treat nested metadata as complete objects at the route that overrides them. Next.js merges metadata from the root toward the page, but nested fields are replaced rather than deeply merged. A page-level openGraph object can accidentally discard an inherited image if it only supplies a new title.

Current Next.js releases can stream metadata for bots that execute JavaScript while keeping metadata blocking for HTML-limited bots. That behavior improves response timing, but it does not make slow or unreliable metadata queries harmless. Reuse the page's resolved data, handle missing entities explicitly, and test both successful and missing routes.

app/layout.tsx and app/products/[slug]/page.tsx

// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  metadataBase: new URL("https://www.example.com"),
  title: { default: "Acme", template: "%s | Acme" },
  description: "Tools for production engineering teams.",
  openGraph: {
    siteName: "Acme",
    images: [{ url: "/og-default.png", width: 1200, height: 630 }],
  },
};

// app/products/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params;
  const product = await getProduct(slug);

  if (!product) return { title: "Product not found", robots: { index: false } };

  return {
    title: product.name,
    description: product.summary,
    alternates: { canonical: "/products/" + product.slug },
    openGraph: {
      title: product.name,
      description: product.summary,
      images: product.images.map((image) => ({ url: image.url })),
    },
  };
}

Treat canonicals, redirects, and robots as one URL policy

A canonical URL names the preferred version of duplicate or substantially similar content. Make that preference consistent across redirects, canonical annotations, internal links, and sitemap entries. Google describes redirects and rel=canonical annotations as strong signals and sitemap inclusion as a weaker signal; aligned signals are easier to interpret than a corrective tag surrounded by contradictions.

Define the approved protocol, hostname, trailing-slash rule, case policy, and parameter behavior once. Redirect host and path variants that should never remain accessible. Use a self-referencing canonical on the preferred page when it helps keep generated URLs consistent.

Use robots metadata to control whether a reachable page can be indexed and robots.txt to guide crawling. Neither protects private content. Authorization must reject an unauthenticated request regardless of which crawler headers it sends. Do not ship noindex in the first response and try to remove it in the browser: Google may stop before rendering the JavaScript change.

A page-level canonical and indexability rule

import type { Metadata } from "next";

export const metadata: Metadata = {
  alternates: { canonical: "/guides/nextjs-seo" },
  robots: { index: true, follow: true },
};

Generate crawler files from published content

Generate sitemap.ts and robots.ts from the same source that defines public content. A sitemap should contain absolute, canonical URLs that you want search engines to discover. Exclude account routes, previews, internal search results, redirected paths, and parameter combinations that do not provide standalone value.

Use lastModified only when it represents a significant update to the page's main content, structured data, or links. Google ignores sitemap priority and changefreq values, so those fields do not rescue a stale or noisy URL list. For a large catalog, split sitemaps by content family when that makes generation, monitoring, or ownership clearer.

Treat robots.txt as crawl guidance rather than a hiding mechanism. A disallowed URL may still be known through external links, and blocking a page can prevent a crawler from seeing its noindex rule. Remove sensitive routes from public discovery and enforce access at the application boundary.

  • Fetch the production sitemap and confirm it returns XML with a successful status.
  • Sample URLs from every content family and verify that each resolves without a redirect.
  • Compare the sitemap count with the published, canonical records in the content source.
  • Report generated URLs that are noindex, missing, redirected, or canonicalized elsewhere.

app/sitemap.ts

import type { MetadataRoute } from "next";

const siteUrl = "https://www.example.com";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getPublishedPosts();

  return posts
    .filter((post) => post.indexable)
    .map((post) => ({
      url: new URL("/blog/" + post.slug, siteUrl).toString(),
      lastModified: post.updatedAt,
    }));
}

Describe visible entities with accurate JSON-LD

Structured data names the entities that a page already presents. Choose the type from the page's actual purpose: Article for an editorial post, Product for a purchasable item, BreadcrumbList for hierarchy, or SoftwareApplication for a real software product. Adding a type does not change what the page is.

Include the required properties for the search feature you are targeting and keep values aligned with visible content. Do not invent reviews, ratings, prices, availability, authors, or update dates. Complete and accurate markup is more useful than a large object assembled from fields the page cannot support.

Render JSON-LD on the server and sanitize the serialized value when it contains data you do not fully control. Validate the deployed URL with a structured-data tool, then inspect the script yourself. A passing parser confirms syntax; it does not prove that the entity is honest, current, or eligible for a rich result.

A server-rendered Article script

const article = {
  "@context": "https://schema.org",
  "@type": "Article",
  headline: post.title,
  description: post.excerpt,
  datePublished: post.publishedAt,
  dateModified: post.updatedAt,
  mainEntityOfPage: new URL("/blog/" + post.slug, siteUrl).toString(),
  author: { "@type": "Organization", name: "Acme" },
};

const json = JSON.stringify(article).replaceAll("<", "\u003c");

return <script type="application/ld+json">{json}</script>;

Protect page experience at the component boundary

SEO work fails when the result is technically indexable but frustrating to use. Keep the client boundary narrow, reserve image space, load only the font files the design uses, and place slow secondary regions behind meaningful fallbacks. The first screen should contain the primary answer instead of an empty shell or a layout that shifts as assets arrive.

Measure representative production routes rather than one polished homepage. Largest Contentful Paint measures loading, Interaction to Next Paint measures responsiveness, and Cumulative Layout Shift measures visual stability. Evaluate the 75th percentile of real visits where field data is available, then use lab tests to reproduce a specific regression.

When a route gets slower, inspect the cause before removing useful content. Common sources include a widened Client Component boundary, a blocking data query, an unbounded third-party script, an oversized hero image, or a font configuration that delays text. The fix should preserve the page's job.

  • Measure at least an article, a listing page, and a data-backed detail page.
  • Record LCP, INP, and CLS separately instead of collapsing them into one score.
  • Compare direct visits with client-side navigation between routes.
  • Retest after a deployment because CDN, runtime, and third-party behavior affect the production result.

Run a 15-minute Next.js SEO release check

SEO configuration is not finished when the code looks correct. Build the production application, start the same runtime you plan to deploy, and verify the candidate URL before it receives the canonical domain. A metadata query can break a build, a redirect can remove a section, and a route can return a styled not-found page with the wrong status.

Test one known page, one redirect, one missing page, and one representative dynamic route. Inspect the first HTML response for the title, description, canonical, robots rule, H1, opening answer, structured data, and contextual links. Fetch robots.txt and sitemap.xml separately, then compare their URLs with the route inventory.

Record the checks beside the release instead of relying on memory. If an SEO-critical assertion fails, keep the current healthy version live, fix the candidate, and run the same checks again. The useful outcome is not a perfect checklist document; it is a production route whose response matches the policy you approved.

  • Confirm the primary query and reader outcome still match the visible page.
  • Confirm the main answer and crawlable links exist in the first HTML response.
  • Confirm one title, description, canonical, robots rule, and H1 describe the same page.
  • Confirm structured data matches visible facts and parses on the deployed URL.
  • Confirm sitemap URLs are canonical, indexable, successful, and meaningfully dated.
  • Confirm representative routes meet the team's production performance budget.

A small production smoke test

npm run build
npm run start

curl -I https://preview.example.com/guides/nextjs-seo
curl -I https://preview.example.com/old-guide
curl -I https://preview.example.com/not-a-real-page
curl -s https://preview.example.com/guides/nextjs-seo | grep -E "<title|canonical|robots|application/ld\+json"
curl -I https://preview.example.com/robots.txt
curl -I https://preview.example.com/sitemap.xml
  All articles