seo-automation
Blog

Nextjs SEO: App Router Mistakes That Block Indexing

November 8, 2026

Nextjs SEO problems almost always come from a handful of App Router mistakes, not from the framework itself. If your pages are missing from Google, show the wrong title or get grouped under the wrong canonical, the cause is usually one of the eight issues below, and each one can be confirmed in a few minutes with curl and Search Console.

This guide works like a diagnosis: first the symptom you see, then the check that confirms it, then the fix. It covers the App Router on Next.js 14, 15 and 16.

If your setup is already clean and the real gap is content, SEO Automation's five AI agents find what your buyers search, write sourced articles and publish them to your GitHub repo. Start for $1 and get your SEO plan in a few minutes.

How to tell if your Next.js site has an SEO problem

Start in Search Console. Open the Page indexing report, then run URL Inspection on one affected page and look at the HTML Google actually crawled. Then compare it with what your server sends:

curl -s https://yoursite.com/blog/my-post | grep -iE "<title|<h1|canonical|noindex"

Match what you see with this table:

Symptom Most likely cause Section
Page crawled but not indexed, thin or empty HTML in curl Content fetched on the client Mistake 1
Title or description missing from the initial HTML Streamed metadata on a dynamic route Mistake 2
Same title on many pages, or Google picks a different canonical Inherited metadata, hard-coded canonical Mistake 3
Pages excluded by a noindex tag noindex left over from staging Mistake 4
New posts take a long time to be discovered Stale or bloated sitemap Mistake 5
Duplicate URLs with and without www or a trailing slash No single host and slash rule Mistake 6
Structured data warnings about dates Date without a timezone in JSON-LD Mistake 7
Poor Core Web Vitals on templates Too much client JavaScript, unsized images Mistake 8

Mistake 1: public pages rendered on the client

This is the most common cause of Next.js pages that don't get indexed. A 'use client' component fetches the post after load, so the server sends an empty shell.

Google can run JavaScript, but pages that need rendering wait in a queue. A page "may stay on this queue for a few seconds, but it can take longer than that". Google also says server-side or pre-rendering "is still a great idea because it makes your website faster for users and crawlers, and not all bots can run JavaScript" (Google Search Central, JavaScript SEO basics).

Check: if the curl above returns no H1 and no body text, you have this problem.

Fix: move the fetch into a Server Component and prerender the route.

// app/blog/[slug]/page.tsx
import { getPost, getAllPosts } from '@/lib/posts'

export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map((p) => ({ slug: p.slug }))
}

export const revalidate = 3600 // ISR: rebuilt in the background

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  )
}

Keep 'use client' for interactive pieces like a search box or a newsletter form, at the leaves of the tree. The revalidate option is documented in the route segment config.

Mistake 2: metadata that streams on dynamic routes

Since Next.js 15.2.0, metadata on dynamically rendered pages can stream. Next.js sends the page first and adds the tags later. For bots that only read HTML, Next.js waits for generateMetadata before responding. For bots that run JavaScript, the tags are added once they resolve (Next.js docs, generateMetadata).

The list of bots that get blocking metadata is set by htmlLimitedBots in next.config. Setting the option replaces the default list, and htmlLimitedBots: /.*/ turns streaming off for everyone (Next.js docs, htmlLimitedBots).

Check: run curl on a dynamic route. If <title> and the canonical are not in the <head>, they are streamed.

Fix: the clean option is to make ranked pages static or ISR, so their metadata sits in the first byte of HTML and streaming never applies. If a page has to stay dynamic, consider the htmlLimitedBots option and test again.

Mistake 3: duplicate titles and a canonical that points home

Two App Router behaviours cause most duplicate metadata.

  • Inheritance. A page that exports metadata without a title inherits title.default from the layout. Ten pages, one title.
  • Shallow merge. If a page defines openGraph, it replaces the layout's openGraph object completely. The same applies to alternates.

The worst version is a layout with alternates: { canonical: '/' }. Every child page then declares the homepage as its canonical, and Google may drop them as duplicates.

Fix: keep the canonical out of layouts and set it per page.

// app/layout.tsx
export const metadata = {
  metadataBase: new URL('https://example.com'),
  title: { default: 'Example', template: '%s | Example' },
}

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

metadataBase lets you use relative paths for canonicals, and title.template only applies to child segments, so the root page keeps the default.

Mistake 4: a noindex inherited from staging

A robots: { index: false } added to a layout during development applies to every page below it. It often ships to production unnoticed.

Check: curl returns noindex, or Search Console reports pages excluded by a noindex tag.

Fix: tie crawler rules to the environment, never to a value someone has to remember to change.

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

export default function robots(): MetadataRoute.Robots {
  const isProd = process.env.VERCEL_ENV === 'production'
  return {
    rules: isProd ? { userAgent: '*', allow: '/' } : { userAgent: '*', disallow: '/' },
    sitemap: 'https://example.com/sitemap.xml',
  }
}

Then search the codebase for index: false and keep it only on pages that really should stay out of search.

