/ โ† JavaScript Fixes / Next.js SEO
In this guide
  1. What SEO means in a Next.js site
  2. Rendering choices and how they affect crawling
  3. The Metadata API: titles, descriptions and canonicals
  4. Sitemaps, robots, structured data and status codes
  5. Links, images and speed
  6. A worked example: a shop that Google could not read
  7. The fix, step by step
  8. Edge cases worth knowing
  9. Keeping a Next.js site search-friendly
  10. Questions and answers

What SEO means in a Next.js site

Search engines judge the HTML a URL returns, the status code, how fast it arrives and the links inside it. In a Next.js site, the framework decides much of that for you, so Next.js SEO is mostly about using its features in the way that puts the right content into the first response:

Google can run JavaScript, but rendering happens in a queue after crawling, and many other crawlers, including most AI crawlers, read only the raw HTML. Content that needs the browser to appear is slower to be indexed at best, and invisible to some systems at worst. See JavaScript rendering for crawlers.

Rendering choices and how they affect crawling

ApproachHow it is writtenContent in first HTML?SEO verdict
Static generationServer Component with no dynamic data, or generateStaticParamsYesBest for content pages
Incremental regenerationexport const revalidate = 3600YesGood for catalogues that change
Server rendering per requestDynamic Server Component, or getServerSidePropsYesGood, watch server response time
Client Component rendered on the server'use client' with data passed in as propsYesFine
Client-side data fetchinguseEffect or a client fetch libraryNo, only a loading stateAvoid for content you want indexed

A common mistake is to believe 'use client' hides content from Google. It does not: Client Components are still rendered to HTML on the server. What hides content is fetching the data inside the browser, so the server-sent HTML contains only a spinner. Fetch the data in a Server Component, or in getStaticProps or getServerSideProps in the Pages Router, and pass it down.

How to check what is in the HTML

curl -s https://example.com/products/blue-mug | grep -c 'Blue Mug'
curl -s https://example.com/products/blue-mug | grep -iE '<title>|rel="canonical"|name="description"|name="robots"'

If the product name is not found in the raw HTML, the content is being built in the browser. URL Inspection in Search Console shows the rendered version Google produced, which is useful for comparison.

The Metadata API: titles, descriptions and canonicals

In the App Router, metadata is exported from layout and page files, and Next.js writes the tags into the head. Set the defaults once in the root layout:

// app/layout.tsx
export const metadata = {
  metadataBase: new URL('https://example.com'),
  title: { default: 'Example Bakery', template: '%s | Example Bakery' },
  description: 'Sourdough, pastries and celebration cakes baked daily.',
  alternates: { canonical: '/' },
};

metadataBase turns relative URLs into absolute ones, so canonicals and social images do not point at localhost. For dynamic routes, use generateMetadata and give every page its own canonical:

// app/products/[slug]/page.tsx
import { notFound } from 'next/navigation';

export async function generateMetadata({ params }) {
  const { slug } = await params;
  const product = await getProduct(slug);
  if (!product) return {};
  return {
    title: product.name,
    description: product.summary,
    alternates: { canonical: `/products/${slug}` },
    openGraph: { title: product.name, images: [product.image] },
  };
}

export default async function Page({ params }) {
  const { slug } = await params;
  const product = await getProduct(slug);
  if (!product) notFound();
  return <ProductView product={product} />;
}

In recent versions params is a promise, hence the await. Data requests made with fetch in both functions are de-duplicated, so the product is not fetched twice. The Next.js meta tags guide covers Open Graph and Twitter fields in more detail.

The SEO component in the Pages Router

In the Pages Router there is no Metadata API. The equivalent is the built-in next/head component, which many people call the Next.js SEO component:

import Head from 'next/head';

<Head>
  <title>{`${product.name} | Example Bakery`}</title>
  <meta name="description" content={product.summary} />
  <link rel="canonical" href={`https://example.com/products/${product.slug}`} />
</Head>

Third-party packages such as next-seo wrap the same tags in a component with props. They work, but in the App Router the built-in Metadata API does the job without an extra dependency.

Sitemaps, robots, structured data and status codes

Next.js generates /sitemap.xml and /robots.txt from special files in the app folder:

// app/sitemap.ts
import type { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const products = await getAllProducts();
  return [
    { url: 'https://example.com/', lastModified: new Date() },
    ...products.map((p) => ({
      url: `https://example.com/products/${p.slug}`,
      lastModified: p.updatedAt,
    })),
  ];
}

