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
AI Development Educators

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
// 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
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
// 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
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)
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:
npm install next-sitemap// 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:
{
"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:
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
// 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
// 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:
- Submit your sitemap URL at Search Console → Sitemaps
- Use the URL Inspection tool to test individual pages
- Check Core Web Vitals report (takes 28 days of data to populate)
- 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