By SitemapFixer Team
Updated April 2026

How to Add a Canonical Tag in HTML (With Examples)

Check your canonical tags freeAnalyze Free

This tutorial walks through exactly how to add a canonical tag in HTML, where to place it, and how to implement it on the platforms most people use — WordPress (Yoast, Rank Math), Next.js, and via HTTP headers for non-HTML files like PDFs. Every example is copy-paste ready and matches what Google Search Console expects to see.

The Exact HTML Syntax

The canonical tag is a <link> element with rel="canonical" and the absolute URL in href:

<link rel="canonical" href="https://example.com/page/" />

Where to Place It

Inside the <head>, ideally near the top. Google ignores canonical tags placed in the body. Example:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Example Page</title>
  <link rel="canonical" href="https://example.com/page/" />
</head>
<body>...</body>
</html>

The tag must be in the initial server-rendered HTML. Canonicals injected by JavaScript after page load are sometimes picked up by Google but frequently ignored.

Self-Referencing Canonical

Every indexable page should canonical to itself. The URL in the tag matches the URL the page loads at:

<!-- On https://example.com/blog/post-slug/ -->
<link rel="canonical" href="https://example.com/blog/post-slug/" />

Cross-Domain Canonical

When the same content is syndicated on a partner domain, the syndicated version canonicals back to your original:

<!-- On https://partner.com/republished-article/ -->
<link rel="canonical" href="https://example.com/original-article/" />

Setting Canonical via HTTP Header (for PDFs etc.)

For files that cannot contain HTML — PDFs, images, plain text — send canonical as an HTTP response header:

Link: <https://example.com/whitepaper.pdf>; rel="canonical"

In Nginx: add_header Link '<https://example.com/whitepaper.pdf>; rel="canonical"';. In Apache: Header set Link '<https://example.com/whitepaper.pdf>; rel="canonical"'.

How to Add Canonical in WordPress

Yoast SEO: Edit the post > scroll to the Yoast box > Advanced > "Canonical URL" field. Leave blank for self-canonical (Yoast generates it), or enter a URL to override.

Rank Math: Edit the post > Rank Math sidebar > Advanced tab > "Canonical URL" field. Same behavior as Yoast.

Both plugins automatically add self-referencing canonicals to every post and page. Do not also enable canonical in your theme — two sources will produce duplicate tags and Google will ignore both.

How to Add Canonical in Next.js / React

In Next.js App Router, use the metadata export:

export const metadata = {
  alternates: {
    canonical: 'https://example.com/page/',
  },
};

In Pages Router or plain React with next/head:

import Head from 'next/head';

export default function Page() {
  return (
    <Head>
      <link rel="canonical" href="https://example.com/page/" />
    </Head>
  );
}

Common Mistakes

— Relative URL (href="/page/") instead of absolute. Use full URLs.

— Trailing slash mismatch with the actual URL.

— Canonical in <body> instead of <head>.

— Multiple canonicals on the same page (from theme + plugin).