// app/robots.ts
export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/', disallow: '/api/' },
    sitemap: 'https://example.com/sitemap.xml',
  };
}

For structured data, render a JSON-LD script inside the page component, escaping the less-than character as the Next.js documentation recommends:

const jsonLd = { '@context': 'https://schema.org', '@type': 'Product', name: product.name };

<script type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c') }} />

See JSON-LD in Next.js and robots and sitemaps in Next.js.

Status codes and redirects

A worked example: a shop that Google could not read

Example Bakery rebuilt its online shop in Next.js. Six weeks after launch, Search Console showed most product pages as Crawled, currently not indexed, and the ones that were indexed all had the same title.

The curl check found the product name missing from the raw HTML. The product page was a Client Component that fetched its data in useEffect, so the server sent a loading skeleton. The root layout set a title but no page set its own, and there was no canonical. Missing products showed a friendly message with status 200.

The fix moved the data fetch into the Server Component page, added generateMetadata with a title, description and canonical per product, called notFound() for missing slugs and added app/sitemap.ts. The same curl command then found the product name, and URL Inspection showed the correct title and canonical. Over the next month the indexed product count rose steadily as Google recrawled.

The fix, step by step

Step 1

Check what the server sends

Pick a page of each template, such as the homepage, a category and a product or article. Fetch each one with curl and search the raw HTML for its main heading and a sentence of body text. Also grep for the title, canonical, description and robots tags. Write down which templates are missing content or tags. This baseline shows where the work is needed and lets you prove later that each fix made it into the HTML.

Step 2

Move content fetching to the server

For any template whose content is missing from the raw HTML, move the data fetch out of useEffect and into a Server Component, or into getStaticProps or getServerSideProps in the Pages Router. Pass the data to Client Components as props if they need interactivity. Rebuild and fetch the page with curl again. Confirm that the heading and body text are now in the HTML before any JavaScript runs.

Step 3

Set default metadata in the root layout

In app/layout.tsx, export a metadata object with metadataBase set to your production address, a default title, a title template that adds your brand, and a default description. In the Pages Router, set the same defaults in a shared layout component using next/head. Build the site and confirm with curl that every page now has a title and that canonical and image URLs are absolute, not relative to localhost.

Step 4

Add page-level metadata and canonicals

In every dynamic route, export generateMetadata and return a unique title, a description and alternates.canonical built from the clean path, without query parameters. For static routes, export a metadata object. Keep the trailing slash consistent with your trailingSlash setting. Confirm by fetching several pages with curl and checking that each canonical matches the address you requested and every title is different.

Step 5

Return correct status codes

Where a route looks up a record, call notFound() when it does not exist, before any loading boundary starts streaming. Add permanent redirects for moved pages in next.config.js with permanent: true, which return 308. Then run curl -I on a made-up product address and on an old address. Confirm the first returns 404 and the second returns 308 with a location header pointing to the new page.

Step 6

Generate the sitemap and robots file

Create app/sitemap.ts that returns every indexable URL with its last modified date, built from the same data source as your pages, and app/robots.ts that allows crawling and points to the sitemap. Leave out redirected, noindexed and parameter URLs. Deploy, open both files in a browser, then submit the sitemap in Search Console. Confirm the status reads Success and the discovered count matches your pages.

Step 7

Add structured data to key templates

Build a JSON-LD object for each template, such as Product, Article or LocalBusiness, from the page's data, and render it in a script tag with type application/ld+json, escaping the less-than character. Test a page of each type in the Rich Results Test and fix any errors. Confirm that the structured data appears in the raw HTML from curl and matches the content visible on the page.

Step 8

Fix links, images and scripts

Replace any navigation built from buttons with router.push with next/link, so pages link with real anchors. Give the hero next/image priority loading and accurate sizes, and load third-party scripts with next/script using a deferred strategy. Then test a typical page in Lighthouse on mobile and compare Largest Contentful Paint and total JavaScript size with your earlier figures. Both should be lower, and the hero image should start loading early in the waterfall.

Step 9

Verify in Search Console

Use URL Inspection on one page of each template, run Test live URL and check the rendered HTML, the detected canonical and that indexing is allowed. Request indexing for the most important pages. Then watch the Pages report over the next few weeks. Falling numbers in reasons such as crawled, currently not indexed show that Google is now reading the content and metadata you fixed.

