Next.js SEO Checklist for Production Websites
Most Next.js sites fail at SEO. Not because of content—but because of wrong setup. This checklist fixes that.
Last updated: Feb 12, 2026
Disclosure: Some links in this post are affiliate links. I may earn a commission at no extra cost to you if you sign up or buy through them. I only recommend tools I use and trust.
Introduction
Most Next.js sites fail at SEO.
Not because of content—but because of wrong setup.
This Next.js SEO checklist fixes that. Use it to ensure your production site is optimized for indexing, speed, schema, and rankings. For hosting, I deploy on Vercel (see my Vercel review)—zero config and great for Next.js SEO.
1. Rendering Setup
In Next.js App Router, use Server Components by default. They render on the server, making content immediately available to crawlers.
✅ Use Server Components (Default)
// app/blog/page.tsx - Server Component (default)
export default async function BlogPage() {
const posts = await getPosts() // Server-side data fetch
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}Why: Server Components render HTML on the server. Google crawlers see fully rendered content immediately. No JavaScript required.
❌ Avoid Client Components for SEO Content
// ❌ BAD: Client Component for content
'use client'
export default function BlogPage() {
const [posts, setPosts] = useState([])
useEffect(() => {
fetch('/api/posts').then(res => res.json()).then(setPosts)
}, [])
// Crawlers won't see this content!
}Problem: Client Components require JavaScript. Crawlers may not execute JS, so content won't be indexed.
✅ Use ISR for Dynamic Content
// app/products/[id]/page.tsx
export const revalidate = 3600 // Revalidate every hour
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return <ProductDetail product={product} />
}Why: Incremental Static Regeneration (ISR) pre-renders pages at build time and regenerates them on demand. Best of both worlds: fast static pages + fresh content.
2. Metadata
Export metadata from your page.tsx or layout.tsx. Next.js App Router uses the Metadata API.
✅ Static Metadata
// app/blog/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Blog | My Site',
description: 'Read our latest blog posts',
alternates: {
canonical: 'https://www.mysite.com/blog',
},
openGraph: {
title: 'Blog | My Site',
description: 'Read our latest blog posts',
url: 'https://www.mysite.com/blog',
siteName: 'My Site',
type: 'website',
images: [{ url: 'https://www.mysite.com/og-image.jpg' }],
},
twitter: {
card: 'summary_large_image',
title: 'Blog | My Site',
description: 'Read our latest blog posts',
images: ['https://www.mysite.com/og-image.jpg'],
},
robots: {
index: true,
follow: true,
},
}
export default function BlogPage() {
return <div>...</div>
}✅ Dynamic Metadata
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title + ' | My Site',
description: post.excerpt,
alternates: {
canonical: 'https://www.mysite.com/blog/' + params.slug,
},
openGraph: {
title: post.title,
description: post.excerpt,
url: 'https://www.mysite.com/blog/' + params.slug,
type: 'article',
publishedTime: post.publishedAt,
images: [post.ogImage],
},
}
}Key points:
- Always include
alternates.canonicalwith full URL (www domain) - Set
robots: { index: true, follow: true }unless page should be excluded - Include OpenGraph and Twitter Card for social sharing
- Use
generateMetadatafor dynamic routes
✅ Root Layout Metadata
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://www.mysite.com'),
title: {
default: 'My Site',
template: '%s | My Site',
},
description: 'Default site description',
openGraph: {
type: 'website',
locale: 'en_US',
siteName: 'My Site',
},
}Why: metadataBase sets the base URL for relative URLs. title.template allows child pages to inherit the site name.
3. Sitemap
Create app/sitemap.ts. Next.js serves it at /sitemap.xml automatically.
✅ Static Sitemap
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://www.mysite.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
{
url: 'https://www.mysite.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
},
]
}✅ Dynamic Sitemap
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts()
const postEntries = posts.map((post) => ({
url: 'https://www.mysite.com/blog/' + post.slug,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
return [
{
url: 'https://www.mysite.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...postEntries,
]
}Best practices:
- Use absolute URLs with www domain
- Set
lastModifiedto actual update dates - Use appropriate
changeFrequency(daily, weekly, monthly) - Set
priority0.0–1.0 (homepage = 1.0)
✅ Robots.txt
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/'],
},
],
sitemap: 'https://www.mysite.com/sitemap.xml',
}
}Next.js serves this at /robots.txt. Always include your sitemap URL.
4. Schema (Structured Data)
Add JSON-LD structured data for rich results. Use script tags with type="application/ld+json".
✅ Article Schema
// app/blog/[slug]/layout.tsx
export default function BlogPostLayout({ children }) {
const articleSchema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: 'Post Title',
description: 'Post description',
author: {
'@type': 'Person',
name: 'Author Name',
},
datePublished: '2026-02-12',
dateModified: '2026-02-12',
image: 'https://www.mysite.com/post-image.jpg',
}
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
/>
{children}
</>
)
}✅ Breadcrumb Schema
const breadcrumbSchema = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://www.mysite.com',
},
{
'@type': 'ListItem',
position: 2,
name: 'Blog',
item: 'https://www.mysite.com/blog',
},
{
'@type': 'ListItem',
position: 3,
name: 'Post Title',
item: 'https://www.mysite.com/blog/post-slug',
},
],
}✅ FAQ Schema
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: 'What is Next.js?',
acceptedAnswer: {
'@type': 'Answer',
text: 'Next.js is a React framework for production.',
},
},
],
}Common schemas: Article, BlogPosting, BreadcrumbList, FAQPage, Organization, Person, WebSite.
Validate schemas using Google Rich Results Test.
5. Speed
Fast sites rank better. Optimize Core Web Vitals: LCP, FID/INP, CLS.
✅ Use next/image
import Image from 'next/image'
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={630}
priority // For above-the-fold images
quality={85}
placeholder="blur"
/>Why: Automatic WebP/AVIF conversion, lazy loading, responsive sizes, and optimized delivery.
✅ Optimize Fonts
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
{children}
</html>
)
}Why: Next.js automatically optimizes fonts, self-hosts them, and eliminates layout shift.
✅ Code Splitting
// Dynamic imports for heavy components
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // Only if component requires browser APIs
})✅ Script Optimization
import Script from 'next/script' <Script src="https://analytics.example.com/script.js" strategy="afterInteractive" // or "lazyOnload" />
Strategies: afterInteractive (default), lazyOnload (after page load), beforeInteractive (critical only).
6. Indexing
Ensure Google can crawl and index your site.
✅ Verify robots.txt
Check https://www.mysite.com/robots.txt. Ensure important pages aren't disallowed.
✅ Submit Sitemap
Submit https://www.mysite.com/sitemap.xml in Google Search Console.
✅ Check for noindex
// ❌ BAD: Don't do this unless page should be excluded
export const metadata = {
robots: {
index: false, // Page won't be indexed!
},
}Only set index: false for pages that shouldn't be indexed (admin, drafts, etc.).
✅ Use Canonical URLs
Always set canonical URLs to prevent duplicate content issues. Use www domain consistently.
✅ Test Rendering
Use Google Rich Results Test and URL Inspection Tool to verify pages render correctly for Google.
7. Common Bugs
❌ Missing Metadata Export
// ❌ BAD: No metadata export
export default function Page() {
return <div>Content</div>
}
// ✅ GOOD: Export metadata
export const metadata = {
title: 'Page Title',
description: 'Page description',
}❌ Relative Canonical URLs
// ❌ BAD
alternates: {
canonical: '/blog', // Relative URL
}
// ✅ GOOD
alternates: {
canonical: 'https://www.mysite.com/blog', // Absolute URL
}❌ Client-Side Content
// ❌ BAD: Content in Client Component
'use client'
export default function Page() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data').then(res => res.json()).then(setData)
}, [])
return <div>{data}</div> // Crawlers won't see this
}
// ✅ GOOD: Server Component
export default async function Page() {
const data = await getData() // Server-side fetch
return <div>{data}</div> // Crawlers see this
}❌ Missing Alt Text
// ❌ BAD
<Image src="/image.jpg" width={500} height={300} />
// ✅ GOOD
<Image
src="/image.jpg"
alt="Descriptive alt text for SEO"
width={500}
height={300}
/>❌ Duplicate Canonical Tags
Don't add canonical tags manually if you're using Next.js Metadata API. The API handles it automatically.
8. Tools
Use these tools to audit and monitor your Next.js SEO optimization:
- Google Search Console — Monitor indexing, search performance, and issues
- Google Rich Results Test — Validate structured data
- PageSpeed Insights — Test Core Web Vitals and performance
- Lighthouse — Audit SEO, performance, accessibility
- Vercel Analytics — Real user metrics (RUM) for production sites
For more tools, see the SEO tools and resources page.
Hosting & deploy for Next.js SEO (quick comparison)
Vercel Deployment Best Practices
When deploying to Vercel:
- Use
wwwsubdomain consistently (set in Vercel project settings) - Enable
x-robots-tagheaders if needed (via Vercel config) - Monitor Core Web Vitals in Vercel Analytics
- Use Edge Functions for API routes that need low latency
- Enable ISR for dynamic content that changes infrequently
Best hosting for Next.js SEO
For Next.js, Vercel is the default choice: same team as Next.js, automatic optimizations, and Analytics for Core Web Vitals. I’ve written a full Vercel review with pros, cons, and when to choose it.
Is Vercel worth it in 2026?
For production Next.js sites, yes. Free tier is enough for small projects; Pro when you need more bandwidth and analytics. No server config—deploy and your SEO setup (sitemap, metadata, server rendering) just works.
👉 If you want to deploy your Next.js site with zero SEO headaches, I personally recommend Vercel — start free → see resources.
Frequently Asked Questions
What are the most common Next.js SEO mistakes?
Common mistakes include: missing metadata exports, incorrect canonical URLs, no sitemap.xml, missing structured data, client-side rendering for SEO-critical content, and not using next/image for images. This checklist covers all of these.
How do I set up metadata in Next.js App Router?
Use the Metadata API by exporting a metadata object or generateMetadata function from your page.tsx or layout.tsx. Include title, description, canonical URL, OpenGraph tags, and Twitter Card. See the Metadata section in this checklist for code examples.
Do I need a sitemap for Next.js SEO?
Yes. Create a sitemap.ts file in your app directory that exports a sitemap function returning an array of URLs with lastModified, changeFrequency, and priority. Next.js will serve it at /sitemap.xml automatically.
How do I add structured data (schema.org) to Next.js?
Use JSON-LD script tags in your layout or page components. Create schema objects for Article, BreadcrumbList, FAQPage, etc., and render them using dangerouslySetInnerHTML in a script tag with type="application/ld+json". See the Schema section for examples.
What rendering method is best for Next.js SEO?
Server-side rendering (SSR) or Static Site Generation (SSG) are best for SEO. Use Server Components by default in App Router. Avoid client-side rendering for content that needs to be indexed. Use ISR (Incremental Static Regeneration) for dynamic content that needs frequent updates.
Want More SEO Resources?
Check out the SEO for Developers guide, performance optimization case study, and SEO tools and resources.
👉 Deploy your Next.js site with zero config → Vercel review (free tier available).
Key Takeaways
- •Use Server Components by default in App Router—avoid client-side rendering for SEO-critical content.
- •Export metadata objects from page.tsx/layout.tsx. Include title, description, canonical, OpenGraph, and Twitter Card.
- •Create sitemap.ts and robots.ts in app directory. Next.js serves them automatically at /sitemap.xml and /robots.txt.
- •Add JSON-LD structured data for Article, BreadcrumbList, FAQPage. Use script tags with type="application/ld+json".
Related Guides
Want more articles like this?
Subscribe to get practical guides and case studies delivered to your inbox. No spam, just real systems that work.