Solve.rs
Next.js SEO guide

Next.js SEO architecture for organic growth

Next.js SEO starts with architecture. The framework already gives you server-rendered HTML, the Metadata API, sitemaps and image optimization; your job is to use them consistently on every page. This guide shows how to turn that into fast, search-friendly applications that rank well and convert visitors into users.

Core components

Six essential Next.js SEO features

Metadata API

Use the Next.js Metadata API for all SEO tags: a metadata object for static pages, or generateMetadata for dynamic ones.

  • Benefit: type-safe, server-rendered metadata on every page

Static generation

Pre-render pages at build time and serve them from cache, without waiting on the server.

  • Benefit: short time to first byte (TTFB) and a stable LCP

Dynamic sitemap

app/sitemap.ts builds your sitemap from the same data that builds your pages.

  • Benefit: always up-to-date sitemap without manual updates

Image optimization

Use the Next.js Image component for automatic sizing, modern formats (WebP or AVIF) and lazy loading.

  • Benefit: faster LCP, proper sizing, modern formats

Structured data

JSON-LD in a <script type="application/ld+json"> tag tells search engines who you are and what is on the page; some types also unlock rich results.

  • Benefit: search engines understand your content more accurately

Robots.txt

Control where crawlers may go in app/robots.ts.

  • Benefit: crawlers do not waste time on admin and private pages. robots.txt does not remove a page from results – use noindex for that.

Copy-paste ready code

The four core pieces of SEO architecture in the Next.js App Router: metadata, sitemap, robots.txt and structured data. Replace yoursite.com with your domain and the strings with your own content.

app/page.tsx · app/sitemap.ts · app/robots.ts · components/StructuredData.tsxtsx
// app/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Your Page Title — Brand Name',
  description: 'Compelling description 150-160 characters that includes target keywords and value proposition.',
  openGraph: {
    title: 'Your Page Title',
    description: 'Description for social sharing',
    url: 'https://yoursite.com/page',
    siteName: 'Your Site Name',
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'Your Page Title',
    description: 'Description for Twitter',
  },
  alternates: {
    canonical: 'https://yoursite.com/page',
  },
}


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

// lastModified = the date the content really changed, not new Date() on every build.
// Google ignores changeFrequency and priority, so we leave them out.
export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://yoursite.com',
      lastModified: new Date('2026-11-02'),
    },
    {
      url: 'https://yoursite.com/about',
      lastModified: new Date('2026-06-15'),
    },
    // Add all your routes
  ]
}


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

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: '/admin/',
    },
    sitemap: 'https://yoursite.com/sitemap.xml',
  }
}


// components/StructuredData.tsx
export function OrganizationSchema() {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name: 'Your Company',
    url: 'https://yoursite.com',
    logo: 'https://yoursite.com/logo.png',
    description: 'Company description',
    sameAs: [
      'https://x.com/yourcompany',
      'https://www.linkedin.com/company/yourcompany',
    ],
  }

  // Replacing < prevents HTML injection through the data (as the Next.js docs recommend)
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify(schema).replace(/</g, '\\u003c'),
      }}
    />
  )
}
Performance

Optimize Core Web Vitals

Core Web Vitals are part of Google’s ranking signals. They will not make up for weak content, but a fast site keeps visitors. Google measures them on real visits: a page passes when at least 75% of visits are within the thresholds below. Next.js has a tool for each of the three metrics.

LCP (Largest Contentful Paint) – target: 2.5 seconds or less

  • Render the main above-the-fold image with the Image component and fetchPriority="high" so it is not lazy-loaded (in Next.js 16 the priority prop is deprecated in favor of preload)
  • Load fonts with next/font: they are self-hosted and preloaded
  • Server-side render above-the-fold content
  • Use static generation when possible

INP (Interaction to Next Paint) – target: 200 milliseconds or less

  • Keep non-interactive parts as Server Components so less JavaScript reaches the browser
  • Load heavy components only when needed (next/dynamic)
  • Wrap expensive state updates in useTransition so the response to a click does not wait
  • Break long tasks into shorter ones so they do not block the main thread
  • Load third-party scripts with next/script using the afterInteractive or lazyOnload strategy

CLS (Cumulative Layout Shift) – target: 0.1 or less

  • Always give images dimensions (width and height, or fill inside a fixed-size container)
  • Reserve space for content that arrives later: banners, embeds, ads
  • Avoid inserting content above what is already on screen
  • next/font tunes the fallback font so text does not shift when the real font loads

INP replaced FID (First Input Delay) on March 12, 2024. FID only measured the delay of the first interaction; INP tracks the responsiveness of every click, tap and key press during a visit, so it is stricter.

Dynamic routes and multilingual metadata

