← Back to blog

SEO for Next.js Websites: Metadata, Sitemaps and Rendering

7 min read
Next.jsSEOTechnical SEO

Next.js gives you more control over SEO than almost any other framework, and that is exactly why it is easy to get wrong. Nothing stops you from writing a page with no metadata, a canonical tag that points everywhere at once, or a sitemap that lists broken URLs. There is no plugin quietly protecting you the way there might be on a platform like WordPress. This guide walks through the parts of a Next.js project that actually decide how the site performs in search, in the order we check them on real projects, including a few mistakes we made on our own site before fixing them.

If you are choosing between Next.js and another platform in the first place, our Next.js vs WordPress comparison covers that decision. This article assumes you have already chosen Next.js and want to use it well.

Rendering: make sure content exists without JavaScript

The single biggest SEO advantage Next.js offers is also the easiest to lose. If a page is rendered entirely on the client, with an empty shell delivered first and content filled in afterward by JavaScript, crawlers that do not execute scripts see nothing, and even Google's own rendering is queued and slower than reading plain HTML.

Next.js supports static generation, server rendering and incremental regeneration, and the right choice depends on the page. Marketing pages, service pages and blog posts change rarely and are the same for every visitor, so static generation is usually correct: the HTML is built once and served instantly, already containing your headings and text. Pages with per-user content need server rendering. A blog that publishes often can use incremental regeneration to refresh static pages on a schedule without a full rebuild. Whichever you choose, the test is simple: view the page source, or fetch the URL without a browser, and confirm your actual content is there.

The Metadata API: title, description and canonical per page

Next.js lets each route export its own metadata through generateMetadata or a static metadata object, and this is where most real SEO work happens. Every indexable page should define its own title and description rather than inheriting one from a layout, because a layout-level title becomes generic the moment two pages need to say different things. We rewrote our own site's metadata pattern after noticing that a layout-level canonical was silently pointing every page without its own to the homepage. It is easy to write once, forget, and never notice until a page fails to rank for anything.

A working pattern for a page component:

export async function generateMetadata({ params }): Promise<Metadata> {
  const { locale, slug } = await params;
  return {
    title: pageTitle,
    description: pageDescription,
    alternates: {
      canonical: `https://example.com/${locale}/path/${slug}`,
      languages: { en: enUrl, fa: faUrl, "x-default": faUrl },
    },
    openGraph: { title: pageTitle, description: pageDescription, url: canonicalUrl, images: [ogImage] },
  };
}

Two habits prevent most title problems. First, avoid a root-level title template that appends the brand name to a title that already contains it, which produces "Page Title | Brand | Brand" on any page that sets its own full title. Second, check the rendered output after building, not just the code, because a template applied at the wrong level is invisible until you look at the actual HTML.

Sitemaps and robots as code

The app/sitemap.ts and app/robots.ts files let you generate these from your real content instead of maintaining them by hand. This matters more than it sounds: a hand-maintained sitemap silently drifts out of date the first time someone forgets to add a new page, while a generated one is automatically correct as long as it reads from the same source your pages are built from, such as a services list or the blog's file system.

For a bilingual site, build the sitemap so that each URL declares its language alternates, including an x-default entry. This is what lets Google understand that your Persian and English pages are versions of the same content rather than unrelated duplicates, and it is a detail that is trivial to add in code and easy to skip if you write the file manually.

Structured data as JSON-LD

Structured data belongs inline in the page, generated from the same data that renders the visible content, so the two can never drift apart. For a business site, a practical starting set is Organization on the layout, WebSite with a search action if relevant, BreadcrumbList on nested pages, Article on blog posts, and Service or Product where it fits. Next.js makes this straightforward: build a plain JavaScript object and render it inside a script tag with type="application/ld+json". The only rule that matters is that the markup must match what a visitor can actually see; do not describe content that is not really on the page.

Common Next.js-specific mistakes

A few problems show up repeatedly on Next.js projects specifically, because they come from how the framework is structured rather than from general SEO ignorance.

A layout-level canonical or title that overrides every page. Metadata defined in a parent layout merges with, or in some cases overrides, metadata from a page, depending on which fields you set where. Test this by inspecting two different page types after a build, not by reading the code and assuming it is correct.

Dynamic routes without proper generateStaticParams. If a dynamic route such as [slug] does not pre-generate its known paths, pages may render on demand in a way that is slower for the first crawl or, depending on configuration, not statically available at all.

Client components used where server components would do. Marking a component "use client" unnecessarily pulls more JavaScript to the browser and can delay the content that component renders. Keep data-fetching and content rendering in server components wherever the page does not need interactivity.

Images without the next/image component. The built-in image component handles responsive sizing, lazy loading below the fold and format optimization automatically. Skipping it for a large hero image is one of the most common causes of a poor LCP score, which we cover in detail in our guide to Core Web Vitals.

Forgetting robots on intentionally private routes. Preview routes, internal tools or draft content built into the same Next.js app need an explicit noindex or exclusion from the sitemap, because nothing prevents Next.js from happily rendering and serving them to a crawler.

Verify after every deploy

Configuration mistakes in code are invisible until you look at the built output. After a deploy, spot-check the rendered HTML of a few different page types: fetch the URL directly and search for your title tag, your canonical link and your JSON-LD block. Run the pages through Google's Rich Results Test and PageSpeed Insights. And watch the Search Console indexing report over the following weeks; if a category of pages is excluded or a canonical points somewhere unexpected, that is where the framework's own configuration usually deserves a second look. We describe exactly this kind of investigation, on our own site, in our case study on pages that were not indexed.

Next.js does not do SEO for you, but it removes almost every technical excuse for getting it wrong. The rest is the same discipline any site needs: unique metadata per page, a sitemap that reflects reality, structured data that matches the content, and enough substance on each page to deserve a place in the index. If you want an outside review of your own setup, our SEO service includes a full technical audit of Next.js projects, and our website design and development service builds these foundations in from the start.

Related articles

Comments