·9 min read

7 SEO Mistakes Every Next.js App Makes (and How to Fix Them)

Next.js gives you all the tools for good SEO: server rendering, the Metadata API, sitemap generation, and more. But having the tools and using them correctly are two different things. These are the 7 mistakes we see on almost every Next.js site we scan, with copy-paste fixes for each one.

1. Missing canonical URLs on dynamic routes

The problem: Dynamic routes like /blog/[slug] or /report/[id] often skip the canonical tag. When the same page is accessible at multiple URLs (with or without trailing slash, with query params), search engines treat each variant as a separate page, splitting your ranking authority.

The impact: Duplicate content dilution. Your pages compete against themselves in search results.

Before (no canonical)

// No canonical:every URL variant is treated as a separate page
export const metadata: Metadata = {
  title: "My Blog Post",
  description: "A great article.",
}

After (canonical on every dynamic route)

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  return {
    title: post.title,
    description: post.excerpt,
    alternates: { canonical: `/blog/${slug}` },
  }
}

2. No sitemap.ts file

The problem: Many Next.js apps ship without a sitemap. Developers assume search engines will find all pages by crawling links. For new sites with few external links, this assumption is wrong.

The impact: Slower indexing. New pages may take weeks to appear in search results instead of days.

Create app/sitemap.ts

// app/sitemap.ts
import type { MetadataRoute } from "next"

const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "https://yoursite.com"

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    { url: baseUrl, changeFrequency: "monthly", priority: 1.0 },
    { url: `${baseUrl}/pricing`, changeFrequency: "monthly", priority: 0.8 },
    { url: `${baseUrl}/blog`, changeFrequency: "weekly", priority: 0.7 },
    // Add dynamic routes by fetching from your DB:
    // ...posts.map(p => ({ url: `${baseUrl}/blog/${p.slug}`, priority: 0.7 }))
  ]
}

After deploying, submit the URL in Google Search Console under Sitemaps.

3. Broken or missing Open Graph images

The problem: OG images are set to a relative path like /og-image.png but the file does not exist, or the image is the wrong size. Some developers set og:image to a localhost URL during development and forget to update it.

The impact: Blank previews on social media. Shared links look unprofessional and get fewer clicks.

Common problem

// Broken: relative URL without metadataBase, or file doesn't exist
openGraph: {
  images: [{ url: "/og.png" }], // returns 404
}

Fix: set metadataBase and verify the image exists

// app/layout.tsx:set metadataBase once
export const metadata: Metadata = {
  metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? "https://yoursite.com"),
  openGraph: {
    images: [{ url: "/og-image.png", width: 1200, height: 630, alt: "YourProduct" }],
  },
}

// Ensure public/og-image.png exists and is exactly 1200x630px

4. Client-side rendering on pages that need indexing

The problem: Using 'use client' at the top of a page component means the entire page renders in the browser. Search engine crawlers may not execute JavaScript, or may see a loading spinner instead of your content.

The impact: Pages may not be indexed at all. Even when they are, the crawled version may be incomplete.

Before (client-rendered pricing page)

"use client"

// This entire page renders in the browser:search engines may see nothing
export default function PricingPage() {
  const [plans, setPlans] = useState([])
  useEffect(() => { fetchPlans().then(setPlans) }, [])
  return <div>{plans.map(p => <PlanCard key={p.id} {...p} />)}</div>
}

After (server-rendered, indexable)

// app/pricing/page.tsx:server component by default
import { PlanCard } from "@/components/PlanCard"

const plans = [
  { id: "starter", name: "Starter", price: 19 },
  { id: "pro", name: "Pro", price: 29 },
]

export default function PricingPage() {
  return (
    <div>
      {plans.map(p => <PlanCard key={p.id} {...p} />)}
    </div>
  )
}

Keep 'use client' for interactive components (buttons, forms) but wrap them inside server component pages.

5. Missing robots.txt

The problem: Without a robots.txt, search engines have no guidance about which paths to crawl. They may index your dashboard, API routes, or auth pages.

The impact: Private routes appear in search results. Crawl budget is wasted on non-public pages.

Create app/robots.ts

// app/robots.ts
import type { MetadataRoute } from "next"

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: ["/dashboard/", "/api/", "/auth/"],
    },
    sitemap: `${process.env.NEXT_PUBLIC_SITE_URL}/sitemap.xml`,
  }
}

6. No JSON-LD structured data

The problem: Most Next.js apps ship with zero structured data. Search engines can still index the page, but they miss the explicit signals that trigger rich results (FAQ snippets, product cards, article dates).

The impact: No rich results in Google. Lower AI search citation probability. Competitors with structured data get preferential treatment.

Create a JsonLd component and add schemas to every page

// components/seo/JsonLd.tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  )
}

// Usage in any page:
const faqSchema = {
  "@context": "https://schema.org",
  "@type": "FAQPage",
  mainEntity: [{
    "@type": "Question",
    name: "What does this tool do?",
    acceptedAnswer: {
      "@type": "Answer",
      text: "It scans websites for SEO issues and returns fix instructions.",
    },
  }],
}

// In your JSX:
<JsonLd data={faqSchema} />

7. Wrong metadata template (or no template at all)

The problem: Without a title template, every page either shows just 'YourProduct' or just the page title without branding. Some apps accidentally set the same title on every page because they only define metadata in the root layout.

The impact: Duplicate titles across all pages. Lost branding. Search engines may derank pages with identical titles.

Before (same title on every page)

// app/layout.tsx:no template, just a static title
export const metadata: Metadata = {
  title: "YourProduct", // every page shows "YourProduct"
}

After (template in layout, unique title per page)

// app/layout.tsx:use a template
export const metadata: Metadata = {
  metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? "https://yoursite.com"),
  title: {
    default: "YourProduct",
    template: "%s | YourProduct",
  },
  description: "Default site description under 160 characters.",
}

// app/pricing/page.tsx:per-page title fills the template
export const metadata: Metadata = {
  title: "Pricing", // renders as "Pricing | YourProduct"
}

FAQ

Does Next.js handle SEO automatically?

No. Next.js provides the tools (Metadata API, sitemap.ts, robots.ts, server components) but does not configure them for you. Every page needs explicit metadata, canonical URLs, and structured data. Without these, a Next.js app has the same SEO gaps as any other framework.

Should I use generateMetadata or export const metadata?

Use export const metadata for static pages where the values are known at build time (pricing, about, blog index). Use generateMetadata for dynamic pages where metadata depends on fetched data (blog posts, user profiles, report pages). Both produce the same HTML output.

How do I test if my Next.js metadata is working?

View the page source (Ctrl+U or Cmd+Option+U) and search for your title, description, and og: tags. Use Google's Rich Results Test for JSON-LD validation. Use a tool like SEOLint to scan the deployed URL and catch issues across all pages at once.

Do I need a sitemap if my site only has 5 pages?

Yes. A sitemap takes 2 minutes to create and helps search engines discover your pages immediately after launch. Without one, Google has to find your pages by following links, which can take days or weeks. For a new site with no backlinks, a sitemap is essential.

Find all 7 mistakes in one scan

SEOLint checks for every issue in this article, plus 40 more. Paste your URL and get a full report with fix instructions.