DEV Community

Nana Okamoto
Nana Okamoto

Posted on • Edited on

Practical Technical SEO in Next.js: The Implementation Guide

Modern SEO in Next.js (App Router) is not about adding meta tags.

It’s about controlling what HTML is delivered to crawlers, how it’s generated, and which pages are eligible for indexing.

This guide focuses on practical implementation patterns + why they matter.

1. Rendering Strategy Comes First

Why this matters

Rendering strategy determines:

  • Whether your content is visible in initial HTML
  • How fast your page loads (Core Web Vitals)
  • Whether Google treats your page as reliable

👉 In other words, this is the foundation of SEO in Next.js

Recommended strategies

Page Type Strategy Why
Blog / Marketing SSG (force-cache) Fast + fully indexable
Large sites ISR Scalable with good freshness
Personalized SSR (no-store) Needed for dynamic content
Dashboard CSR Not meant for indexing

Example

export default async function Page({ params }) {
  const post = await fetch(
    `https://api.example.com/posts/${params.slug}`,
    { cache: 'force-cache' }
  ).then(res => res.json())

  return <h1>{post.title}</h1>
}
Enter fullscreen mode Exit fullscreen mode

👉 Wrong choice here = everything else won’t fix SEO

2. Ensure Content Exists in Initial HTML

Why this matters

Search engines primarily index server-rendered HTML.
If your content depends on client-side JavaScript:

  • Crawlers may not execute it fully
  • Indexing becomes delayed or incomplete

👉 This leads to missing or weak rankings

Avoid

'use client'

useEffect(() => {
  fetch('/api/data')
}, [])
Enter fullscreen mode Exit fullscreen mode

Prefer

export default async function Page() {
  const data = await fetch('https://api.example.com').then(res => res.json())

  return <div>{data.title}</div>
}
Enter fullscreen mode Exit fullscreen mode

Verification

curl -s https://example.com | grep "<h1>"
Enter fullscreen mode Exit fullscreen mode

👉 If it’s not in HTML, it’s not reliably indexable

3. Metadata API: Full Implementation

Why this matters

Metadata controls how your page:

  • Appears in search results (title/description)
  • Avoids duplication (canonical)
  • Looks when shared (Open Graph)

👉 It directly impacts CTR (click-through rate) and index quality

Implementation

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,

    alternates: {
      canonical: `https://example.com/blog/${post.slug}`,
    },

    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.image],
      type: 'article',
    },

    twitter: {
      card: 'summary_large_image',
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

Key rules

  • Always define a canonical URL
  • Keep metadata consistent with page content
  • Use dynamic metadata for dynamic routes

4. Indexing Control (robots + metadata)

Why this matters

Search engines don’t decide what should be indexed—you do.

Without proper control:

  • Staging environments can get indexed
  • Duplicate pages can appear
  • Sensitive routes may leak

👉 This can damage your SEO trust and rankings

robots.ts

export default function robots() {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: ['/admin'],
    },
    sitemap: 'https://example.com/sitemap.xml',
  }
}
Enter fullscreen mode Exit fullscreen mode

Page-level control

const isProd = process.env.NEXT_PUBLIC_SITE_URL === 'https://example.com'

export const metadata = {
  robots: {
    index: isProd,
    follow: isProd,
  },
}
Enter fullscreen mode Exit fullscreen mode

👉 Always base this on domain, not environment

5. Sitemap Generation

Why this matters

A sitemap is a signal of intent:

“These are the URLs I want indexed.”

If incorrect:

  • Crawlers waste time on irrelevant pages
  • Important pages may be ignored
  • Indexing becomes inconsistent

Implementation

export default async function sitemap() {
  const posts = await getPosts()

  return posts
    .filter(post => post.published)
    .map(post => ({
      url: `https://example.com/blog/${post.slug}`,
      lastModified: post.updatedAt,
    }))
}
Enter fullscreen mode Exit fullscreen mode

Rules

  • Include only indexable pages
  • Match canonical URLs
  • Keep it clean (no duplicates)

6. Structured Data (JSON-LD)

Why this matters

Structured data helps search engines:

  • Understand your content better
  • Display rich results (e.g., article previews)

👉 This improves visibility and CTR, even if it’s not a direct ranking factor

Implementation

const jsonLd = {
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  headline: post.title,
  image: post.image,
  datePublished: post.date,
  dateModified: post.updatedAt,
  author: {
    "@type": "Person",
    name: post.author,
  },
}
Enter fullscreen mode Exit fullscreen mode

7. Performance: Optimize LCP

Why this matters

Core Web Vitals are ranking signals.

  • LCP (Largest Contentful Paint) = how fast main content appears
  • Slow LCP = worse rankings

👉 SEO is not just content—it’s performance

Example

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority
  sizes="100vw"
/>
Enter fullscreen mode Exit fullscreen mode

Additional tips

  • Keep above-the-fold content server-rendered
  • Avoid blocking scripts
  • Don’t delay main content with streaming

8. URL and Canonical Consistency

Why this matters

Search engines treat different URLs as different pages—even if content is identical.

Without consistency:

  • Duplicate content issues
  • Split ranking signals
  • Lower SEO performance

Ensure alignment across:

  • canonical
  • sitemap URLs
  • internal links

9. Scaling Dynamic Routes (ISR)

Why this matters

Large sites can’t statically generate everything.

ISR allows you to:

  • Keep pages fast
  • Update content incrementally

👉 This balances performance and freshness

Example

export const revalidate = 60
Enter fullscreen mode Exit fullscreen mode

Static params

export async function generateStaticParams() {
  const posts = await getPosts()

  return posts.map(post => ({
    slug: post.slug,
  }))
}
Enter fullscreen mode Exit fullscreen mode

10. Verification Checklist

Why this matters

You can’t trust what you see in the browser—JavaScript modifies it.

👉 SEO depends on raw server response

Check HTML

curl -i https://example.com/page
Enter fullscreen mode Exit fullscreen mode

Confirm:

  • Content is present
  • Metadata is correct
  • Canonical is correct

Tools

  • Google Search Console (URL Inspection)
  • Rich Results Test

Final Checklist

◽️ Data fetched in Server Components
◽️ Correct rendering strategy selected
◽️ HTML contains full content
◽️ Metadata fully implemented
◽️ Canonical URLs consistent
◽️ Sitemap clean and accurate
◽️ Robots configured correctly
◽️ Structured data added
◽️ LCP optimized
◽️ Verified via raw HTML

Conclusion

Technical SEO in Next.js is about intentional control.

  • Control how pages are rendered
  • Control what gets indexed
  • Keep all signals consistent

If you get these right, SEO becomes predictable—not guesswork.

Top comments (0)