Next.js SEO Guide: 7 Mistakes Even Experienced Developers Make
Next.js has great SEO out of the box, but it's easy to mess up. Learn the 7 most common SEO mistakes in Next.js apps and how to avoid them.
Next.js is the most SEO-friendly React framework. It gives you SSR, SSG, ISR, and the new App Router with built-in metadata support.
But even experienced developers make mistakes that hurt their SEO.
Here are 7 common ones, and how to fix them.
Mistake 1: Not Using the Metadata API (App Router)
The Problem
With the App Router, many developers still manually add meta tags instead of using the built-in Metadata API.
The Fix
tsx// app/page.tsx import type { Metadata } from 'next'; export const metadata: Metadata = { title: 'Home | MyApp', description: 'The best app for doing things', openGraph: { title: 'Home | MyApp', description: 'The best app for doing things', images: ['/og-home.png'], }, }; export default function Home() { return <main>...</main>; }
For dynamic pages:
tsx// 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, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [post.image], }, }; }
Mistake 2: Forgetting generateStaticParams
The Problem
Dynamic routes like /blog/[slug] aren't statically generated unless you tell Next.js about them.
The Fix
tsx// app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await getAllPosts(); return posts.map((post) => ({ slug: post.slug, })); }
This ensures all blog posts are pre-rendered at build time.
Mistake 3: Using Client Components for SEO-Critical Content
The Problem
Adding 'use client' to a component means it renders on the client. Meta tags in client components may not be picked up by crawlers.
The Fix
Keep SEO-critical content in Server Components:
tsx// app/product/[id]/page.tsx (Server Component - no 'use client') import { ProductDetails } from './ProductDetails'; // Client Component for interactivity export async function generateMetadata({ params }) { const product = await getProduct(params.id); return { title: product.name, description: product.description, }; } export default async function ProductPage({ params }) { const product = await getProduct(params.id); return ( <main> <h1>{product.name}</h1> <p>{product.description}</p> <ProductDetails product={product} /> {/* Interactive parts */} </main> ); }
Mistake 4: Not Setting Up next/image Properly
The Problem
Using regular <img> tags or misconfiguring next/image.
The Fix
tsximport Image from 'next/image'; // Good - with proper sizing <Image src="/hero.jpg" alt="Hero image showing our product" width={1200} height={630} priority // For above-the-fold images /> // For responsive images <Image src="/hero.jpg" alt="Hero image" fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover" />
Configure remote images in next.config.js:
jsmodule.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.example.com', }, ], }, };
Mistake 5: Missing robots.txt and sitemap.xml
The Problem
Next.js doesn't create these automatically.
The Fix
tsx// app/robots.ts import { MetadataRoute } from 'next'; export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: '*', allow: '/', disallow: ['/dashboard/', '/api/'], }, sitemap: 'https://yoursite.com/sitemap.xml', }; }
tsx// app/sitemap.ts import { MetadataRoute } from 'next'; export default async function sitemap(): MetadataRoute.Sitemap { const posts = await getAllPosts(); const blogUrls = posts.map((post) => ({ url: `https://yoursite.com/blog/${post.slug}`, lastModified: post.updatedAt, changeFrequency: 'monthly' as const, priority: 0.7, })); return [ { url: 'https://yoursite.com', lastModified: new Date(), changeFrequency: 'weekly', priority: 1, }, { url: 'https://yoursite.com/pricing', lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8, }, ...blogUrls, ]; }
Mistake 6: Not Adding Structured Data
The Problem
Missing JSON-LD means missing rich results in search.
The Fix
tsx// app/blog/[slug]/page.tsx export default async function BlogPost({ params }) { const post = await getPost(params.slug); const jsonLd = { '@context': 'https://schema.org', '@type': 'BlogPosting', headline: post.title, description: post.excerpt, author: { '@type': 'Person', name: post.author, }, datePublished: post.publishedAt, dateModified: post.updatedAt, image: post.image, }; return ( <> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> <article> <h1>{post.title}</h1> {/* ... */} </article> </> ); }
Mistake 7: Ignoring Core Web Vitals
The Problem
Next.js is fast, but lazy loading everything or using heavy libraries can hurt LCP and CLS.
The Fix
tsx// Preload critical resources <link rel="preload" href="/fonts/inter.woff2" as="font" crossOrigin="anonymous" /> // Use next/dynamic for heavy components import dynamic from 'next/dynamic'; const HeavyChart = dynamic(() => import('./Chart'), { loading: () => <ChartSkeleton />, ssr: false, // If not needed for SEO }); // Add priority to above-fold images <Image src="/hero.jpg" priority alt="..." /> // Avoid layout shift with explicit dimensions <div style={{ aspectRatio: '16/9' }}> <Image src="/video-thumb.jpg" fill alt="..." /> </div>
Quick Audit Checklist
Before you ship, verify:
- Every page has unique title and description via Metadata API
- Dynamic pages use generateMetadata
- Blog/product pages use generateStaticParams
- robots.ts and sitemap.ts exist
- JSON-LD on key pages (blog, products, FAQ)
- Images use next/image with proper sizing
- Above-fold images have priority prop
- No SEO-critical content in 'use client' components
Automated Checking
Run this on every deploy:
bashnpx rankcli audit --url https://your-nextjs-site.com
Or add to your build:
json{ "scripts": { "build": "next build", "postbuild": "rankcli audit --url $DEPLOY_URL --output json" } }
For CI/CD integration with error checking, use the JSON output and parse the result:
bash# In your CI pipeline RESULT=$(rankcli audit --url $DEPLOY_URL --output json) ERRORS=$(echo $RESULT | jq '.issues | map(select(.severity == "error")) | length') if [ "$ERRORS" -gt 0 ]; then echo "Found $ERRORS critical SEO errors" exit 1 fi
RankCLI detects all these Next.js SEO issues automatically. Try the free audit.
Try RankCLI
Catch SEO issues before they hurt your rankings. Run your first audit in seconds.