Mistake 5: a sitemap Google stops trusting

app/sitemap.ts generates the file for you (Next.js docs, sitemap). The mistake is what goes into it.

  • A fake lastModified. Setting new Date() on every entry tells Google everything changed today. Google's warning: if you say a page changed when it didn't, "eventually we're not going to believe you anymore" (Google Search Central blog). Use the content's real updatedAt.
  • Time spent on changeFrequency and priority. In the same post, Google says it "still doesn't use the changefreq or priority elements at all".
  • Drafts and noindex URLs. A URL listed in the sitemap but marked noindex sends two opposite signals. Filter them out.

Fixing the code is a one-time job. Keeping a Next.js blog fed with articles that match real searches is the part that never ends. See how SEO Automation's agents plan, write and publish for you, then Start for $1 and get your SEO plan in a few minutes.

Mistake 6: www, trailing slash and other duplicate URLs

If example.com/pricing, www.example.com/pricing and example.com/pricing/ all return 200, signals get split between them.

Fix: choose one host in your hosting dashboard and redirect the other. Choose one slash rule with trailingSlash in next.config (Next.js redirects the other form for you). Then make sure your canonicals and sitemap use exactly the same form.

Mistake 7: JSON-LD dates and escaping

Next.js has no special API for structured data. You render a <script type="application/ld+json"> from a Server Component (Next.js guide, JSON-LD). Two details trip people up:

const data = {
  '@context': 'https://schema.org',
  '@type': 'Article',
  headline: post.title,
  datePublished: '2026-11-08T09:00:00+00:00',
  dateModified: '2026-11-08T09:00:00+00:00',
}

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, '\\u003c') }}
/>
  • Dates need a time and a timezone. A bare 2026-11-08 is how you end up with timezone warnings in Search Console. See Google's Article structured data docs.
  • Escape < so CMS content can't close the script tag, as shown in the Next.js guide.

Use Article or FAQPage for content your team wrote, and never invent ratings or review counts.

Mistake 8: Core Web Vitals lost to client JavaScript

Google's targets, measured at the 75th percentile of page loads, are LCP within 2.5 seconds, INP of 200 milliseconds or less, and CLS of 0.1 or less (web.dev, Web Vitals).

On Next.js sites, the usual causes are:

  • Slow INP: a 'use client' at the top of a page hydrates the whole tree. Push it down to the components that need it.
  • Layout shift: images without dimensions. Use next/image with width and height, or fill inside a sized container.
  • Slow LCP: the hero image loads late. Add priority to it.
  • Text shift: web fonts swapping in. Load them with next/font.

Is Next.js bad for SEO?

No. Next.js gives you prerendering, a Metadata API, sitemap.ts and robots.ts out of the box. Sites run into Nextjs SEO problems when public pages are rendered on the client or when layouts pass the wrong metadata down. Once the eight issues above are fixed, the framework stops being the problem and content becomes the limit.

After the fix: get Google to recrawl

  1. In URL Inspection, test the live URL and request indexing for your key pages.
  2. Submit the sitemap again. The Search Console API accepts a PUT on https://www.googleapis.com/webmasters/v3/sites/{siteUrl}/sitemaps/{feedpath} (sitemaps.submit). The old anonymous ping endpoint is retired (Google Search Central blog).
  3. Notify Bing and other participating engines through IndexNow, with the key file in public/.
  4. Check the Page indexing report again over the next weeks. Google decides when to recrawl, and no fix guarantees a ranking.

Some things should stay with a person: which routes exist and which are canonical, any redirect plan during a migration, and a review of every article published under your brand. Google's spam policies target content made at scale mainly to manipulate rankings (Google spam policies).

How SEO Automation fits a Next.js site

SEO Automation publishes directly to GitHub repos, including Next.js, Astro and Hugo sites. After each publish, it resends the sitemap to Google through the Search Console and notifies IndexNow. Its agents find the real searches for your market, write researched articles with sources and check every fact and link. Articles on page 2 get reworked, and titles with a low CTR get rewritten. A human should still review every article before it goes live under your brand. Plan details are on the pricing page.

Start for $1 and get your SEO plan in a few minutes.

Frequently asked questions

Why is my Next.js site not indexed by Google?

The usual causes are content fetched on the client, a noindex inherited from a layout, or a canonical pointing to another page. Run curl on the URL, then URL Inspection in Search Console, and compare the HTML with the table at the top of this guide.

Does the App Router hurt Nextjs SEO?

No. The App Router adds the Metadata API, sitemap.ts and robots.ts. The things to watch are metadata streaming on dynamic routes since 15.2.0 and the shallow merge of metadata objects between layouts and pages.

Why does every page of my Next.js site have the same title?

Your pages export metadata without a title, so they inherit title.default from the root layout. Give each page its own title, or return one from generateMetadata.

Do I need SSR for SEO in Next.js?

Not always. Static generation and ISR also send full HTML to Google, and they are faster to serve. Keep SSR for pages that depend on the request, like search results.