# Next.js SEO: what the framework does for you and what it does not

You ship with Next.js. You want pages that get crawled, indexed and clicked. Here is what the framework handles for SEO, what it exposes for you, and what no framework can do for you.

Updated 2026-09-14 · Source: https://porteur.ai/guides/nextjs-seo

## What Next.js gives you for SEO by default

Next.js App Router ships with good SEO defaults. Pages are statically rendered by default. Dynamic rendering is opt in. Crawlers that do not run scripts can read your HTML.

- Metadata API for titles, canonicals and Open Graph, via metadata objects or generateMetadata
- app/sitemap.ts and app/robots.ts to output XML and robots.txt
- Redirects and rewrites in next.config
- next/image for responsive images and formats
- next/font to self host fonts
- A trailingSlash setting to enforce one URL style

These tools remove boilerplate work. They do not write your titles. They do not decide the canonical. They do not make thin pages rank.

## Metadata API: titles, canonicals and social tags

Set per page metadata in the App Router. Use a static metadata export for simple pages. Use generateMetadata for dynamic values from params or data.

```ts
// app/pricing/page.tsx
export const metadata = {
  title: "Pricing for Acme",
  description: "Plans and limits.",
  alternates: { canonical: "/pricing" },
  openGraph: {
    title: "Pricing for Acme",
    description: "Plans and limits.",
    url: "/pricing",
    type: "website"
  }
};

// app/blog/[slug]/page.tsx
import { getPost } from "@/lib/data";
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  return {
    title: post.seoTitle ?? post.title,
    description: post.excerpt,
    metadataBase: new URL("https://yourproduct.com"),
    alternates: { canonical: `/blog/${post.slug}` },
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url: `/blog/${post.slug}`
    }
  };
}

```

Set metadataBase once so relative canonicals and Open Graph URLs resolve to the right origin. Use alternates.canonical for the page you want indexed. Keep titles under about 60 characters.

A fixed page looks like this: /guides/getting-started has a title that says the task, a canonical to itself, and an Open Graph image that matches the post.

## Sitemap and robots: ship the right files from the app

Keep your sitemap small, current and on the same host. Output only indexable URLs. Exclude paginated filters and drafts. Update on deploys or when content changes.

```ts
// app/sitemap.ts
import { getAllRoutes } from "@/lib/routes";
export default async function sitemap() {
  const base = "https://yourproduct.com";
  const routes = await getAllRoutes(); // e.g. ["/","/pricing","/blog/nextjs-seo"]
  return routes.map((path) => ({
    url: `${base}${path}`,
    lastModified: new Date(),
    changeFrequency: "weekly",
    priority: 0.7
  }));
}

```

```ts
// app/robots.ts
export default function robots() {
  const base = "https://yourproduct.com";
  return {
    rules: [{ userAgent: "*" }],
    sitemap: [`${base}/sitemap.xml`],
    host: "yourproduct.com"
  };
}

```

Test the files in a browser. Check Search Console, Sitemaps, after deploy. If you split sitemaps, link a sitemap index at /sitemap.xml. Keep only canonical URLs in it.

## Rendering: static HTML and crawlers that do not run scripts

Static rendering is the default in the App Router. That helps crawlers that fetch HTML only. Your title, canonical and content arrive in the first response.

- Use fetch with cache options to keep a route static when data allows it
- Mark truly dynamic routes with dynamic = "force-dynamic" only when needed
- Avoid client components for content that could be server rendered
- Do not hide primary content behind client side effects

```ts
// app/blog/[slug]/page.tsx
export const dynamic = "force-static"; // or omit if static by default
export async function generateStaticParams() {
  const slugs = await getAllSlugs();
  return slugs.map((slug) => ({ slug }));
}

```

If a route must be dynamic, keep the HTML meaningful without JavaScript. Render headings and copy on the server. Defer widgets to the client with care.

> Do not assume every crawler runs your React on first fetch. Some do not. Ship useful HTML for the first response.

## Redirects, rewrites and site moves

Use redirects for URL changes and migrations. Configure them in next.config so they run at the edge on most platforms. Keep hops to one.