generateStaticParams turns dynamic routes into static pages, and generateMetadata gives every page its own canonical and hreflang. Since Next.js 15, params is a Promise, so you await it. For non-English content, load the latin-ext font subset and set the right html lang. This example is for a site with Serbian at “/” and English at “/en”, which is how this site works.

app/blog/[slug]/page.tsx · app/layout.tsx · lib/format.tstsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { getAllPosts, getPost } from '@/lib/posts'

// Pre-render every post at build time
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map((post) => ({ slug: post.slug }))
}

// Unique title, description, canonical and hreflang per page
export async function generateMetadata(
  { params }: { params: Promise<{ slug: string }> },
): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) return {}
  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical: `https://yoursite.com/blog/${slug}`,
      languages: {
        sr: `https://yoursite.com/blog/${slug}`,
        en: `https://yoursite.com/en/blog/${post.enSlug}`,
        'x-default': `https://yoursite.com/en/blog/${post.enSlug}`,
      },
    },
  }
}

export default async function Page(
  { params }: { params: Promise<{ slug: string }> },
) {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) notFound()
  return <article>{post.title}</article>
}


// app/layout.tsx
import { Inter } from 'next/font/google'

// latin-ext adds č, ć, š, ž, đ and other Central European letters
const inter = Inter({ subsets: ['latin', 'latin-ext'], display: 'swap' })

// <html lang> lives in the root layout. For two languages, use one root layout
// per language (route groups such as app/(sr) and app/en) or a [lang] segment.
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="sr-Latn" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}


// lib/format.ts
// Plain “sr” gives Cyrillic month names; use sr-Latn-RS for Latin script
export const formatDate = (date: Date) =>
  new Intl.DateTimeFormat('sr-Latn-RS', { day: 'numeric', month: 'long', year: 'numeric' }).format(date)

Next.js SEO questions answered

Should I use App Router or Pages Router for SEO?

Both serve HTML from the server, so a Pages Router site can rank well too. The App Router still has the edge: the Metadata API, sitemap.ts and robots.ts files, Server Components that ship less JavaScript to the browser, and streaming. It is also where Next.js is heading, so start new projects with the App Router.

How do I handle dynamic routes for SEO in Next.js?

Use generateStaticParams to pre-render dynamic routes at build time. This gives you static pages for SEO while maintaining dynamic routing. For frequently changing content, use revalidate (or cacheLife if you use Cache Components in Next.js 16) to control how often pages are regenerated.

Do I need Server-Side Rendering for SEO?

Not always. What search engines need is HTML that already contains the text, and both SSR and static generation deliver that. A static page is served from cache, so the server responds faster. Use SSR when a page must show data at request time, such as live stock levels or water levels. Most marketing pages should use static generation.

How do I add structured data in Next.js?

Create a component that renders a <script type="application/ld+json"> tag with your JSON-LD data and include it in your page or layout. Replace < in the data with \u003c, as in the example above. Use schema.org types that match your content (Organization, Article, Product, etc.) and check them with the Rich Results Test.

Can Next.js handle large sitemaps?

Yes. Generate your sitemap from your database or CMS. A single sitemap file can hold at most 50,000 URLs and 50 MB, so split larger sites into several files with generateSitemaps and list them all in robots.txt or a sitemap index.

How do I optimize Core Web Vitals in Next.js?

Use the Image component, static generation, next/font and next/script, and load heavy parts only when needed. Next.js handles most optimizations automatically if you use its built-in components. Monitor with PageSpeed Insights and the Core Web Vitals report in Search Console, and collect real-user data yourself with the useReportWebVitals hook.

How do I set up hreflang for multiple languages?

In generateMetadata, list every language version plus x-default in alternates.languages, and give each version its own canonical. The links must be reciprocal – each version lists all the others – or Google ignores them. Set the matching html lang on each version. The code example above shows the full setup.

Implementation checklist

Next.js SEO checklist

Use this checklist to ensure your Next.js application is fully optimized for search engines.

  • Metadata API used for all pages
  • metadataBase set in the root layout so canonicals and Open Graph images get absolute URLs
  • Unique title and description per page
  • Open Graph and Twitter Card metadata
  • Dynamic sitemap.ts with accurate lastModified dates
  • Robots.txt properly configured
  • Structured data (JSON-LD) for relevant pages
  • Next.js Image component used throughout
  • Static generation for all possible pages
  • Dynamic routes with generateStaticParams
  • Canonical and hreflang for every language version (if multilingual)
  • Font with the latin-ext subset for content in Serbian or other Central European languages
  • Internal linking between related pages
  • Proper heading hierarchy (single H1)
  • Alt text on all images
  • Core Web Vitals passing (LCP, INP, CLS)
  • Mobile responsive and tested
  • HTTPS enabled

Need help building SEO-optimized Next.js apps?

We specialize in building Next.js applications with SEO-first architecture: metadata, structured data, sitemaps and speed are part of the build, not an afterthought.

Explore web development