01 / TRAP #1: ROOT LAYOUT CANONICAL INHERITANCE

Why a hardcoded canonical in `app/layout.tsx` de-indexes your blog and product pages

When developers migrate to the Next.js App Router (`app/`), they often configure a global `metadata` object in `app/layout.tsx` that includes `alternates: { canonical: '/' }` or `canonical: 'https://example.com'`. If a child route such as `app/blog/[slug]/page.tsx` overrides `title` and `description` inside `generateMetadata` but forgets to override `alternates.canonical`, the child page can inherit or omit the canonical in unexpected ways.

When every blog post renders <link rel="canonical" href="https://example.com" /> in its HTML <head>, Googlebot obeys your instruction literally: it marks all of your articles as Alternate page with proper canonical tag pointing to the homepage, and removes them from search results.

02 / DYNAMIC CANONICAL URLS IN APP ROUTER

Copy-paste pattern: `metadataBase` + dynamic `generateMetadata`

To guarantee that every static and dynamic route outputs an exact, absolute, self-referencing canonical tag, pair a clean `metadataBase` in `app/layout.tsx` with explicit `alternates.canonical` in every dynamic route’s `generateMetadata`:

1. ROOT LAYOUT (app/layout.tsx)Define metadataBase once using your canonical production origin
import type { Metadata } from 'next';

const SITE_URL = 'https://yourdomain.com'; // Always use your final canonical production host

export const metadata: Metadata = {
  metadataBase: new URL(SITE_URL),
  title: {
    default: 'Your Brand — Technical Guides',
    template: '%s · Your Brand',
  },
  robots: {
    index: true,
    follow: true,
  },
};
2. DYNAMIC ROUTE (app/blog/[slug]/page.tsx)Generate an explicit self-referencing canonical for every slug
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getPostBySlug } from '@/lib/posts';

type Props = {
  params: Promise<{ slug: string }>;
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
  if (!post) return {};

  // Match your trailingSlash setting in next.config.mjs!
  const canonicalPath = `/blog/${slug}/`;

  return {
    title: post.title,
    description: post.description,
    alternates: {
      canonical: canonicalPath, // Resolves against metadataBase → https://yourdomain.com/blog/slug/
    },
    openGraph: {
      title: post.title,
      description: post.description,
      url: canonicalPath,
      type: 'article',
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
    },
  };
}
03 / TRAP #2: SITEMAP & TRAILING SLASH 308 REDIRECTS

Why your Next.js `app/sitemap.ts` shows “Sitemap could not be read” or “Page with redirect”

Google treats `https://yourdomain.com/pricing` and `https://yourdomain.com/pricing/` as two distinct URLs. In Next.js:

If your `app/sitemap.ts` file outputs URLs without a trailing slash while `trailingSlash: true` is active (or vice versa), every single URL in your XML sitemap is a redirecting URL. Google Search Console will flag those URLs under Page with redirect or leave them unindexed.

3. DYNAMIC SITEMAP (app/sitemap.ts)Enforcing exact URL parity between sitemap.xml and generateMetadata
import type { MetadataRoute } from 'next';
import { getAllPublishedPosts } from '@/lib/posts';

const SITE_URL = 'https://yourdomain.com';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPublishedPosts();

  const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
    // Keep trailing slash identical to generateMetadata and next.config.mjs
    url: `${SITE_URL}/blog/${post.slug}/`,
    lastModified: new Date(post.updatedAt || post.publishedAt),
    changeFrequency: 'monthly',
    priority: 0.7,
  }));

  return [
    {
      url: `${SITE_URL}/`,
      lastModified: new Date('2026-11-01'),
      changeFrequency: 'weekly',
      priority: 1.0,
    },
    ...postEntries,
  ];
}
04 / TRAP #3: ENVIRONMENT VARIABLE LEAKS

Preventing `VERCEL_URL`, `*.pages.dev`, or `localhost:3000` from polluting canonical tags

Never set `metadataBase: new URL(process.env.VERCEL_URL)` without distinguishing production from preview deployments, and never let `process.env.NEXT_PUBLIC_SITE_URL` fall back to `http://localhost:3000` during a production build. If a build runs with a missing environment variable or uses a branch deployment hostname, Next.js bakes <link rel="canonical" href="http://localhost:3000/blog/post" /> into your static HTML cache—causing Google to drop those pages from the index until the next clean build.

Symptom in Google Search Console Exact Next.js App Router Cause Code Fix
Alternate page with proper canonical tag Child routes inherit root `canonical: '/'` from `app/layout.tsx`. Set explicit `alternates: { canonical: ... }` in each route’s `generateMetadata`.
Page with redirect on sitemap URLs `app/sitemap.ts` trailing slashes disagree with `next.config.mjs` (`308` redirect). Normalize all sitemap <loc> and canonical URLs to use the exact same slash rule.
Submitted URL marked ‘noindex’ Preview branch check (`VERCEL_ENV !== 'production'`) evaluated true on production build. Verify live <meta name="robots"> in production HTML source via `curl -s`.
Soft 404 on missing dynamic slugs Dynamic route catches missing data and renders a `200 OK` "Post not found" component. Call `notFound()` from `next/navigation` inside the Server Component so Next.js returns HTTP `404`.

Verification commands & official sources

Always verify against built production HTML: Next.js metadata behavior in `next build && next start` can differ from `next dev`. Before deploying, run `curl -s https://yourdomain.com/blog/your-slug/ | grep -E "canonical|robots"` to confirm the rendered <head> tags match your `sitemap.xml` <loc> entries character-for-character. Reference: Next.js Docs — generateMetadata and Google Search Central — Consolidate Duplicate URLs.

Research context & search queries covered on this page Next.js · P1

Cluster & intent: nextjs (Informational / Commercial · P1) · Target queries: Next.js canonical URL, Next.js sitemap not indexed, Next.js SEO audit.

Benchmark prompt addressed: P15 — How do I audit indexing, metadata and canonicals on a Next.js site?