Edge cases worth knowing

Keeping a Next.js site search-friendly

Framework upgrades and new features can change what each route sends. Build the checks into your workflow:

  1. Add a test that fetches a few key routes from a production build and fails if the title, canonical or main heading is missing from the raw HTML.
  2. Check /sitemap.xml and /robots.txt after every release that changes routing.
  3. Run curl -I on a deliberately missing URL after upgrades to confirm it still returns 404.
  4. Crawl the site every few months to catch missing metadata on new routes. The Next.js site audit guide explains what to look for.
Questions and answers

1. What is SEO in Next.js?

SEO in Next.js means using the framework's features so each page sends search engines complete HTML on the first request, with its own title, description, canonical, structured data, correct status code and crawlable links. Next.js helps because it renders pages on the server by default in the App Router, and it has built-in tools for metadata, sitemaps and robots files. Most Next.js SEO problems come from fetching content in the browser, missing per-page metadata or wrong status codes, not from the framework itself.

2. Is Next.js good for SEO?

Yes, Next.js is one of the better choices for SEO among JavaScript frameworks, because it can generate pages statically or render them on the server, so the content is in the HTML that crawlers download. It also provides a Metadata API, sitemap and robots file generation, an image component and a link component that outputs real anchors. It can still be used badly. A page that fetches all its content in the browser with useEffect is no better for search than a single-page app.

3. What is the Next.js SEO component?

In the Pages Router, the SEO component is usually next/head, the built-in component that lets you place title, meta and link tags in the head of a page. Some projects use the third-party next-seo package, which wraps the same tags in a component with props for titles, descriptions, canonicals and Open Graph data. In the App Router you do not need a component at all: you export a metadata object or a generateMetadata function from each layout or page, and Next.js writes the tags.

4. Does Google index client-side rendered Next.js pages?

Google can index content that is rendered in the browser, because it runs JavaScript in a second rendering stage, but it is slower and less reliable than server-rendered HTML. Rendering happens in a queue, errors or timeouts in your scripts can leave content out, and many other search engines and AI crawlers do not run JavaScript at all. For pages you want found, render the content on the server with Server Components, static generation or server-side rendering, and keep client-side fetching for personal or interactive data.

5. How do I add a canonical tag in Next.js?

In the App Router, set alternates.canonical in the metadata object or in the value returned by generateMetadata, for example a path such as /products/blue-mug. Define metadataBase in the root layout so Next.js turns the path into an absolute address. In the Pages Router, add a link tag with rel set to canonical inside next/head. Build the canonical from the clean path without tracking or sort parameters, keep the trailing slash consistent, and check the output with curl or URL Inspection.

6. How do I create a sitemap in Next.js?

In the App Router, add a sitemap.ts file to the app folder that exports a default function returning an array of entries, each with a url and optionally lastModified. Next.js serves it at /sitemap.xml. Build the list from the same data source as your pages so new content appears automatically, and leave out redirected or noindexed addresses. For very large sites, generateSitemaps splits the output into several files. In the Pages Router, generate the file at build time or serve it from an API route or a package.

7. Should I use static generation or server-side rendering for SEO?

Both are good for SEO, because both put the full content in the HTML. Static generation is usually better for pages that change rarely, such as articles and landing pages, because the HTML is ready before anyone asks for it and arrives quickly from a CDN. Server-side rendering suits pages that must be current on every request, such as stock levels or prices. Incremental regeneration with a revalidate interval sits between the two and suits most catalogues. Avoid client-only rendering for indexable content.

See what crawlers get from your Next.js pages

The JavaScript checker compares the raw HTML your server sends with the rendered page, so you can see which content, links and tags only appear after JavaScript runs and need moving to the server.

Open JavaScript Fixes โ†’
๐Ÿ’ฌ Got a problem?
Related Guides: All JavaScript Fixes  ·  Meta tags in Next.js  ·  Robots.txt and sitemaps in Next.js  ·  JSON-LD in Next.js  ·  JavaScript rendering for crawlers  ·  Learning Hub

About aiwebpageseo

aiwebpageseo.com is a data-driven SEO and AEO (Answer Engine Optimisation) platform providing a free suite of technical SEO tools. Rather than relying on AI-theorised assumptions, the platform analyses live URL performance, delivering objective diagnostics, page speed metrics, CLS debugging, and site crawl data alongside actionable technical tutorials.