SEO & AI Engine Optimization Framework · May 2026

Next.js SEO: App Router, Metadata API, ISR, Server Components

A comprehensive reference for Next.js SEO implementation. Next.js powers many modern marketing sites, e-commerce stores, and applications. Its hybrid rendering approach (server-side rendering, static…

App Router vs Pages Router, SSR/SSG/ISR Strategies, Metadata API, Schema Implementation, Performance Optimization, and the React Framework SEO Stack

A comprehensive reference for Next.js SEO implementation. Next.js powers many modern marketing sites, e-commerce stores, and applications. Its hybrid rendering approach (server-side rendering, static generation, incremental static regeneration) provides flexibility but requires SEO-specific decisions.


1. Document Purpose

Next.js is the dominant React framework for production sites in 2026. Vercel, the company behind Next.js, has continued rapid evolution: App Router became the standard, React Server Components matured, partial pre-rendering emerged, and the Metadata API replaced ad-hoc head management.

For SEO, Next.js requires understanding rendering strategies. Client-side React rendering (the original SPA pattern) is hostile to SEO. Server-rendered, statically-generated, or hybrid approaches preserve SEO while enabling React's component model. The choice affects everything from crawlability to performance to update workflows.

This framework specifies Next.js SEO implementation including App Router patterns, rendering strategy decisions, metadata management, and the standard production stack.

1.1 Required Tools


2. Rendering Strategy Decision

The single most important Next.js SEO decision: how does each page render?

2.1 Rendering Options

nextjs_rendering_strategies:
  
  static_site_generation_ssg:
    description: "Pre-rendered at build time"
    file_pattern: "Default behavior in App Router for static pages"
    performance: "Fastest; served as static HTML"
    seo: "Excellent — pre-rendered HTML"
    use_for:
      - Marketing pages
      - Documentation
      - Blog posts (revalidated periodically)
      - Most content
    drawback: "Requires rebuild for updates (unless ISR)"
  
  incremental_static_regeneration_isr:
    description: "Statically generated; regenerates after time interval or on demand"
    file_pattern: "export const revalidate = 60 (or any seconds)"
    performance: "Fast; cached + background regeneration"
    seo: "Excellent"
    use_for:
      - Content that updates periodically
      - Product pages
      - Blog with comment updates
    benefit: "Best of static + fresh content"
  
  server_side_rendering_ssr:
    description: "Rendered on each request"
    file_pattern: 'fetch with { cache: "no-store" } in App Router'
    performance: "Slower than static (server work per request)"
    seo: "Good — pre-rendered HTML"
    use_for:
      - Personalized content
      - User-specific pages
      - Real-time data
    consideration: "Server cost; performance"
  
  client_side_rendering_csr:
    description: "Rendered in browser via JavaScript"
    file_pattern: '"use client" + useEffect data fetching'
    performance: "Variable; dependent on client device"
    seo: "POOR — minimal HTML for crawlers"
    use_for:
      - Interactive components within otherwise rendered pages
      - User dashboards behind auth
    avoid_for: "Public-facing content"
  
  partial_prerendering:
    description: "Static shell + streaming dynamic parts"
    status: "Stable in Next.js 15+"
    benefit: "Fast initial render with dynamic content"
    seo: "Excellent for static parts"

2.2 Rendering Decision Matrix

                  | Updates Often? | Personalized? |  Strategy
Marketing pages   |      No        |      No       |  SSG
Blog posts        |    Sometimes   |      No       |  SSG with ISR
Product pages     |     Often      |      No       |  ISR (60-300s)
News articles     |    Constantly  |      No       |  ISR (10-60s)
User dashboard    |      n/a       |     Yes       |  SSR (or CSR + auth)
Search results    |    Per query   |      No       |  SSR
Real-time data    |    Per request |      No       |  SSR

2.3 App Router vs Pages Router

router_choice:
  
  app_router:
    status: "Default for new projects since Next.js 13.4"
    benefits:
      - React Server Components
      - Streaming
      - Layouts and templates
      - Better data fetching patterns
      - Metadata API
    learning_curve: "Steeper if from Pages Router"
    recommended: "All new projects"
  
  pages_router:
    status: "Maintained but legacy"
    benefits:
      - Stable; well-documented
      - Simpler mental model
    use_for: "Existing projects; not new development"

This framework focuses on App Router patterns.


3. Metadata API

Next.js 13+ App Router provides a structured Metadata API.

3.1 Static Metadata

For pages with consistent metadata:

// app/about/page.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'About ThatDeveloperGuy',
  description: 'Learn about Joseph W. Anady and ThatDeveloperGuy, a Service-Disabled Veteran-Owned Small Business based in Cassville, Missouri.',
  keywords: ['about us', 'web developer', 'SDVOSB', 'Cassville Missouri'],
  authors: [{ name: 'Joseph W. Anady' }],
  openGraph: {
    title: 'About ThatDeveloperGuy',
    description: 'Service-Disabled Veteran-Owned Small Business based in Cassville, Missouri.',
    url: 'https://thatdeveloperguy.com/about/',
    siteName: 'ThatDeveloperGuy',
    images: [
      {
        url: 'https://thatdeveloperguy.com/og-about.jpg',
        width: 1200,
        height: 630,
        alt: 'Joseph W. Anady at ThatDeveloperGuy office',
      },
    ],
    locale: 'en_US',
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'About ThatDeveloperGuy',
    description: 'Service-Disabled Veteran-Owned Small Business based in Cassville, Missouri.',
    images: ['https://thatdeveloperguy.com/og-about.jpg'],
  },
  alternates: {
    canonical: 'https://thatdeveloperguy.com/about/',
  },
};