— Protocol mismatch (http:// on an HTTPS page).

— Canonical to a URL that 301 redirects instead of returning 200.

Verification

Open Google Search Console, go to URL Inspection, paste your URL. The tool shows "User-declared canonical" and "Google-selected canonical." If they match, you are done. If they differ, Google is overriding — fix any conflicting internal links, sitemap entries, or hreflang before adding more canonical tags.

Shopify Canonical Tag

Shopify auto-generates canonicals through {{ canonical_url }} in theme.liquid. On most stores this is already in the head — open layout/theme.liquid and look for:

<link rel="canonical" href="{{ canonical_url }}">

The big Shopify gotcha: product variants. /products/shirt?variant=12345 gets the same canonical as /products/shirt, which is correct. But if you've customized the variant URL structure or installed an app that rewrites product URLs, confirm canonical still resolves to the parent product.

Raw PHP Example

On a non-WordPress PHP site, compute the canonical server-side from the request URL, then echo it in <head>:

<?php
// Strip query strings, normalize trailing slash, force HTTPS
$scheme = 'https';
$host   = $_SERVER['HTTP_HOST'];
$path   = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (substr($path, -1) !== '/') $path .= '/';
$canonical = $scheme . '://' . $host . $path;
?>
<link rel="canonical" href="<?php echo htmlspecialchars($canonical); ?>" />

Don't forget htmlspecialchars(). I've seen canonical injection vulnerabilities where user-supplied query strings ended up unescaped in the canonical href.

The Wrong Way (Don't Do These)

Every one of these I've found on live production sites in the last year.

Wrong: canonical in the body.

<body>
  <link rel="canonical" href="https://example.com/page/" />
  <h1>...</h1>
</body>

Google ignores this completely. If you're injecting canonical via a third-party script that mounts after <body> opens, rewrite to inject into <head> pre-render.

Wrong: relative URL.

<!-- DON'T -->
<link rel="canonical" href="/page/" />
<link rel="canonical" href="page.html" />

Technically allowed by the spec, but error-prone. Absolute every time.

Wrong: injected by JavaScript only.

// Client-side only — risky
const link = document.createElement('link');
link.rel = 'canonical';
link.href = window.location.href;
document.head.appendChild(link);

Google sometimes picks this up, often doesn't. If you're on an SPA, server-render or use getServerSideProps/route metadata to emit canonical in initial HTML.

HTTP Header vs HTML: When to Use Which

HTML <link> tag is the standard for web pages. The Link: HTTP header is mandatory for non-HTML content (PDFs, images served standalone, XML feeds) and optional for HTML pages. You can use both on an HTML page — Google reads either.

A complete Nginx example for a PDF library:

location ~* \.pdf$ {
  add_header Link '<https://example.com$uri>; rel="canonical"';
  add_header X-Robots-Tag "noarchive";
}

Verify with curl -I https://example.com/file.pdf and look for the Link: header in the response.

A Real Case: The 2,000-Product E-commerce Canonical Mess

I audited a mid-size e-commerce site in March 2026 running around 2,000 SKUs on a custom Laravel setup. Their product pages had canonical tags hardcoded to https:// URLs. Fine in isolation. But their category filter URLs generated pages like /shop/shoes?brand=nike&color=black&size=10 that rendered the category template without a canonical at all. GSC was flagging around 6,400 filter combinations as "Duplicate without user-selected canonical."

The dev team's fix: add canonical via a global middleware that emitted the request URL minus query params. This worked for 90% of pages but broke pagination — page 2 of a category canonicalized to page 1, which collapsed the entire catalog into just the first 40 products.

Right fix: compute canonical per route type. Product pages self-canonical to their clean URL. Category pages self-canonical with pagination preserved (?page=2 included in canonical). Filter combinations canonical to the parent category only when the filter combo has low search volume; high-volume combinations (color + size) get self-canonical and hreflang alternate if translated. Took three weeks of careful route-by-route logic, but indexing recovery was immediate.

The takeaway: there's no single "correct" canonical rule for a multi-template site. You need per-route decisions.

A Sanity Checklist Before You Ship

I run through this every time I audit a canonical implementation:

  • Exactly one <link rel="canonical"> per page (grep the rendered HTML).
  • Absolute URL with https://.
  • Target returns 200, not 301 or 404.
  • Trailing slash matches site convention.
  • Matches case exactly (Google treats /Page/ and /page/ as distinct).
  • Present in server-rendered HTML, not JS-injected.
  • Sitemap URL matches canonical URL.
  • Internal links to this page match the canonical URL.

If all eight pass, GSC will almost always agree with your canonical. If any fail, you're shipping a conflict signal and Google may pick something else.

Dynamically Generating Canonical URLs Safely

Most canonical bugs come from string concatenation mistakes. Don't build canonical URLs by stitching together $_SERVER['HTTP_HOST'] with $_SERVER['REQUEST_URI'] and hoping for the best. Both values can be manipulated by attackers via HTTP headers, and both can contain query strings or fragments that shouldn't be in the canonical.

Safer pattern: maintain a whitelist of valid hosts, normalize the path, strip disallowed query params, and only then emit the canonical.

// Node.js / Next.js example
const VALID_HOSTS = ['example.com', 'www.example.com'];
const CANONICAL_HOST = 'example.com';
const INDEXABLE_QUERY_PARAMS = ['page', 'sort'];

function buildCanonical(req) {
  const host = req.headers.host?.toLowerCase();
  if (!VALID_HOSTS.includes(host)) return null;

  const url = new URL(req.url, 'https://' + CANONICAL_HOST);
  // Only keep allowed query params
  const allowed = new URLSearchParams();
  for (const p of INDEXABLE_QUERY_PARAMS) {
    if (url.searchParams.has(p)) {
      allowed.set(p, url.searchParams.get(p));
    }
  }
  const query = allowed.toString();

  // Force trailing slash convention
  let pathname = url.pathname;
  if (!pathname.endsWith('/') && !pathname.includes('.')) {
    pathname += '/';
  }

  return `https://${CANONICAL_HOST}${pathname}${query ? '?' + query : ''}`;
}

This handles four common bugs at once: wrong-host canonicals, trailing slash inconsistency, leaky query params (UTM, session IDs, tracking), and host header injection.

Canonical in an App Router Layout Hierarchy

Next.js App Router gets complicated when you have nested layouts. A root layout with metadata, a section layout with metadata, and a page with metadata — all three can contribute alternates.canonical, and the nearest one wins.

In practice I recommend always setting canonical at the page level, never at a layout level. Layouts handle site-wide concerns (favicons, OG images). Pages handle page-specific metadata including canonical. This avoids accidental inheritance where a section layout's canonical leaks to all its children.

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

export async function generateMetadata(
  { params }
): Promise<Metadata> {
  const slug = (await params).slug;
  return {
    title: `Post: ${slug}`,
    alternates: {
      canonical: `https://example.com/blog/${slug}`,
    },
  };
}

Dynamic routes especially need this pattern — you can't hardcode canonical in a [slug] route at build time unless you use generateStaticParams.

Related Guides

Audit your canonical tags now
Free analysis in 60 seconds
Analyze My Site Free
Related guides