```js
// next.config.js
module.exports = {
  async redirects() {
    return [
      { source: "/old-pricing", destination: "/pricing", permanent: true },
      { source: "/blog/:slug/", destination: "/blog/:slug", permanent: true },
    ];
  },
  async rewrites() {
    return [
      { source: "/api/:path*", destination: "https://api.yourproduct.com/:path*" },
    ];
  },
};

```

Plan a site move like a project. Build a redirect map as a two column table in your repo. Old URL, new URL. Test every mapping before you switch DNS or deploy the change.

1. **Build the map** Crawl the old site, export from its sitemap, and export indexed pages from Search Console. Merge into one list. Map each to one new URL.
2. **Apply and test** Apply redirects in next.config or at your proxy. Script test that each old URL 301s in one hop to a 200 at the new URL.
3. **Switch and monitor** Deploy. Keep the old domain responding with 301s. Submit the old sitemap for a while so Google recrawls and sees the moves.

> A site move with URL changes needs a one to one 301 map. Do not point everything to the home page, that pattern is treated like soft 404s.

For a domain change, verify both domains in Search Console. Use Settings, Change of address. It works for domain level moves. Not for path only changes. Expect some fluctuation for weeks. Keep redirects for at least a year, longer if you can. Update internal links, canonicals, hreflang, structured data and sitemaps to the new URLs.

## URLs: trailing slashes, www and HTTPS

Pick one URL style and be consistent. Google treats /page and /page/ as different URLs. The root / is the exception. Next.js has a trailingSlash setting.

```js
// next.config.js
module.exports = {
  trailingSlash: false, // or true, but choose one and redirect the other
};

```

Pick www or non www. Redirect the other, on every path, with a 301. Set canonicals to the chosen host. Verify a Domain property in Search Console so you see both.

Move to HTTPS if you are not already. 301 each HTTP URL to its HTTPS twin in one hop. Update internal links, canonicals and sitemaps. Avoid mixed content. Consider HSTS once you are sure.

A fixed page looks like this: /pricing loads at https://www.yourproduct.com/pricing, the non www and HTTP forms redirect to it, and its canonical matches the final URL.

## Images and fonts: ship fast, stable pages

next/image helps with responsive sizes and modern formats. It can reduce payload and Largest Contentful Paint. You still choose sizes and placeholders that make sense for your layout.

```tsx
// app/(marketing)/components/Hero.tsx
import Image from "next/image";
export default function Hero() {
  return (
    <Image
      src="/hero.png"
      alt="Dashboard overview"
      width={1200}
      height={800}
      priority
      sizes="(max-width: 768px) 100vw, 1200px"
      placeholder="blur"
      blurDataURL="/hero-blur.png"
    />
  );
}

```

The width and height shown here are examples, not measured on your page. Measure your hero and set exact dimensions for your layout.

Set explicit width and height to avoid layout shift. Use priority on the above the fold hero only. Lazy load the rest by default. Serve real alt text that says the image, not the keyword list.

next/font lets you self host fonts. That helps with privacy, control and performance. Preload the main font weight. Avoid layout shift by using font metrics or fallback settings.

```tsx
// app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], display: "swap" });
export default function RootLayout({ children }) {
  return <html className={inter.className}><body>{children}</body></html>;
}

```

A fixed page looks like this: the hero uses next/image with correct sizes, the LCP element loads within 2.5 seconds, and there is no layout jump from fonts or images.

## Internal links and content: the work no framework does

Search needs pages worth indexing. That is not a build setting. Write a title that says the page. Put the task or the product in the first heading. Cover the topic fully.

- Link related pages. From /guides/getting-started to /pricing with a clear anchor like "See pricing"
- Fix orphan pages. Add them to navigation or link them from a hub page
- Avoid keyword cannibalisation. One page per topic or intent
- Use descriptive anchors. Not "click here"
- Keep canonicals self referencing, unless you have a true duplicate variant

A fixed page looks like this: /blog/nextjs-seo links to /“Technical SEO checklist for a small site” and /pricing in body copy. The anchor text explains the target. The canonical is set to itself in generateMetadata.

## Measure and improve: Core Web Vitals and crawl checks

Check your field data. Core Web Vitals use the 75th percentile of real Chrome users over 28 days. Good thresholds are LCP up to 2.5 s, CLS up to 0.1, INP up to 200 ms.

- Use PageSpeed Insights for field data and a Lighthouse run
- Use Lighthouse locally for lab work. Scores vary by run
- Watch the Search Console Core Web Vitals report for per URL groups
- Use your platform logs or a crawler to find broken links and redirect chains