export default function AboutPage() {
  return (
    <main>
      {/* Page content */}
    </main>
  );
}

3.2 Dynamic Metadata

For dynamic routes (e.g., blog posts):

// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getPost } from '@/lib/posts';

interface Props {
  params: { slug: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug);
  
  if (!post) {
    return { title: 'Not Found' };
  }
  
  return {
    title: post.title,
    description: post.excerpt,
    authors: [{ name: post.author.name, url: post.author.url }],
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url: `https://thatdeveloperguy.com/blog/${params.slug}/`,
      type: 'article',
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.author.name],
      tags: post.tags,
      images: [post.featuredImage],
    },
    alternates: {
      canonical: `https://thatdeveloperguy.com/blog/${params.slug}/`,
    },
  };
}

export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug);
  if (!post) notFound();
  
  return (
    <article>
      {/* Post content */}
    </article>
  );
}

3.3 Root Metadata

Set sitewide defaults in root layout:

// app/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  metadataBase: new URL('https://thatdeveloperguy.com'),
  title: {
    template: '%s | ThatDeveloperGuy',
    default: 'ThatDeveloperGuy — Web Development & AI Search Optimization',
  },
  description: 'Service-Disabled Veteran-Owned Small Business providing web development, SEO, AI search optimization, and computer repair services from Cassville, Missouri.',
  applicationName: 'ThatDeveloperGuy',
  generator: 'Next.js',
  keywords: ['web development', 'SEO', 'AI search optimization', 'SDVOSB'],
  referrer: 'origin-when-cross-origin',
  authors: [{ name: 'Joseph W. Anady', url: 'https://thatdeveloperguy.com/about/' }],
  creator: 'Joseph W. Anady',
  publisher: 'ThatDeveloperGuy',
  formatDetection: {
    email: false,
    address: false,
    telephone: false,
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

4. Schema Implementation

Next.js doesn't have built-in schema management; implement directly.

4.1 Schema Component Pattern

// components/Schema.tsx
interface SchemaProps {
  data: object;
}

export function Schema({ data }: SchemaProps) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}

4.2 Per-Page Schema Implementation

// app/blog/[slug]/page.tsx
import { Schema } from '@/components/Schema';

