The #1 Next.js indexing bug: root alternates.canonical inheritance
In Next.js App Router, developers often set metadataBase: new URL("https://example.com") and alternates: { canonical: "/" } inside the root app/layout.tsx, assuming Next.js will automatically set each child page’s canonical to its own pathname. It does not.
If a child page (`app/blog/[slug]/page.tsx`) defines title and description in generateMetadata without overriding alternates.canonical, Next.js shallow-merges or inherits the parent layout’s alternates object—or omits the canonical altogether depending on your layout tree. When child pages inherit <link rel="canonical" href="https://example.com/">, you are explicitly instructing Google that every blog post on your site is a duplicate of your homepage.
// ❌ BEFORE: Missing alternates.canonical in dynamic route metadata
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) return {};
return {
title: post.title,
description: post.summary,
// Danger: if app/layout.tsx set alternates: { canonical: '/' },
// this page may emit canonical="https://example.com/"!
};
}
// ✅ AFTER: Explicit self-referencing canonical + OpenGraph URL
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) return {};
const url = `/blog/${slug}`;
return {
title: post.title,
description: post.summary,
alternates: { canonical: url },
openGraph: {
title: post.title,
description: post.summary,
url,
type: "article",
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt ?? post.publishedAt,
},
};
}
Preventing soft 404s and client-only rendering traps
When auditing a Markdown or CMS-backed Next.js blog, check two rendering behaviors on every dynamic route:
- Always call
notFound()fromnext/navigationwhen a slug is missing: If a user or bot requests/blog/deleted-articleand your component renders<div>Article not found</div>without callingnotFound(), Next.js serves that page with HTTP status200 OK. Google Search Console flags this as a Soft 404 and wastes crawl budget on dead URLs. - Keep primary article prose and headings in Server Components: While Googlebot executes JavaScript, fetching your primary article body or
<h1>inside a client-sideuseEffect()delays content discovery, breaks OpenGraph link previews (which do not run JS), and risks empty renders when API calls time out.
Why a Next.js sitemap says “Submitted, not indexed”
Next.js makes it easy to generate a sitemap via app/sitemap.ts. However, two subtle mismatches frequently cause Search Console to reject or ignore URLs in /sitemap.xml:
- Mismatch with
trailingSlashinnext.config.ts: If you deploy a static export (`output: "export"`) on Cloudflare Pages or S3 withtrailingSlash: true, every page resolves at/blog/post-slug/. If yourapp/sitemap.tsoutputshttps://example.com/blog/post-slug(without the trailing slash), every single URL in your sitemap triggers a308redirect! Your sitemap and your canonical tags must use the exact same trailing-slash convention. - Using
new Date()for every entry’slastModified: SettinglastModified: new Date()on every post inapp/sitemap.tsstamps the current build time on articles you haven’t touched in two years. Always pass the post’s actualupdatedAtor Git commit date.
import type { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/posts";
const ORIGIN = "https://yourdomain.com";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
const postEntries: MetadataRoute.Sitemap = posts
.filter((post) => !post.draft && !post.noindex)
.map((post) => ({
url: `${ORIGIN}/blog/${post.slug}/`, // Match trailingSlash setting!
lastModified: new Date(post.updatedAt ?? post.publishedAt),
}));
return [
{ url: `${ORIGIN}/`, lastModified: new Date("2026-11-02") },
...postEntries,
];
}
The accidental 307 Temporary Redirect in middleware.ts
In Next.js middleware, calling NextResponse.redirect(new URL("/new-path", request.url)) defaults to HTTP status 307 Temporary Redirect. When Googlebot encounters a 302 or 307 temporary redirect, it assumes the move is short-lived and often keeps the old URL in the index instead of consolidating signals to the destination URL.
Whenever a redirect in middleware.ts is permanent (such as normalizing uppercase paths, stripping legacy prefixes, or redirecting old blog slugs), pass { status: 308 } or { status: 301 } explicitly, or define the redirect in next.config.ts with permanent: true.
Preventing staging and preview domains from competing with production
When you deploy a Next.js or static site to Cloudflare Pages (*.pages.dev) or Vercel (*.vercel.app), every branch and commit generates a publicly reachable preview URL. If Googlebot discovers a link to your project-name.pages.dev subdomain, it can index the preview host alongside your custom domain.
Protect your canonical domain with two layers:
- Hardcode or strictly validate your production canonical origin: Never derive your
<link rel="canonical">host dynamically from the incoming request’s `Host` header on public static pages; otherwise, a request to `project.pages.dev/pricing` will emit a self-referencing canonical for `project.pages.dev`! - Send
X-Robots-Tag: noindexon non-production hosts: Configure your hosting headers or middleware so any hostname other than your canonical production domain returns `X-Robots-Tag: noindex, nofollow`.
Honest boundaries & code review safety
Always verify diffs before merging: Automated SEO fixes in a Next.js repository should never bypass your TypeScript compiler or build step. In Searchbop, proposed metadata updates, JSON-LD blocks, and Markdown article drafts are presented as file diffs and submitted via a GitHub pull request so your CI pipeline validates every change.
Official documentation: Google Search Technical Essentials.
Research context & topics covered on this page Next.js · P1
Cluster & intent: nextjs (Informational / commercial · P1) · Planned URL: /guides/nextjs-seo-audit/
Topics addressed: Next.js SEO audit, Next.js sitemap not indexed, Next.js canonical URL, Cloudflare Pages SEO, Markdown blog SEO.
Primary documentation source: Google Search Central — Technical Essentials.