Lighthouse weights, as of version 10 and later, are TTB 30%, LCP 25%, CLS 25%, FCP 10% and Speed Index 10%. Use it to spot long tasks and render delays. Do not chase the score at the expense of features that users need.

1. **Check a slow page** Open PageSpeed Insights for https://yourproduct.com/pricing. Note the LCP element and its load time. Note any layout shift sources.
2. **Fix what you ship** Compress the hero image, serve the right size, and preload its font. Remove unused third party scripts. Keep INP under 200 ms by trimming expensive event handlers.
3. **Verify in the field** Wait for the 28 day window to reflect changes. Confirm in Search Console, Core Web Vitals, that the URL group moved to good.

Porteur can scan your site and rivals from a URL and show three findings to start. It takes about thirty seconds and is free.

## Questions

### Is Next.js good for SEO?

Yes. App Router pages are static by default, which gives crawlers HTML with titles and content on first fetch. It also exposes metadata, sitemap, robots, redirects, image and font tooling. You still need to write good titles, choose canonicals, and build internal links.

### Do I need to use generateMetadata or is a static metadata object enough?

Use a static export when values are known at build, like /pricing. Use generateMetadata when you need params or fetched content, like /blog/[slug]. In both cases set metadataBase and alternates.canonical so URLs resolve correctly.

### How do I make Next.js pages indexable when content is dynamic?

Prefer server components and static rendering where possible. For truly dynamic routes, render meaningful HTML on the server and hydrate only what needs interaction. Avoid hiding headings and copy behind client effects. Keep query string gates off primary content.

### Should I add a trailing slash in URLs?

Pick a style and keep it. /page and /page/ are different to Google. Set trailingSlash in next.config and add a redirect for the other form. Keep internal links consistent in code and content.

### What performance targets should I use for SEO?

Aim for LCP up to 2.5 seconds, CLS up to 0.1, and INP up to 200 ms. Field assessment uses the 75th percentile over 28 days. Use PageSpeed Insights for field data and Lighthouse for lab checks.

### How do I handle a domain change with Next.js?

Build a one to one 301 map, apply it in next.config or at the edge, and keep it live for at least a year. Verify both domains in Search Console and use the Change of address tool. Update internal links, canonicals, hreflang, structured data and sitemaps to the new URLs. Expect some fluctuation for a few weeks.

## Read next

- [Technical SEO checklist for a small site](https://porteur.ai/guides/technical-seo-checklist): Run these 20 technical SEO checks, in order. Each shows how to check it free and what fixed looks like for a site under 1,000 pages.
- [Canonical tags: what they do and the mistakes that cost rankings](https://porteur.ai/guides/canonical-tag): What a canonical tag does, when to use one, the mistakes that cost rankings, and how to check and fix canonicals on a small site.
- [Trailing slash: two URLs for one page unless you decide](https://porteur.ai/guides/trailing-slash-seo): Google sees /page and /page/ as different. Pick one form, redirect the other, and keep links and your sitemap consistent. Here is how to do it.
- [Core Web Vitals for a product site: the three numbers](https://porteur.ai/guides/core-web-vitals): The product founder’s guide to LCP, CLS and INP: what Google measures, why phones decide the pass, common causes, and how to test and fix.
- [Image optimisation for page speed: the hero image and everything below it](https://porteur.ai/guides/image-optimisation-for-page-speed): Fix the hero image first for LCP, then lazy-load the rest. Choose AVIF or WebP, set srcset and sizes, and add width and height to stop shift.
- [Web fonts and page speed: load one face, and load it first](https://porteur.ai/guides/web-fonts-and-page-speed): Ship crisp type without slowing first paint or shifting layout. Choose one fast path, preload the face above the fold, and stop the swap from moving text.
- [The best SEO Chrome extensions: what each shows in one click](https://porteur.ai/guides/best-seo-chrome-extensions): The Chrome extensions that show titles, headings, canonicals, robots, schema and Web Vitals in one click, plus volumes on SERPs, and which two to install.

Paste your URL to get a free check of your Next.js site’s metadata, sitemaps, redirects and rivals in about thirty seconds, with three findings you can ship. Free check: https://porteur.ai/
