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
- Next.js — current stable version (App Router recommended)
- TypeScript — strongly recommended
- Tailwind CSS — common styling choice
- Vercel — natural deployment platform (alternative: Netlify, Cloudflare, self-hosted)
- Sentry — error monitoring
- PostHog or similar — analytics with GA4 in parallel
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
- Client-side rendering for content — destroys SEO
- Missing Metadata API — relying on default page titles
- No schema implementation — missed rich results
- Standard img tags — losing next/image benefits
- Wrong rendering strategy — SSG for personalized; SSR for static
- No sitemap.ts — leaving sitemap to chance
- Excessive Client Components — bloating client bundle
- Missing Open Graph tags — poor social sharing
- No alternates.canonical — duplicate URL issues
- Hardcoded URLs everywhere — should use metadataBase
End of Framework Document
Companion documents:
framework-schema.md— Schema implementationframework-pageexperience.md— Performance metricsframework-imageseo.md— Image optimization principlesframework-international.md— i18n SEOframework-headless.md— Headless CMS with Next.js
Want this framework implemented on your site?
ThatDevPro ships these frameworks as productized services. SDVOSB-certified veteran owned. Cassville, Missouri.
See Engine Optimization service ›