Technical SEO · 2026-09-10 · 10 min read

Canonicalisation failures in Next.js App Router

Canonicalisation is a URL identity problem. Next.js can render the right page while search engines still see multiple URL identities. Define one public URL policy, derive metadata from the same route params, and test the rendered head—not only the source component.

Scope: This is an implementation reference. Validate claims against current Google, vendor, and platform documentation before making a release or policy decision.

Set one URL policy

Choose HTTPS, one host, one trailing-slash convention, and one locale path convention. Enforce host redirects at the edge, then make `metadataBase`, canonical URLs, sitemaps, hreflang, and internal links agree. A canonical tag is a hint; conflicting redirects and links weaken it.

// next.config.js
const nextConfig = {
  trailingSlash: false,
  async redirects() {
    return [{
      source: '/:path*',
      has: [{ type: 'host', value: 'abdallahmekky.com' }],
      destination: 'https://www.abdallahmekky.com/:path*',
      permanent: true,
    }]
  },
}

Correct generateMetadata for dynamic pages

Use the same slug lookup for content and metadata. Never build a canonical from an unvalidated query string. Return `notFound()` before emitting metadata for an unknown entity.

import type { Metadata } from 'next'
import { notFound } from 'next/navigation'

export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params
  const page = await getPageBySlug(slug)
  if (!page) notFound()

  const canonical = new URL('/resources/slug', SITE_URL)
  return {
    title: page.title,
    description: page.description,
    alternates: { canonical: canonical.toString() },
    openGraph: { type: 'article', url: canonical.toString(), title: page.title },
  }
}

For localized pages, use explicit alternates rather than hoping a shared layout infers them:

alternates: {
  canonical: 'https://www.example.com/ar/guide',
  languages: {
    en: 'https://www.example.com/guide',
    ar: 'https://www.example.com/ar/guide',
    'x-default': 'https://www.example.com/guide',
  },
}

Test the rendered contract

For every route family, crawl the final deployment with and without a slash, both hosts, locale variants, and query parameters. Assert one 200 URL, one canonical, one indexable response, and reciprocal hreflang. Also inspect `sitemap.xml`: a sitemap full of non-canonical URLs is an architectural bug.

curl -sSI https://example.com/guide/
curl -sS https://www.example.com/guide | grep -i 'canonical|hreflang'

# A minimal invariant for an automated crawler
status == 200
canonical == final_url
sitemap_url == canonical
hreflang['en'] == english_url
hreflang['ar'] == arabic_url