export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug);
  
  const schema = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    '@id': `https://thatdeveloperguy.com/blog/${params.slug}/#article`,
    headline: post.title,
    description: post.excerpt,
    image: post.featuredImage,
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: {
      '@type': 'Person',
      '@id': 'https://thatdeveloperguy.com/about/joseph-anady/#person',
      name: 'Joseph W. Anady',
      url: 'https://thatdeveloperguy.com/about/joseph-anady/',
    },
    publisher: {
      '@type': 'Organization',
      '@id': 'https://thatdeveloperguy.com/#organization',
    },
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': `https://thatdeveloperguy.com/blog/${params.slug}/`,
    },
  };
  
  return (
    <>
      <Schema data={schema} />
      <article>{/* Post content */}</article>
    </>
  );
}

4.3 Sitewide Schema (Organization)

// app/layout.tsx
const organizationSchema = {
  '@context': 'https://schema.org',
  '@type': 'ProfessionalService',
  '@id': 'https://thatdeveloperguy.com/#organization',
  name: 'ThatDeveloperGuy',
  url: 'https://thatdeveloperguy.com/',
  logo: 'https://thatdeveloperguy.com/logo.png',
  // ... full schema
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Schema data={organizationSchema} />
        {children}
      </body>
    </html>
  );
}

5. Sitemap and Robots

5.1 Sitemap Generation

Next.js 13+ supports sitemap.ts file:

// app/sitemap.ts
import { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/posts';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();
  
  const staticPages = [
    {
      url: 'https://thatdeveloperguy.com/',
      lastModified: new Date(),
      changeFrequency: 'weekly' as const,
      priority: 1.0,
    },
    {
      url: 'https://thatdeveloperguy.com/about/',
      lastModified: new Date(),
      changeFrequency: 'monthly' as const,
      priority: 0.8,
    },
    {
      url: 'https://thatdeveloperguy.com/services/',
      lastModified: new Date(),
      changeFrequency: 'monthly' as const,
      priority: 0.9,
    },
  ];
  
  const blogPages = posts.map((post) => ({
    url: `https://thatdeveloperguy.com/blog/${post.slug}/`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'monthly' as const,
    priority: 0.7,
  }));
  
  return [...staticPages, ...blogPages];
}

For very large sites with multiple sitemaps:

// app/sitemap.xml/route.ts (sitemap index)
export async function GET() {
  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>https://example.com/sitemap-pages.xml</loc></sitemap>
  <sitemap><loc>https://example.com/sitemap-posts.xml</loc></sitemap>
  <sitemap><loc>https://example.com/sitemap-products.xml</loc></sitemap>
</sitemapindex>`;
  
  return new Response(sitemap, {
    headers: { 'Content-Type': 'application/xml' },
  });
}

5.2 Robots.txt

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

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/admin/', '/api/', '/private/'],
      },
      {
        userAgent: 'GPTBot',
        allow: '/',
      },
      {
        userAgent: 'ClaudeBot',
        allow: '/',
      },
    ],
    sitemap: 'https://thatdeveloperguy.com/sitemap.xml',
  };
}

6. Image Optimization

Next.js Image component is one of its strongest SEO features.

6.1 Image Component Usage

import Image from 'next/image';

export function Hero() {
  return (
    <section>
      <Image
        src="/hero.jpg"
        alt="ThatDeveloperGuy founder Joseph Anady at Cassville office"
        width={1600}
        height={900}
        priority
        sizes="100vw"
      />
    </section>
  );
}

6.2 Image Configuration

// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
      },
    ],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 31536000,
  },
};

6.3 Image Patterns

// Below-fold image — lazy loaded by default
<Image src="/feature.jpg" alt="Feature description" width={800} height={600} />

// Above-fold image — eager loaded with priority
<Image src="/hero.jpg" alt="Hero" width={1600} height={900} priority />

// Responsive image with multiple sizes
<Image
  src="/responsive.jpg"
  alt="Responsive image"
  width={1600}
  height={900}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>

// Fill container
<div style={{ position: 'relative', height: '400px' }}>
  <Image src="/fill.jpg" alt="Filled image" fill style={{ objectFit: 'cover' }} />
</div>

7. Performance Optimization

7.1 Core Performance Patterns

nextjs_performance_patterns:
  
  default_static:
    rule: "Static is fastest"
    pattern: "Default to SSG; opt into dynamic when needed"
  
  appropriate_caching:
    fetch_caching:
      static: "fetch(url) — cached by default in App Router"
      revalidate: "fetch(url, { next: { revalidate: 60 } })"
      no_cache: "fetch(url, { cache: 'no-store' })"
  
  streaming:
    pattern: "Use loading.tsx and Suspense for progressive rendering"
    benefit: "Time to first byte improvement"
  
  partial_prerendering:
    when_stable: "Static shell + streaming dynamic"
    benefit: "Fast initial render + dynamic content"
  
  bundle_optimization:
    next_dynamic: "Dynamic imports for code splitting"
    server_components: "Reduce client bundle by keeping components server-only"
    
  fonts:
    use: "next/font for automatic optimization"
    benefits: "Self-hosted; no layout shift; preloaded"
    example: |
      import { Inter } from 'next/font/google';
      const inter = Inter({ subsets: ['latin'] });
  
  third_party_scripts:
    use: "next/script for optimization"
    strategies: ['beforeInteractive', 'afterInteractive', 'lazyOnload']
    example: |
      <Script
        src="https://www.googletagmanager.com/gtag/js?id=G-XXX"
        strategy="afterInteractive"
      />

7.2 Server Components vs Client Components

component_decisions:
  
  server_components_default:
    description: "App Router defaults all components to Server Components"
    benefits:
      - Render on server
      - Zero JavaScript to client
      - Direct database/API access
      - Better performance
    use_for: "Most components, data fetching, content"
  
  client_components:
    marker: '"use client" directive at top of file'
    when_required:
      - useState, useEffect, other hooks
      - onClick, onChange, other event handlers
      - Browser-only APIs
    pattern: "Push 'use client' as deep as possible in tree"
  
  pattern_combinations:
    - Server Component fetches data
    - Passes data as props to Client Component
    - Client Component handles interactivity

8. Hosting and Deployment

8.1 Hosting Options

nextjs_hosting:
  
  vercel:
    description: "Built by Next.js team"
    pros:
      - Optimized for Next.js
      - Easy deployment
      - Edge functions
      - Built-in analytics
      - ISR support out of box
    cons:
      - Cost at scale
      - Vendor lock-in concerns
    best_for: "Most Next.js sites"
  
  netlify:
    description: "Strong static + serverless"
    pros: "Good Next.js support; competitive pricing"
    cons: "Some Next.js features less optimized"
    best_for: "Static-heavy sites"
  
  cloudflare_pages:
    description: "Cloudflare's hosting"
    pros: "Global edge; free tier generous"
    cons: "Some Next.js features partial support"
    best_for: "Static sites; Cloudflare-integrated stacks"
  
  self_hosted:
    pattern: "Node.js + reverse proxy (Nginx)"
    pros: "Full control; predictable cost"
    cons: "More operational overhead"
    best_for: "Joseph's portfolio if scaling Next.js sites"

8.2 Joseph's Self-Hosted Pattern

For Next.js sites on Joseph's Bubbles infrastructure:

# Production build
npm run build

# Start with PM2 for process management
pm2 start npm --name "site-name" -- start

# Nginx reverse proxy
server {
    listen 443 ssl http2;
    server_name nextjs-site.example.com;
    
    ssl_certificate /etc/letsencrypt/live/nextjs-site.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/nextjs-site.example.com/privkey.pem;
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
    
    # Static assets cache
    location /_next/static {
        proxy_cache_valid 200 1y;
        proxy_pass http://localhost:3000;
    }
}

9. Common Next.js SEO Patterns

9.1 Pagination

// app/blog/page/[number]/page.tsx
export async function generateStaticParams() {
  const totalPosts = await getPostCount();
  const totalPages = Math.ceil(totalPosts / 10);
  
  return Array.from({ length: totalPages }, (_, i) => ({
    number: String(i + 1),
  }));
}

export default async function BlogPage({ params }) {
  const posts = await getPostsForPage(parseInt(params.number));
  return (
    <main>
      {posts.map(post => <ArticleCard key={post.id} post={post} />)}
      <Pagination current={params.number} total={totalPages} />
    </main>
  );
}

9.2 Internationalization (i18n)

// app/[locale]/layout.tsx
export async function generateStaticParams() {
  return [
    { locale: 'en' },
    { locale: 'es' },
    { locale: 'fr' },
  ];
}

export default function LocaleLayout({ children, params }) {
  return (
    <html lang={params.locale}>
      <body>{children}</body>
    </html>
  );
}

Add hreflang via Metadata API:

export const metadata: Metadata = {
  alternates: {
    canonical: 'https://example.com/en/about/',
    languages: {
      'en-US': 'https://example.com/en/about/',
      'es-ES': 'https://example.com/es/about/',
      'fr-FR': 'https://example.com/fr/about/',
    },
  },
};

9.3 Redirects

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/old-path',
        destination: '/new-path',
        permanent: true, // 308 (perm) or false for 307 (temp)
      },
      {
        source: '/blog/old-slug',
        destination: '/blog/new-slug',
        permanent: true,
      },
    ];
  },
};

For dynamic redirects, use middleware:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  const redirect = await getRedirectFor(request.nextUrl.pathname);
  if (redirect) {
    return NextResponse.redirect(new URL(redirect, request.url), 301);
  }
  return NextResponse.next();
}

9.4 404 and Error Pages

// app/not-found.tsx
export default function NotFound() {
  return (
    <main>
      <h1>404 — Page Not Found</h1>
      <p>The page you're looking for doesn't exist.</p>
      <a href="/">Return home</a>
    </main>
  );
}

// app/error.tsx
'use client';

export default function Error({ error, reset }) {
  return (
    <main>
      <h1>Something went wrong</h1>
      <button onClick={reset}>Try again</button>
    </main>
  );
}

10. Audit Mode

# Criterion Pass/Fail
NJ1 App Router used (not legacy Pages Router)
NJ2 Appropriate rendering strategy per page (SSG/ISR/SSR)
NJ3 Metadata API used for all pages
NJ4 Schema implemented per page type
NJ5 Sitemap.ts generates comprehensive sitemap
NJ6 Robots.ts configured
NJ7 next/image used for all images
NJ8 next/font used for fonts
NJ9 next/script with appropriate strategy
NJ10 Server Components used where possible
NJ11 Client Components only where required
NJ12 Performance baselines met (CWV)
NJ13 Canonical URLs set via Metadata API
NJ14 Open Graph and Twitter Card metadata complete
NJ15 Internationalization implemented (if applicable)
NJ16 Redirects configured properly

Score: 16. World-class Next.js implementation: 14+/16.


11. Common Mistakes

  1. Client-side rendering for content — destroys SEO
  2. Missing Metadata API — relying on default page titles
  3. No schema implementation — missed rich results
  4. Standard img tags — losing next/image benefits
  5. Wrong rendering strategy — SSG for personalized; SSR for static
  6. No sitemap.ts — leaving sitemap to chance
  7. Excessive Client Components — bloating client bundle
  8. Missing Open Graph tags — poor social sharing
  9. No alternates.canonical — duplicate URL issues
  10. Hardcoded URLs everywhere — should use metadataBase

End of Framework Document

Companion documents:

Want this framework implemented on your site?

ThatDevPro ships these frameworks as productized services. SDVOSB-certified veteran owned. Cassville, Missouri.

See Engine Optimization service ›