Back to Blog
Next.js
React
Technical SEO

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.

RankCLI TeamJanuary 22, 20269 min read

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

tsx
import 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:

js
module.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:

bash
npx 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.