Back to Blog
Tutorials 11 min read February 19, 2026

Technical SEO for Next.js: The Complete Developer's Guide

Next.js is inherently SEO-friendly — but still requires deliberate optimization. Here's how to implement every technical SEO requirement in Next.js 14 App Router.

DevForge Team

DevForge Team

AI Development Educators

Code editor showing Next.js configuration files for SEO optimization

Why Next.js and SEO Are a Natural Fit

Next.js has an inherent SEO advantage over traditional React applications: pages are server-rendered by default in the App Router, which means search engines receive fully-formed HTML rather than an empty shell waiting for JavaScript to hydrate.

But "server-rendered by default" doesn't mean "automatically SEO-optimized." There's a significant gap between a working Next.js application and a technically SEO-complete one. This guide closes that gap.

The generateMetadata() API

The App Router replaced the older Head component with a type-safe metadata system. Every page that should rank needs a proper generateMetadata() implementation.

Static metadata

tsx
// app/blog/[slug]/page.tsx
export const metadata = {
  title: 'Technical SEO for Next.js: The Complete Developer Guide | DevForge',
  description: 'Implement structured data, sitemaps, canonical tags, Core Web Vitals optimization, and Open Graph tags in Next.js 14 App Router. Step-by-step technical SEO guide.',
  openGraph: {
    title: 'Technical SEO for Next.js',
    description: 'Complete technical SEO guide for Next.js App Router.',
    images: [{ url: '/og/technical-seo-nextjs.png', width: 1200, height: 630 }],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'Technical SEO for Next.js',
  },
};

Dynamic metadata for content pages

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

  return {
    title: `${post.title} | DevForge Academy Blog`,
    description: post.excerpt,
    alternates: {
      canonical: `https://devforgeacademy.com/blog/${params.slug}`,
    },
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.date,
      authors: [post.author.name],
    },
  };
}

Key properties to always include:

  • title — unique per page, 50–60 characters, keyword-first
  • description — unique per page, 150–160 characters
  • alternates.canonical — prevents duplicate content penalties
  • openGraph — controls appearance when shared on social media

Implementing JSON-LD Structured Data

Structured data is the most underused SEO tool available to developers. It enables rich results in Google SERPs — FAQ dropdowns, breadcrumb trails, article bylines, and more.

Article schema for blog posts

tsx
// components/ArticleSchema.tsx
export function ArticleSchema({ post }: { post: BlogPost }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.excerpt,
    author: {
      '@type': 'Organization',
      name: post.author.name,
      url: 'https://devforgeacademy.com',
    },
    publisher: {
      '@type': 'Organization',
      name: 'DevForge Academy',
      logo: {
        '@type': 'ImageObject',
        url: 'https://devforgeacademy.com/logo.png',
      },
    },
    datePublished: post.date,
    dateModified: post.modified,
    image: post.featuredImage,
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': `https://devforgeacademy.com/blog/${post.slug}`,
    },
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

BreadcrumbList schema

tsx
export function BreadcrumbSchema({ items }: { items: { name: string; url: string }[] }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: items.map((item, index) => ({
      '@type': 'ListItem',
      position: index + 1,
      name: item.name,
      item: item.url,
    })),
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

FAQPage schema (triggers rich results)

tsx
export function FAQSchema({ faqs }: { faqs: { question: string; answer: string }[] }) {
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'FAQPage',
    mainEntity: faqs.map(faq => ({
      '@type': 'Question',
      name: faq.question,
      acceptedAnswer: {
        '@type': 'Answer',
        text: faq.answer,
      },
    })),
  };

  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Validate all structured data at Google's Rich Results Test before deploying.

Setting Up next-sitemap

Install and configure next-sitemap to auto-generate your sitemap and robots.txt:

bash
npm install next-sitemap
js
// next-sitemap.config.js
/** @type {import('next-sitemap').IConfig} */
module.exports = {
  siteUrl: process.env.SITE_URL || 'https://devforgeacademy.com',
  generateRobotsTxt: true,
  generateIndexSitemap: false,
  changefreq: 'weekly',
  priority: 0.7,
  transform: async (config, path) => {
    // Customize priority per path type
    const priority =
      path === '/' ? 1.0 :
      path.startsWith('/tutorials') ? 0.8 :
      path.startsWith('/blog') ? 0.7 :
      0.5;

    return {
      loc: path,
      changefreq: 'weekly',
      priority,
      lastmod: new Date().toISOString(),
    };
  },
  robotsTxtOptions: {
    policies: [
      { userAgent: '*', allow: '/' },
      { userAgent: '*', disallow: ['/admin', '/api'] },
    ],
  },
};

Add the postbuild script to your package.json:

json
{
  "scripts": {
    "build": "next build",
    "postbuild": "next-sitemap"
  }
}

Submit your sitemap at Google Search Console → Sitemaps.

Core Web Vitals Optimization

LCP: next/image with priority

The most common LCP element is a hero image or large featured image. Use next/image with the priority prop for above-the-fold images:

tsx
import Image from 'next/image';

export function HeroImage({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={1200}
      height={630}
      priority        // preloads the image — use only for above-the-fold
      className="w-full object-cover"
    />
  );
}

CLS: Explicit dimensions and font optimization

tsx
// app/layout.tsx — prevent font-related CLS
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',   // prevents invisible text during font load
});

INP: Route prefetching

Next.js Link components prefetch routes automatically — this significantly improves INP by preloading the next page before the user clicks.

Canonical Tags in Dynamic Routes

tsx
// app/tutorials/[subject]/[slug]/page.tsx
export async function generateMetadata({ params }) {
  return {
    alternates: {
      canonical: `https://devforgeacademy.com/tutorials/${params.subject}/${params.slug}`,
    },
  };
}

Verifying in Google Search Console

After deployment:

  1. Submit your sitemap URL at Search Console → Sitemaps
  2. Use the URL Inspection tool to test individual pages
  3. Check Core Web Vitals report (takes 28 days of data to populate)
  4. Monitor the Coverage report for crawl errors and indexing issues

Key Takeaways

  • Next.js App Router server-renders pages by default — a significant SEO foundation advantage
  • generateMetadata() with canonical, openGraph, and Twitter Card properties should be on every page
  • JSON-LD structured data (Article, BreadcrumbList, FAQPage) is underused and can unlock rich results
  • next-sitemap automates sitemap and robots.txt generation — submit to Search Console after every deploy
  • next/image with priority prevents the most common LCP issue; explicit font display settings prevent CLS
#SEO#Next.js#Technical SEO#Core Web Vitals#Structured Data