Learn

/

Canonical URLs

Canonical URLs

8 patterns

The canonical link element, trailing slash handling, www vs non-www, and how to tell search engines which URL is the original. You will hit this when Google indexes three versions of the same page and splits your ranking.

Avoid
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About Us",
  description: "Learn about our company.",
  // No canonical URL set
};
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About Us",
  description: "Learn about our company.",
  // No canonical URL set
};

Prefer
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About Us",
  description: "Learn about our company.",
  alternates: {
    canonical: "https://acme.com/about",
  },
};
// app/about/page.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "About Us",
  description: "Learn about our company.",
  alternates: {
    canonical: "https://acme.com/about",
  },
};
Why avoid

Without a canonical tag, search engines must guess which URL version is the 'real' one. If your page is accessible at both /about and /about?ref=footer, Google might index the wrong version or split ranking signals between them.

Why prefer

Every page should have a self-referencing canonical tag that points to its own URL. This tells search engines that this is the authoritative version of the page. Without it, Google may choose a canonical on its own, which could be a version with query parameters or a different protocol.

Google: Canonicalization
Avoid
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  // No trailingSlash config
  // /about and /about/ both work
};

export default nextConfig;
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  // No trailingSlash config
  // /about and /about/ both work
};

export default nextConfig;

Prefer
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  trailingSlash: false,
  // /about/ redirects to /about
  // Consistent canonical URLs
};

export default nextConfig;
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  trailingSlash: false,
  // /about/ redirects to /about
  // Consistent canonical URLs
};

export default nextConfig;
Why avoid

Without the trailingSlash setting, both /about and /about/ may serve the same content. Search engines treat these as two different URLs, which splits your page's ranking signals. Internal links with inconsistent trailing slashes make the problem worse.

Why prefer

Setting trailingSlash explicitly ensures consistent URL behavior. When set to false, visiting /about/ automatically redirects to /about (or vice versa when set to true). This prevents duplicate content issues where both URL variants are indexed separately.

Next.js: trailingSlash config
Avoid
// middleware.ts
import { NextResponse } from "next/server";

export function middleware() {
  // No redirect logic
  // Both www.acme.com and acme.com
  // serve the same content
  return NextResponse.next();
}
// middleware.ts
import { NextResponse } from "next/server";

export function middleware() {
  // No redirect logic
  // Both www.acme.com and acme.com
  // serve the same content
  return NextResponse.next();
}

Prefer
// middleware.ts
import { NextRequest, NextResponse } from
  "next/server";

export function middleware(request: NextRequest) {
  const host = request.headers.get("host") || "";
  if (host.startsWith("www.")) {
    const url = request.nextUrl.clone();
    url.host = host.replace("www.", "");
    return NextResponse.redirect(url, 301);
  }
  return NextResponse.next();
}
// middleware.ts
import { NextRequest, NextResponse } from
  "next/server";

export function middleware(request: NextRequest) {
  const host = request.headers.get("host") || "";
  if (host.startsWith("www.")) {
    const url = request.nextUrl.clone();
    url.host = host.replace("www.", "");
    return NextResponse.redirect(url, 301);
  }
  return NextResponse.next();
}
Why avoid

Serving identical content on both www.acme.com and acme.com creates a duplicate content problem. Google may index both versions, splitting backlink equity and ranking signals between two domains. Even with canonical tags, a redirect is the strongest signal.

Why prefer

Redirecting www to non-www (or vice versa) with a 301 ensures search engines consolidate all signals under one domain. A permanent redirect tells crawlers to update their index. This should also be configured at the DNS or CDN level for requests that never reach Next.js.

Google: Consolidate duplicate URLs
Avoid
// app/blog/page.tsx
export async function generateMetadata({
  searchParams,
}: {
  searchParams: { page?: string };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical: "https://acme.com/blog",
    },
    // All pages point canonical to page 1
  };
}
// app/blog/page.tsx
export async function generateMetadata({
  searchParams,
}: {
  searchParams: { page?: string };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical: "https://acme.com/blog",
    },
    // All pages point canonical to page 1
  };
}

Prefer
// app/blog/page.tsx
export async function generateMetadata({
  searchParams,
}: {
  searchParams: { page?: string };
}): Promise<Metadata> {
  const page = searchParams.page || "1";
  const canonical = page === "1"
    ? "https://acme.com/blog"
    : `https://acme.com/blog?page=${page}`;

  return {
    alternates: {
      canonical,
    },
  };
}
// app/blog/page.tsx
export async function generateMetadata({
  searchParams,
}: {
  searchParams: { page?: string };
}): Promise<Metadata> {
  const page = searchParams.page || "1";
  const canonical = page === "1"
    ? "https://acme.com/blog"
    : `https://acme.com/blog?page=${page}`;

  return {
    alternates: {
      canonical,
    },
  };
}
Why avoid

Setting every paginated page's canonical to the first page tells Google to ignore pages 2 and beyond. Posts that only appear on later pages will never be discovered through search. Each page has unique content and needs its own canonical URL.

Why prefer

Each page of paginated content should have its own canonical URL. Page 2 has unique content (different blog posts) that deserves its own place in search results. Pointing all pages to page 1 tells Google that pages 2, 3, and beyond are duplicates, hiding their content from search.

Google: Pagination and SEO
Avoid
// app/products/[id]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  return {
    other: {
      "link:canonical":
        `https://acme.com/products/${params.id}`,
    },
  };
}
// app/products/[id]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  return {
    other: {
      "link:canonical":
        `https://acme.com/products/${params.id}`,
    },
  };
}

Prefer
// app/products/[id]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical:
        `https://acme.com/products/${params.id}`,
    },
  };
}
// app/products/[id]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { id: string };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical:
        `https://acme.com/products/${params.id}`,
    },
  };
}
Why avoid

Using the other field to set a canonical link is a hack that does not generate a proper <link rel='canonical'> element. The other field creates <meta> tags, not <link> tags. The canonical tag requires a <link> element to be recognized by search engines.

Why prefer

Next.js provides the alternates.canonical field specifically for setting canonical URLs. It renders the proper <link rel='canonical'> tag in the document head. This is the idiomatic approach that integrates with the rest of the metadata system.

Next.js: alternates metadata
Avoid
// app/products/page.tsx
export async function generateMetadata({
  searchParams,
}: {
  searchParams: {
    sort?: string;
    color?: string;
  };
}): Promise<Metadata> {
  const qs = new URLSearchParams(searchParams);
  return {
    alternates: {
      canonical:
        `https://acme.com/products?${qs}`,
    },
  };
}
// app/products/page.tsx
export async function generateMetadata({
  searchParams,
}: {
  searchParams: {
    sort?: string;
    color?: string;
  };
}): Promise<Metadata> {
  const qs = new URLSearchParams(searchParams);
  return {
    alternates: {
      canonical:
        `https://acme.com/products?${qs}`,
    },
  };
}

Prefer
// app/products/page.tsx
export async function generateMetadata({
  searchParams,
}: {
  searchParams: {
    sort?: string;
    color?: string;
  };
}): Promise<Metadata> {
  // Filter params only keep meaningful ones
  const color = searchParams.color;
  const canonical = color
    ? `https://acme.com/products?color=${color}`
    : "https://acme.com/products";

  return {
    alternates: { canonical },
  };
}
// app/products/page.tsx
export async function generateMetadata({
  searchParams,
}: {
  searchParams: {
    sort?: string;
    color?: string;
  };
}): Promise<Metadata> {
  // Filter params only keep meaningful ones
  const color = searchParams.color;
  const canonical = color
    ? `https://acme.com/products?color=${color}`
    : "https://acme.com/products";

  return {
    alternates: { canonical },
  };
}
Why avoid

Including all query parameters in the canonical creates a unique canonical for every combination of sort and filter. This fragments your page's ranking signals across dozens of URLs that all show similar content. Search engines may also waste crawl budget on low-value parameter variations.

Why prefer

Only include query parameters that produce meaningfully different content. A color filter shows different products, so it deserves its own canonical. A sort parameter shows the same products in a different order, so it should be stripped from the canonical to avoid duplicate content.

Google: URL parameters
Avoid
// Syndicated article on partner.com
// app/syndicated/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical:
        `https://partner.com/syndicated/${params.slug}`,
    },
  };
}
// Syndicated article on partner.com
// app/syndicated/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical:
        `https://partner.com/syndicated/${params.slug}`,
    },
  };
}

Prefer
// Syndicated article on partner.com
// app/syndicated/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical:
        `https://acme.com/blog/${params.slug}`,
    },
  };
}
// Syndicated article on partner.com
// app/syndicated/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical:
        `https://acme.com/blog/${params.slug}`,
    },
  };
}
Why avoid

Setting the canonical to the partner site's own URL means the syndicated copy becomes the 'original' in Google's eyes. Your original article on acme.com could be treated as a duplicate, losing rankings to the partner's republished version.

Why prefer

When content is syndicated (republished on a partner site), the canonical should point back to the original source on your domain. This tells Google that the original lives at acme.com, so all ranking signals flow to your site instead of the partner's copy.

Google: Canonical for syndication
Avoid
// app/docs/[...slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string[] };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical: "/docs",
    },
  };
  // All nested docs pages share one canonical
}
// app/docs/[...slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string[] };
}): Promise<Metadata> {
  return {
    alternates: {
      canonical: "/docs",
    },
  };
  // All nested docs pages share one canonical
}

Prefer
// app/docs/[...slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string[] };
}): Promise<Metadata> {
  const path = params.slug.join("/");
  return {
    alternates: {
      canonical: `https://acme.com/docs/${path}`,
    },
  };
}
// app/docs/[...slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string[] };
}): Promise<Metadata> {
  const path = params.slug.join("/");
  return {
    alternates: {
      canonical: `https://acme.com/docs/${path}`,
    },
  };
}
Why avoid

Pointing every nested documentation page to /docs tells Google that hundreds of unique pages are all duplicates of the docs index. Google will likely ignore this incorrect signal, but it creates confusion and may delay proper indexing of your documentation.

Why prefer

Catch-all routes like [...slug] serve many different pages. Each one needs its own canonical URL built from the route parameters. This ensures /docs/getting-started and /docs/api/reference are recognized as distinct pages with their own search rankings.

Next.js: Catch-all segments