Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SEO & Digital Marketing  Module 2 – Technical SEO: crawling and indexing

Canonical URLs and duplicate content

How Google picks one address from a group of duplicate pages, the signals that decide it, and how to fix http, www, slash and parameter copies.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • List the signals Google uses to choose a canonical URL, and set them so that they agree
  • Fix common duplicates, from http and www to slashes, capitals, parameters and print views, with a redirect or a canonical
  • Write a canonical link or Link header that Google can use, and spot one it ignores
  • Diagnose "Duplicate, Google chose different canonical than user" with URL Inspection

Before you start

What you’ll learn · Before you start

What you will learn

  • List the signals Google uses to choose a canonical URL, and set them so that they agree
  • Fix common duplicates, from http and www to slashes, capitals, parameters and print views, with a redirect or a canonical
  • Write a canonical link or Link header that Google can use, and spot one it ignores
  • Diagnose "Duplicate, Google chose different canonical than user" with URL Inspection

Before you start

On this page

Pune Bakes’ mango mousse page answers at twelve addresses: with and without www, over http and https, with a slash at the end, with capital letters, with tracking parameters from Instagram and Facebook, as a print view and at the old shop’s product.php?id=482. To Google most of them are copies of one page, and it shows only one of them.

Choosing that one is called canonicalisation: How Google picks one representative address from a group of pages with the same main content. A site can state its choice with redirects, canonical tags and its sitemap, but Google treats the choice as a hint., and the site has a say in it. By the end of this page you can list the signals Google uses to choose a canonical URL: The address Google picks as the most representative of a group of duplicate pages; it is the one that may be shown in results. A site's preferred canonical is a hint, not a rule. More in “How Google Search works: crawl, index, serve” and make them agree, fix the common causes of copies with a redirect or a canonical tag, write a canonical that Google can use, and read what Search Console says when Google chooses another address.

Duplicates are normal, and Google keeps one address of each group

When Google indexes pages, it works out the main content of each one, the part that is not header, menu or footer, and puts pages whose main content is the same or nearly the same into one group. From each group it picks the page it judges “objectively the most complete and useful for search users” as the canonical (What is URL canonicalization). The canonical is the one results usually link to, it is crawled most often, and the others, the duplicates, are crawled less.

Google names five ordinary reasons for duplicate content: The same or nearly the same main content at more than one address. Within a site it is normal and not a spam-policy violation: Google groups the copies and shows one address of each group., and the bakery has an example of most of them:

Reason Google names What it looks like The bakery’s case
Protocol variants The same page on http and https http://punebakes.example/cakes/mango-mousse
Site functions A list sorted or filtered /cakes/?sort=price
Device variants A mobile and a desktop version None: one page serves every screen
Region variants Pages for two countries in one language None yet
Accidental variants A demo or test copy left open to crawlers A staging copy, if it were not behind a login

None of this is a fault in itself. Google says that “some duplicate content on a site is normal”, and its starter guide adds that it is not a violation of its spam policies and not something that will cause a manual action (SEO Starter Guide). What copies do cost is control: if the site says nothing, Google chooses the address people see, and visits, links and reports are split between the copies. Step through what happens to the twelve addresses:

Interactive · stage by stage From twelve addresses to one canonical Step through what Google does with the twelve addresses at which the bakery's mango mousse page answers.
From twelve addresses to one canonical, step 6 of 6. Showing Crawl the addresses (3 redirects, 9 answer 200), Group by main content (11 addresses, one text), Weigh the signals (most: /cakes/mango-mousse), Pick the canonical (/cakes/mango-mousse), Show and recrawl. In focus: Show and recrawl.another languageCrawl the addresses3 redirects, 9 answer 200Group by main content11 addresses, one textWeigh the signalsmost: /cakes/mango-moussePick the canonical/cakes/mango-mousseShow and recrawlthe canonical in results

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Twelve addresses. Googlebot meets the mango mousse page at twelve addresses, from links, campaigns and old menus. Three answer with a 301 to the bakery's preferred address; the other nine answer 200.
  2. One group of copies. Google compares the main content of the pages, not the headers and footers. Eight of the nine show the same English text, so they form one group of duplicates; the redirects join the group of their target.
  3. Another language stands alone. The ninth page shows the same cake in Marathi. A page in another language is a duplicate only if its main content is in the same language, so it forms a group of its own and keeps its own canonical.
  4. The signals vote. For the English group Google weighs what the site says. The three 301s and the canonical tags name /cakes/mango-mousse, and so does the sitemap. HTTPS also counts, because Google prefers an HTTPS page over the same page on HTTP.
  5. A hint, not a rule. Google chooses the address it judges the most complete and useful. The site's signals are hints that weigh heavily when they agree; when they point different ways, Google may choose another address.
  6. The canonical is what shows. Results link to the canonical, and Google crawls it most often and the copies less. What Google knows about the copies, such as the links that point at them, is gathered on the canonical.

The Marathi page did not join the group. Language versions are duplicates only when their main content is in the same language, so a translated page keeps its own canonical, and only translating the menu and the footer does not make a page distinct.

Checkpoint

Check: Duplicates and the canonical Google keeps

  1. Question 1 of 2 The mango mousse page also answers at ?lang=mr, with the same photos and price and its text in Marathi. Is it a duplicate of the English page?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No; its main content is in another language, so it keeps its own canonical

    The program found 12 addresses showing 2 pages: 11 show the English text and 1 the Marathi text, which forms a group of its own and names itself.

  2. Question 2 of 2 What does Google do with a site that has some duplicate content?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It groups the copies and shows one address of each group, the canonical

    What copies cost is control: if the site says nothing, Google chooses which address people see.

Redirects and canonical tags are strong signals; a sitemap is weak

A site states its choice with signals, and Google ranks them by strength (specify a canonical):

Method Strength Use it when
A permanent redirect (301 or 308) Strong: the target is treated as canonical Nobody needs the copy any more
A rel=canonical: A link element in a page's head, or a Link header for files such as PDFs, that names the address a site prefers for a group of duplicate pages. Google counts it as a strong signal, not a rule. link in the head Strong The copy must keep working: tracking links, print views, sorted lists
A canonical Link header Strong, for web search results The copy is a PDF or another file without a head
Listing the main page in the sitemap Weak Always, alongside a strong signal

The methods add up, so a site that uses two of them makes its choice more likely to be the one shown. Other signals count too. Google prefers an HTTPS page over the same page on HTTP, unless the HTTPS page has an invalid certificate, loads insecure content other than images, redirects to HTTP or names an HTTP canonical. It also reads the site’s own links, so internal links should point at the preferred address and never at a copy. Every page you prefer should name itself, which is called a self-referencing canonical: A canonical tag on the preferred page that names the page itself, so that copies created by tracking or sorting parameters still name the right address.: a copy made by a tracking parameter then still names the right page.

Three tools look like ways to choose a canonical and are not. A robots.txt block keeps Google from reading the copy at all, while the copy’s address can still be indexed without its content. A noindex on a copy “will completely block the page from Search”, which is why Google calls rel="canonical" the preferred solution. And the URL removal tool hides every version of an address, the canonical included. Answer two questions about a copy of your own, and the table marks the methods that fit:

Interactive · answer and compare Which signal does this copy need? Answer two questions about one copy of a page on your site. The table marks the methods that fit and says why the others do not.
Must visitors and campaign links still open the copy as it is, such as a tracking link or a print view?
Is the copy an HTML page, or a file such as a PDF?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Six methods against how strongly each names the main page, whether the copy keeps working, and whether it works for files
Method Names the main page The copy keeps working Works for PDFs and files
A 301 redirect Where it goes: the server, before any page is sent Good a strong signal: Google treats the target as canonical Poor every visitor and crawler is sent on to the main page Good the server can redirect any address it answers
A rel=canonical link Where it goes: the head of the copy Good a strong signal, when the other signals agree with it Good the copy answers 200 as before Poor only an HTML head can hold the tag
A canonical Link header Where it goes: the HTTP response of any file Good a strong signal, used for web search results Good the copy answers 200 as before Good the header travels with PDFs and other files
Listing the main page in the sitemap Where it goes: the sitemap Mixed a weak signal: Google still has to find the copies itself Good nothing changes for visitors Mixed it lists the main page and names no copy
noindex on the copy Where it goes: a meta tag or X-Robots-Tag Poor removes the copy from Search and names no main page Good visitors can still open the copy Mixed X-Robots-Tag reaches files, but it is the wrong tool here
A robots.txt block of the copy Where it goes: robots.txt Poor Google cannot read a blocked copy, which may still be indexed Good visitors are not affected Mixed it blocks files too, and names no main page
How the answers decide
  • “Must visitors and campaign links still open the copy as it is, such as a tracking link or a print view?” Yes, it must keep working: rules out A 301 redirect, noindex on the copy and A robots.txt block of the copy and adds a caveat to Listing the main page in the sitemap. A redirect would stop the copy working, and Google advises against noindex and robots.txt for choosing a canonical. A canonical keeps the copy working; a sitemap listing only adds a weak signal.
  • “Must visitors and campaign links still open the copy as it is, such as a tracking link or a print view?” No, nobody needs it: rules out noindex on the copy and A robots.txt block of the copy and adds a caveat to A rel=canonical link, A canonical Link header and Listing the main page in the sitemap. Nobody needs the copy, so retire it with a 301, the strongest signal. A canonical alone would leave the copy answering, and a sitemap listing alone is weak.
  • “Is the copy an HTML page, or a file such as a PDF?” A PDF or another file: rules out A rel=canonical link. A PDF has no HTML head for a link element, so its canonical travels in a Link header of the response.
  • “Is the copy an HTML page, or a file such as a PDF?” An HTML page: adds a caveat to A canonical Link header. An HTML page can take the link element or the header. Use one of them, not both, because two declarations are more error-prone.

In short: a 301 for a copy nobody needs, a canonical link for an HTML copy that must keep working, a Link header for a file, and the main page in the sitemap in every case.

Key idea

Use the strongest signal the copy allows, and make every signal, the redirects, the tags, the sitemap and the site’s own links, name the same address.

Checkpoint

Check: Signals and their strength

  1. Question 1 of 2 Instagram posts link to /cakes/mango-mousse?utm_source=instagram, and the campaign must keep counting its visits. Which signal suits this copy?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A canonical link to /cakes/mango-mousse, with the copy still answering 200

    A 301 is for copies nobody needs; a canonical is for copies that must keep working, such as tracking links and print views.

  2. Question 2 of 2 The bakery's menu is a page at /menu/ and a file at /menu.pdf. How can the PDF name /menu/ as its canonical?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: With an HTTP Link header on the PDF's response, rel="canonical"

    The header reads Link: <https://punebakes.example/menu/>; rel="canonical", with the full address in angle brackets.

Every copy has a cause, and the cause decides the fix

Each of the bakery’s twelve addresses exists for a reason, and the reason says whether a visitor still needs it. The file below holds what the bakery’s server sends for each one: the status, the target of a redirect, and the start of the page:

responses.json: what the server sends for each address JSON · duplicates/responses.json
[
  {
    "address": "https://punebakes.example/cakes/mango-mousse",
    "status": 200,
    "html": "<head><title>Mango mousse cake | Pune Bakes</title><link rel=\"canonical\" href=\"https://punebakes.example/cakes/mango-mousse\"></head><main><h1>Mango mousse cake</h1><p>Eggless, made with Alphonso mango pulp, half a kilo or one kilo.</p></main>"
  },
  {
    "address": "http://punebakes.example/cakes/mango-mousse",
    "status": 301,
    "location": "https://punebakes.example/cakes/mango-mousse"
  },
  {
    "address": "https://www.punebakes.example/cakes/mango-mousse",
    "status": 301,
    "location": "https://punebakes.example/cakes/mango-mousse"
  },
  {
    "address": "http://www.punebakes.example/cakes/mango-mousse",
    "status": 301,
    "location": "https://punebakes.example/cakes/mango-mousse"
  },
  {
    "address": "https://punebakes.example/cakes/mango-mousse/",
    "status": 200,
    "html": "<head><title>Mango mousse cake | Pune Bakes</title><link rel=\"canonical\" href=\"https://punebakes.example/cakes/mango-mousse\"><link rel=\"canonical\" href=\"https://punebakes.example/cakes/mango-mousse/\"></head><main><h1>Mango mousse cake</h1><p>Eggless, made with Alphonso mango pulp, half a kilo or one kilo.</p></main>"
  },
  {
    "address": "https://punebakes.example/cakes/Mango-Mousse",
    "status": 200,
    "html": "<head><title>Mango mousse cake | Pune Bakes</title></head><main><h1>Mango mousse cake</h1><p>Eggless, made with Alphonso mango pulp, half a kilo or one kilo.</p></main>"
  },
  {
    "address": "https://punebakes.example/cakes/mango-mousse?utm_source=instagram&utm_medium=social",
    "status": 200,
    "html": "<head><title>Mango mousse cake | Pune Bakes</title><link rel=\"canonical\" href=\"https://punebakes.example/cakes/mango-mousse\"></head><main><h1>Mango mousse cake</h1><p>Eggless, made with Alphonso mango pulp, half a kilo or one kilo.</p></main>"
  },
  {
    "address": "https://punebakes.example/cakes/mango-mousse?fbclid=IwAR2x",
    "status": 200,
    "html": "<head><title>Mango mousse cake | Pune Bakes</title><link rel=\"canonical\" href=\"https://punebakes.example/cakes/mango-mousse\"></head><main><h1>Mango mousse cake</h1><p>Eggless, made with Alphonso mango pulp, half a kilo or one kilo.</p></main>"
  },
  {
    "address": "https://punebakes.example/cakes/mango-mousse?ref=menu",
    "status": 200,
    "html": "<head><title>Mango mousse cake | Pune Bakes</title><link rel=\"canonical\" href=\"https://punebakes.example/cakes/mango-mousse?ref=menu\"></head><main><h1>Mango mousse cake</h1><p>Eggless, made with Alphonso mango pulp, half a kilo or one kilo.</p></main>"
  },
  {
    "address": "https://punebakes.example/print/cakes/mango-mousse",
    "status": 200,
    "html": "<head><title>Mango mousse cake (print) | Pune Bakes</title><link rel=\"canonical\" href=\"https://punebakes.example/cakes/mango-mousse\"></head><main><h1>Mango mousse cake</h1><p>Eggless, made with Alphonso mango pulp, half a kilo or one kilo.</p></main>"
  },
  {
    "address": "https://punebakes.example/product.php?id=482",
    "status": 200,
    "html": "<head><title>Mango mousse cake | Pune Bakes</title></head><main><h1>Mango mousse cake</h1><p>Eggless, made with Alphonso mango pulp, half a kilo or one kilo.</p></main>"
  },
  {
    "address": "https://punebakes.example/cakes/mango-mousse?lang=mr",
    "status": 200,
    "html": "<head><title>मँगो मूस केक | Pune Bakes</title><link rel=\"canonical\" href=\"https://punebakes.example/cakes/mango-mousse?lang=mr\"></head><main><h1>मँगो मूस केक</h1><p>अंड्याशिवाय, हापूस आंब्याच्या गरापासून, अर्धा किलो किंवा एक किलो.</p></main>"
  }
]

The program reads it, groups the pages by their main text, works out the preferred address with the bakery’s rules and says what each address needs:

Twelve addresses of one page, and what each needs Python · duplicates/canonical_for.py
"""Twelve addresses that answer with Pune Bakes' mango mousse page, and what each one needs.

responses.json holds what the bakery's server sends for each address: a status, a Location for a redirect, and the
HTML of a page. The program groups the pages by their main text, as Google groups duplicates, works out the one
address the bakery prefers for each, and says whether an address needs a redirect, a canonical tag, or nothing.
"""

import json
from html.parser import HTMLParser
from urllib.parse import parse_qsl, urlencode, urlsplit

SITE = "https://punebakes.example"
TRACKING = ("utm_", "fbclid", "gclid", "msclkid", "ref")  # parameters that never change what the page shows
OLD_IDS = {"482": "/cakes/mango-mousse"}  # the shop's old product.php addresses


class Page(HTMLParser):
    """Reads the canonical link of a page and the text of its main element."""

    def __init__(self, html):
        super().__init__()
        self.canonical, self.main, self.inside = None, "", False
        self.feed(html)

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if tag == "link" and a.get("rel") == "canonical":
            self.canonical = a.get("href")
        self.inside = self.inside or tag == "main"

    def handle_data(self, data):
        self.main += data if self.inside else ""


def preferred(address):
    """https, no www, lower case, no slash after a product, no tracking parameters, no print folder."""
    u = urlsplit(address)
    path = u.path.lower().removeprefix("/print")
    query = [(k, v) for k, v in parse_qsl(u.query) if not k.startswith(TRACKING)]
    if path == "/product.php":
        path, query = OLD_IDS[dict(query)["id"]], []
    if path.count("/") > 2:
        path = path.rstrip("/")
    return SITE + path + ("?" + urlencode(sorted(query)) if query else "")


def short(address):
    return address.removeprefix(SITE) or "/"


def needs(r, want):
    if r["status"] == 301:
        return "nothing: one 301 to it" if r["location"] == want else f"301 to {short(want)}, in one hop"
    page = Page(r["html"])
    if r["address"] == want:
        return "nothing: it names itself" if page.canonical == want else "a canonical tag to itself"
    u = urlsplit(r["address"])
    reachable = u.path.startswith("/print/") or any(k.startswith(TRACKING) for k, _ in parse_qsl(u.query))
    if not reachable:
        return f"301 to {short(want)}"
    return "nothing: its canonical names it" if page.canonical == want else f"canonical tag to {short(want)}"


responses = json.load(open("responses.json", encoding="utf-8"))
by_address = {r["address"]: r for r in responses}
groups = {}  # main text -> addresses that show it (a redirect shows its target's page)
for r in responses:
    shown = by_address[r["location"]] if r["status"] == 301 else r
    groups.setdefault(Page(shown["html"]).main, []).append(r)
sizes = ", ".join(f"one at {len(g)}" for g in groups.values())
print(f"{len(responses)} addresses show {len(groups)} different pages: {sizes}")
fixes = []
for group in groups.values():
    want = preferred(group[0]["address"])
    print(f"\nThe page whose preferred address is {short(want)}:")
    for r in group:
        need = needs(r, want)
        fixes += [need] if not need.startswith("nothing") else []
        shown = short(r["address"]) if r["address"].startswith(SITE + "/") else r["address"]
        print(f"  {r['status']}  {shown}\n       needs {need}")
redirects = sum(f.startswith("301") for f in fixes)
print(f"\nTo fix: {redirects} addresses need a 301, {len(fixes) - redirects} needs a canonical tag")

Output

12 addresses show 2 different pages: one at 11, one at 1

The page whose preferred address is /cakes/mango-mousse:
  200  /cakes/mango-mousse
       needs nothing: it names itself
  301  http://punebakes.example/cakes/mango-mousse
       needs nothing: one 301 to it
  301  https://www.punebakes.example/cakes/mango-mousse
       needs nothing: one 301 to it
  301  http://www.punebakes.example/cakes/mango-mousse
       needs nothing: one 301 to it
  200  /cakes/mango-mousse/
       needs 301 to /cakes/mango-mousse
  200  /cakes/Mango-Mousse
       needs 301 to /cakes/mango-mousse
  200  /cakes/mango-mousse?utm_source=instagram&utm_medium=social
       needs nothing: its canonical names it
  200  /cakes/mango-mousse?fbclid=IwAR2x
       needs nothing: its canonical names it
  200  /cakes/mango-mousse?ref=menu
       needs canonical tag to /cakes/mango-mousse
  200  /print/cakes/mango-mousse
       needs nothing: its canonical names it
  200  /product.php?id=482
       needs 301 to /cakes/mango-mousse

The page whose preferred address is /cakes/mango-mousse?lang=mr:
  200  /cakes/mango-mousse?lang=mr
       needs nothing: it names itself

To fix: 3 addresses need a 301, 1 needs a canonical tag

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 canonical_for.py

The program found 2 pages, not 12, and 4 things to fix. Read the causes from its output:

  • Protocol and host. The three http and www addresses already answer with one 301 straight to the preferred address, as the first lesson of this module set them up. Nothing to do.
  • Trailing slash and capital letters. In the URL standard only the scheme and the host are case-insensitive, and nothing makes a path with a slash at the end equal to one without (RFC 3986, section 6); Google treats /APPLE and /apple as different addresses (URL structure). The bakery’s server answers /cakes/mango-mousse/ and /cakes/Mango-Mousse with the same page, so each becomes a 301 to the one spelling the site uses.
  • Tracking parameters. The utm_source and fbclid copies must keep working, or the campaigns lose what their parameters record, so they keep answering 200 and their canonical names the page. ?ref=menu names itself instead: that is the one canonical tag to fix. A tracking parameter: A query parameter such as utm_source, gclid or fbclid that records where a visit came from and does not change what the page shows. It belongs in links from outside, never in a site's own links or sitemap. belongs in links from outside, never in the site’s own links or its sitemap.
  • The print view must keep working too, so it keeps its canonical to the page: for copies within a site Google advises a canonical, not noindex.
  • The old shop’s address, product.php?id=482, still answers 200: a copy nobody needs, so a 301.
  • The Marathi page is a page of its own and names itself; the lesson on international SEO joins such pages with hreflang.

A fragment is not on the list at all. /cakes/mango-mousse#reviews never reaches the server, and Google generally does not support fragments, so it is the same address to Google and never a canonical.

Change one rule

Press Edit, delete "ref" from TRACKING and run again: ?ref=menu no longer counts as a tracking link that must keep working, so the program asks for a fourth 301 and no canonical tag, and that redirect would drop the parameter before the page could record which menu link was used. Whether a parameter belongs in that list depends on one question, whether it changes what the page shows; ref only records which menu link was used.

Canonical Tag Generator Turn one of your own addresses, or a list of them, into the clean canonical address and its link tag or Link header.

Checkpoint

Check: Causes of copies and their fixes

  1. Question 1 of 2 /cakes/mango-mousse/ answers 200 with the same page as /cakes/mango-mousse, and nothing needs the address with the slash. What does it need?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A 301 to /cakes/mango-mousse

    The program found three copies of this kind, the slash, the capitals and the old product.php address, each needing a 301.

  2. Question 2 of 2 Are /cakes/Mango-Mousse and /cakes/mango-mousse the same address to Google?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No; the path of an address is case-sensitive

    If a server answers every spelling with the same page, Google asks the site to use one case everywhere; the bakery redirects the other spellings to it.

A canonical tag counts only if Google can use it

The link element looks simple, <link rel="canonical" href="https://punebakes.example/cakes/mango-mousse">, and most of its failures are invisible in a browser. Google accepts it only in the <head>, and it reads the head only as far as the first element that does not belong there: the valid ones are title, meta, link, script, style, base, noscript and template, and at anything else, such as an img or an iframe, Google assumes the head has ended and ignores what follows (valid page metadata). Google also asks for absolute addresses, accepts the relative ones but warns that they can cause problems later, for example when a test copy of the site gets crawled, and asks for one method per page, the element or the header, because using both is more error-prone. A canonical link that carries an hreflang, lang, media or type attribute is not used for canonicalisation at all.

The header form is the one for files: Link: <https://punebakes.example/menu/>; rel="canonical", with the address in angle brackets (specify a canonical). The link relation itself is defined in RFC 6596. The program below checks eight of the bakery’s declarations, using Python’s HTML parser (html.parser) and a list of how each target answers:

Eight canonical declarations checked Python · tags/check_tags.py
"""Check the canonical declarations of eight Pune Bakes addresses against Google's rules.

Each page lists the HTML its server sends (only the start of it) and any Link header. ANSWERS says how each target
answers. A canonical counts only in a valid head: Google stops reading the head at the first element that does not
belong there, such as an img. The target should be an absolute address that answers 200 and may be indexed.
"""

from html.parser import HTMLParser

SITE = "https://punebakes.example"
HEAD_ELEMENTS = {"title", "meta", "link", "script", "style", "base", "noscript", "template"}
ANSWERS = {  # address -> (status, carries noindex)
    "/cakes/rasmalai-cake": (200, False),
    "/cakes/eggless-truffle": (200, False),
    "/cakes/blueberry-cake": (301, False),
    "/offers/": (200, True),
    "/cakes/coffee-walnut-cake": (404, False),
    "/cakes/mango-mousse": (200, False),
    "/menu/": (200, False),
}
C = '<link rel="canonical" href="https://punebakes.example'  # the start of an absolute canonical link
PAGES = [  # address, the start of the HTML its server sends, its Link header (or None)
    ("/cakes/rasmalai-cake", f'<head><title>Rasmalai cake</title>{C}/cakes/rasmalai-cake"></head>', None),
    ("/cakes/eggless-truffle", '<head><link rel="canonical" href="/cakes/eggless-truffle"></head>', None),
    ("/cakes/red-velvet-heart",
     f'<head><title>Red velvet</title><img src="/badge.png">{C}/cakes/red-velvet-heart"></head>', None),
    ("/cakes/mango-mousse/", f'<head>{C}/cakes/mango-mousse">{C}/cakes/mango-mousse/"></head>', None),
    ("/cakes/blueberry-cheesecake", f'<head>{C}/cakes/blueberry-cake"></head>', None),
    ("/offers/diwali-hampers", f'<head>{C}/offers/"></head>', None),
    ("/cakes/coffee-walnut", f'<head>{C}/cakes/coffee-walnut-cake"></head>', None),
    ("/menu.pdf", "", '<https://punebakes.example/menu/>; rel="canonical"'),
]


class Head(HTMLParser):
    """The canonical links Google reads: those before the head ends, or before an element that ends it early."""

    def __init__(self, html):
        super().__init__()
        self.read, self.ignored, self.open, self.ended_by = [], [], True, None
        self.feed(html)

    def handle_starttag(self, tag, attrs):
        if self.open and tag not in HEAD_ELEMENTS | {"head"}:
            self.open, self.ended_by = False, tag  # Google assumes the head ended here
        a = dict(attrs)
        if tag == "link" and a.get("rel") == "canonical":
            (self.read if self.open else self.ignored).append(a.get("href", ""))

    def handle_endtag(self, tag):
        self.open = self.open and tag != "head"


def problems(address, html, header):
    head = Head(html)
    named = head.read + ([header.split(">")[0].lstrip("<")] if header else [])
    found = [f"canonical after <{head.ended_by}> is ignored: the head ended there" for _ in head.ignored]
    if len(set(named)) > 1:
        found.append(f"{len(named)} canonicals that disagree")
    for href in dict.fromkeys(named):  # each address once, in the order the page names them
        target = href.removeprefix(SITE)
        if not href.startswith("https://"):
            found.append(f"{href} is relative: write the absolute address")
        status, noindex = ANSWERS.get(target, (None, False))
        if status not in (200, None):
            found.append(f"the target {target} answers {status}")
        if noindex:
            found.append(f"the target {target} carries noindex")
    if not named and not head.ignored:
        return ["no canonical"]
    return found or ["ok"]


for address, html, header in PAGES:
    print(address)
    for problem in problems(address, html, header):
        print(f"  {problem}")

Output

/cakes/rasmalai-cake
  ok
/cakes/eggless-truffle
  /cakes/eggless-truffle is relative: write the absolute address
/cakes/red-velvet-heart
  canonical after <img> is ignored: the head ended there
/cakes/mango-mousse/
  2 canonicals that disagree
/cakes/blueberry-cheesecake
  the target /cakes/blueberry-cake answers 301
/offers/diwali-hampers
  the target /offers/ carries noindex
/cakes/coffee-walnut
  the target /cakes/coffee-walnut-cake answers 404
/menu.pdf
  ok

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_tags.py

Two pass: the rasmalai cake’s tag and the menu PDF’s header. The other six each have a different problem. The red velvet page’s tag sits after an img that a badge plugin put in the head, so Google never reads it. The mango mousse copy has two tags that disagree, one from the theme and one from a plugin. And three name a target that should never be a canonical: an address that redirects, one that carries noindex and one that answers 404. Each contradicts itself: it names, as the page to show, an address that the site redirects, keeps out of results or no longer has.

A relative canonical

The eggless truffle page says <link rel="canonical" href="/cakes/eggless-truffle">. Google accepts it, but the same template on a crawlable test copy of the site would then name the test copy’s own address as canonical. Write the full address, https://punebakes.example/cakes/eggless-truffle, so that every copy of the template names the live page.

A page built by JavaScript adds one more rule. The best place for the canonical is the HTML the server sends; a script that sets it must set the same value, or the original HTML must leave it out (JavaScript SEO basics). The lesson on JavaScript SEO at the end of this module shows why.

Checkpoint

Check: A canonical Google can use

  1. Question 1 of 2 Does Google use the canonical in this head?

    Read the code, then choose one answer.

    <head>
      <title>Red velvet heart cake</title>
      <img src="/badge.png" alt="Bestseller">
      <link rel="canonical" href="https://punebakes.example/cakes/red-velvet-heart">
    </head>
    Show the answer: Question 1 of 2

    Answer: No; the img ends the head for Google, so the link after it is ignored

    Move the img out of the head, or at least put it after the elements Google must read, as the checker reported.

  2. Question 2 of 2 /cakes/blueberry-cheesecake names /cakes/blueberry-cake as its canonical, but that old address now answers 301 to /cakes/blueberry-cheesecake. What should the tag say?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: https://punebakes.example/cakes/blueberry-cheesecake, the page itself

    A canonical names a page that answers 200 without noindex. After a rename, update the tag along with the redirect.

When signals disagree, Google picks its own canonical

The bakery’s cakes listing answers 200 at three addresses, and its signals name all three. The program lists every signal it sends today, then makes the changes that point them all at the one address the bakery prefers, /cakes/: a folder keeps its slash, and the preferred address carries no tracking parameter:

The cakes listing's signals, before and after Python · signals/check_signals.py
"""Which address does each signal of Pune Bakes' cakes listing name, and what makes them agree?

The listing answers 200 at three addresses. A crawl of the site counted the internal links to each one; the page's
canonical tag, the sitemap and the server's redirects are as they are today. The program lists every signal, says
whether they agree, and then makes the changes that point them all at one preferred address.
"""

SITE = {
    "redirects": {},  # address -> where it redirects (none yet)
    "canonical tag": "/cakes/?ref=menu",  # what <link rel="canonical"> on the listing says
    "sitemap": ["/cakes/"],
    "internal links": {"/cakes": 46, "/cakes/": 3, "/cakes/?ref=menu": 1},  # links found on the site's pages
}
PREFERRED = "/cakes/"  # the bakery's rule: a folder keeps its slash, and no tracking parameters


def votes(site):
    """(signal, the address it names) for every signal the listing sends."""
    out = [("redirect", target) for target in site["redirects"].values()]
    out.append(("canonical tag", site["canonical tag"]))
    out += [("sitemap", address) for address in site["sitemap"]]
    for address, count in site["internal links"].items():
        out.append((f"{count} internal link{'s' * (count > 1)}", address))
    return out


def report(title, site):
    named = votes(site)
    print(title)
    for signal, address in named:
        print(f"  {signal:<18} {address}")
    addresses = sorted({address for _, address in named})
    if len(addresses) == 1:
        print(f"  Every signal names {addresses[0]}.")
    else:
        print(f"  The signals name {len(addresses)} addresses: {', '.join(addresses)}. Google must choose.")


def agree(site, preferred):
    """The changes that make every signal name the preferred address."""
    others = [a for a in site["internal links"] if a != preferred]
    moved = sum(site["internal links"][a] for a in others)
    changes = [f"301 from {a} to {preferred}" for a in others if "?" not in a]
    changes.append(f"canonical tag: {preferred} instead of {site['canonical tag']}")
    changes.append(f"{moved} internal links: {preferred} instead of {' or '.join(others)}")
    fixed = {
        "redirects": {a: preferred for a in others if "?" not in a},
        "canonical tag": preferred,
        "sitemap": [preferred],
        "internal links": {preferred: sum(site["internal links"].values())},
    }
    return changes, fixed


report("Today:", SITE)
changes, fixed = agree(SITE, PREFERRED)
print("\nChanges:")
for n, change in enumerate(changes, 1):
    print(f"  {n}. {change}")
print()
report("After the changes:", fixed)

Output

Today:
  canonical tag      /cakes/?ref=menu
  sitemap            /cakes/
  46 internal links  /cakes
  3 internal links   /cakes/
  1 internal link    /cakes/?ref=menu
  The signals name 3 addresses: /cakes, /cakes/, /cakes/?ref=menu. Google must choose.

Changes:
  1. 301 from /cakes to /cakes/
  2. canonical tag: /cakes/ instead of /cakes/?ref=menu
  3. 47 internal links: /cakes/ instead of /cakes or /cakes/?ref=menu

After the changes:
  redirect           /cakes/
  canonical tag      /cakes/
  sitemap            /cakes/
  50 internal links  /cakes/
  Every signal names /cakes/.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 check_signals.py

Today the canonical tag names /cakes/?ref=menu, the sitemap names /cakes/, and 46 of the site’s 50 links name /cakes. With three answers to one question, Google decides for itself, and Search Console reports it in the Page indexing report: The Search Console report that lists a site's pages Google has and has not indexed, with a reason for each page left out, such as a robots.txt block, a noindex rule or a duplicate. More in “How Google Search works: crawl, index, serve” (Page indexing report):

Reason in the report What it means What to do
Duplicate, Google chose different canonical than user The page is marked as canonical, but Google thinks another address makes a better one Inspect it, compare both canonicals, fix the signals
Duplicate without user-selected canonical A copy that names no canonical; Google chose another address Usually nothing; name a canonical if Google chose wrong
Alternate page with proper canonical tag A copy that names its indexed canonical correctly Nothing

The first reason sends you to the URL Inspection tool: The Search Console tool that shows what Google has stored about one URL, from its last crawl to the canonical Google chose, and can test the live page now or ask for it to be indexed. More in “Set up a site for search and Search Console”, which shows the user-declared canonical beside the Google-selected canonical: The address that URL Inspection reports as the canonical Google chose for a page's group of duplicates. It can differ from the user-declared canonical, the one the site names.; Google’s choice can be read only in the indexed result, never in a live test (URL Inspection tool). Step through the bakery’s case:

Interactive · message by message Google chose a different canonical Step through what the bakery's owner does when Search Console says Google chose another canonical for the cakes listing.
Google chose a different canonical, step 7 of 7. Messages so far: The bakery to Search Console: inspect ?ref=menu; Search Console to The bakery: chose /cakes/; The bakery to Search Console: request indexing /cakes/; Googlebot to The bakery: GET /cakes/.Search Console?ref=menu: duplicateThe bakery4 signals, 1 addressGooglebotinspect ?ref=menuchose /cakes/request indexing /cakes/GET /cakes/

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The report. The Page indexing report lists /cakes/?ref=menu under "Duplicate, Google chose different canonical than user". The listing's canonical tags name that address, but Google thinks another one makes a better canonical.
  2. Inspect the address. The bakery's owner pastes /cakes/?ref=menu into URL Inspection. It shows both choices side by side; the Google-selected canonical can be read only in the indexed result, not in a live test.
  3. Two canonicals. User-declared canonical, /cakes/?ref=menu; Google-selected canonical, /cakes/. Google chose the address the sitemap names over the tracking address the tags name.
  4. Read it as evidence. Google's choice is not a penalty. Here it is even the better address, so the owner fixes the signals to agree with it rather than fighting it.
  5. Make every signal agree. The canonical tags change to /cakes/, 47 internal links follow, and /cakes answers with one 301 to /cakes/. The sitemap already lists /cakes/.
  6. Ask for a recrawl, once. The owner requests indexing of /cakes/ in URL Inspection. There is a daily limit, so it is kept for the most important addresses and is never repeated for the same one.
  7. Wait for the recrawl. Googlebot fetches the listing again and finds every signal naming /cakes/. The report changes only after the recrawl, so the owner waits instead of changing the tags back and forth.

Here Google’s choice was the better address, and the fix was to make the site agree with it. When Google’s choice is the wrong one, Google’s troubleshooting guide gives the same order of work (Fix canonicalization issues): check which canonical Google chose and whether it serves searchers better; look for faulty tags, server faults and missing hreflang; make pages that should not be grouped clearly different, since Google may keep pages grouped as duplicates for up to two weeks after such a change; then request indexing for the most important addresses, because the requests have a daily limit. Changing the signals back and forth gives Google a new mix to weigh at every recrawl.

Indexability Checker Check one address of your own site: its status, redirects, robots.txt, noindex and the canonical it names.

Checkpoint

Check: When Google chooses another canonical

  1. Question 1 of 2 Where can you read the canonical that Google selected for a page?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In URL Inspection's indexed result, as the Google-selected canonical

    Compare the two: the user-declared canonical is what the site says, the Google-selected canonical what Google chose.

  2. Question 2 of 2 The print view of the mango mousse page is listed under "Alternate page with proper canonical tag". What should you do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing; it names its indexed canonical correctly

    Only "Duplicate, Google chose different canonical than user" calls for work: an inspection and a look at the signals.

Practise: plan one product’s 15 addresses

Spice Box’s Kashmiri chilli powder answers at 15 addresses, and two of today’s redirects form chains. Write plan() so that every address gets either one 301 straight to its page or a canonical tag that names it; the sample tests check that every redirect reaches a page in one hop and that tracking links and the print view keep working:

Exercise · Medium · Python

Plan the canonicals and redirects of 15 addresses

Spice Box's Kashmiri chilli powder answers at 15 addresses. CRAWL in canonical_map.py lists each one as a crawl found it, with its status today and, for a redirect, where it points today. Two of today's redirects form chains.

Write plan() so that it returns two dictionaries, redirects (address → target) and canonicals (address → the address its canonical tag names), with every address in exactly one of them:

  • The preferred address of a page is https, without www, in lower case, without a slash at the end of a product, without tracking parameters (utm_*, gclid, fbclid, ref) and without the /print folder; an item.php address becomes the path of its product in OLD_IDS. A lang parameter changes the page, so it stays.
  • The preferred address of each page answers 200 and names itself as its canonical.
  • Tracking links and the print view must keep working as they are: they answer 200 with a canonical tag that names their page's preferred address.
  • Every other address gets one 301 straight to its page's preferred address, including the addresses that redirect today, so that no redirect leads to another redirect.

The sample tests check that every address is planned once, that only the product and its Hindi page name themselves, that every redirect reaches a page in one hop, and that tracking links and the print view keep working.

Starter code · canonical_map.py

from urllib.parse import parse_qsl, urlencode, urlsplit

SITE = "https://spicebox.example"
TRACKING = ("utm_", "gclid", "fbclid", "ref")  # parameters that never change what the page shows
OLD_IDS = {"71": "/spices/kashmiri-chilli"}  # the old shop's item.php addresses
P = SITE + "/spices/kashmiri-chilli"

# address as the crawl found it, its status today, and where it redirects today (None: it does not)
CRAWL = [
    (P, 200, None),
    ("http://spicebox.example/spices/kashmiri-chilli", 301, "https://www.spicebox.example/spices/kashmiri-chilli"),
    ("https://www.spicebox.example/spices/kashmiri-chilli", 301, P),
    ("http://www.spicebox.example/spices/kashmiri-chilli", 301, P),
    (P + "/", 200, None),
    (SITE + "/Spices/Kashmiri-Chilli", 200, None),
    (P + "?utm_source=newsletter&utm_campaign=diwali", 200, None),
    (P + "?gclid=Cj0KCQiA", 200, None),
    (P + "?ref=homepage", 200, None),
    (SITE + "/print/spices/kashmiri-chilli", 200, None),
    (SITE + "/item.php?id=71", 200, None),
    (P + "?lang=hi", 200, None),
    (P + "?utm_source=whatsapp&lang=hi", 200, None),
    (SITE + "/spices/chilli-kashmiri", 301, SITE + "/spices/kashmiri-chilli-powder"),
    (SITE + "/spices/kashmiri-chilli-powder", 301, P),
]


def plan():
    """Return (redirects, canonicals): address -> 301 target, and address -> the address its canonical tag names."""
    redirects = {address: target for address, status, target in CRAWL if status == 301}
    canonicals = {address: address for address, status, _ in CRAWL if status == 200}
    return redirects, canonicals
The sample tests · test_canonical_map.py
from canonical_map import CRAWL, SITE, plan

PRODUCT = SITE + "/spices/kashmiri-chilli"
HINDI = PRODUCT + "?lang=hi"
KEEP = [a for a, _, _ in CRAWL if "/print/" in a or any(p in a for p in ("utm_", "gclid=", "ref="))]


def test_every_address_planned_once():
    """puts each of the 15 addresses in redirects or in canonicals, never in both"""
    redirects, canonicals = plan()
    addresses = {a for a, _, _ in CRAWL}
    assert set(redirects) | set(canonicals) == addresses
    assert not set(redirects) & set(canonicals)


def test_only_two_pages_name_themselves():
    """the product and its Hindi page are the only addresses that are their own canonical"""
    _, canonicals = plan()
    assert {a for a, c in canonicals.items() if a == c} == {PRODUCT, HINDI}


def test_redirects_reach_a_page_in_one_hop():
    """every 301 goes straight to a page that answers 200 and names itself"""
    redirects, canonicals = plan()
    for address, target in redirects.items():
        assert target not in redirects, f"{address} leads to another redirect"
        assert canonicals.get(target) == target, f"{address} goes to {target}, which is not a page"


def test_copies_nobody_needs_are_redirected():
    """the other hosts, the slash, the capitals, item.php and the old names all get a 301 to the product"""
    redirects, _ = plan()
    for path in ["http://spicebox.example/spices/kashmiri-chilli", PRODUCT + "/", SITE + "/Spices/Kashmiri-Chilli",
                 SITE + "/item.php?id=71", SITE + "/spices/chilli-kashmiri"]:
        assert redirects.get(path) == PRODUCT, path


def test_tracking_links_and_print_view_keep_working():
    """tracking links and the print view answer 200 with a canonical that names their page"""
    redirects, canonicals = plan()
    assert len(KEEP) == 5
    for address in KEEP:
        assert address not in redirects, address
        assert canonicals[address] == (HINDI if "lang=hi" in address else PRODUCT), address
A hint

Follow today's redirects to the address they finally reach before you work out the preferred address, so that chilli-kashmiri, which points to kashmiri-chilli-powder, which points to the product, goes straight to the product. Then ask one question of each address: is it the preferred address itself, a copy that must keep working (tracking parameters or /print/), or a copy nobody needs, which gets the 301?

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then practise the five ideas of this lesson, starting with the one your answers show is weakest:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 What does Google compare to decide that pages belong to one group of duplicates?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: Their main content, not the header, menu or footer

    The program did the same: it grouped the pages by the text of their main element.

  2. Question 2 of 15 A copy of the bakery's site at staging.punebakes.example is open to crawlers. Which of Google's reasons for duplicates is it, and what keeps it out?

    Choose one answer.

    Show the answer: Question 2 of 15

    Answer: An accidental variant; a login keeps crawlers and visitors out of it

    Protect test copies with a login, as the lesson on indexing controls advised for anything that must stay private.

  3. Question 3 of 15 A shop translates only the menu and the footer of its product pages into Hindi and serves them at ?lang=hi, with the descriptions still in English. What follows?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: They stay duplicates of the English pages, because the main content is in English

    To have Hindi pages that stand alone, translate the main content, then join the language versions with hreflang.

  4. Question 4 of 15 Which of these does Google call a weak canonical signal?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: Listing the page in the sitemap

    Use the sitemap alongside a strong signal, never instead of one.

  5. Question 5 of 15 A developer adds noindex to the print view so that it never shows in results. What does Google advise instead, and why?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: A canonical to the page, because noindex would block the print view from Search completely and name no main page

    The canonical keeps the print view working for readers and points Google at the page.

  6. Question 6 of 15 The bakery's HTTPS pages load their scripts from http:// addresses. What does that do to Google's preference for HTTPS as canonical?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: It can undo it; insecure dependencies other than images are one of the exceptions

    Load every script and style over HTTPS, redirect HTTP to HTTPS and keep only HTTPS addresses in the sitemap.

  7. Question 7 of 15 Where should tracking parameters such as utm_source never appear?
    Show the answer: Question 7 of 15

    Answer: In the site's own links and in its sitemap

    Tracking parameters belong to links from outside; every signal the site sends itself names the clean address.

  8. Question 8 of 15 The old address product.php?id=482 still answers 200 with the mango mousse page. Only old bookmarks use it. What does it need?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: A 301 to /cakes/mango-mousse

    The program treated it like the slash and the capitals: a copy nobody needs, so a 301.

  9. Question 9 of 15 The listing /cakes/?sort=price shows the same cakes in another order, and shoppers use it. What does it need?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: A canonical to /cakes/, while it keeps answering 200 for shoppers

    Sorted and filtered lists are what Google calls site functions; the lesson on site architecture deals with filters that create many such addresses.

  10. Question 10 of 15 Which of these elements makes Google assume that the head has ended?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: iframe

    Keep the head to the eight valid elements, and put anything else in the body.

  11. Question 11 of 15 A theme and an SEO plugin each add a canonical tag, and they name different addresses. What should you do?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: Turn one of them off, so that the page declares one canonical

    The checker found exactly this on the slash copy of the mango mousse page: two canonicals that disagree.

  12. Question 12 of 15 A developer marks the Marathi page with this tag. What does Google do with it?

    Read the code, then choose one answer.

    <link rel="canonical" hreflang="mr" href="https://punebakes.example/cakes/mango-mousse?lang=mr">
    Show the answer: Question 12 of 15

    Answer: It does not use it for canonicalisation, because it carries an hreflang attribute

    Write a plain canonical without the attribute, and mark language versions with rel="alternate" and hreflang.

  13. Question 13 of 15 What does "Duplicate, Google chose different canonical than user" mean in the Page indexing report?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: The page is marked as canonical, but Google thinks another address makes a better canonical

    Inspect the address to see the Google-selected canonical, then look at every signal that names each address.

  14. Question 14 of 15 After the signals are fixed, how often should you request indexing of the same address?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: Once, for the most important addresses, because requests have a daily limit

    For many pages at once, an updated sitemap is the tool, as the sitemaps lesson showed.

  15. Question 15 of 15 Google grouped two cakes with nearly the same description, and chose the plain one as canonical for the gift version too. The gift version should stand on its own. What do you change?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Make the gift version's main content clearly different, then request indexing, and allow up to two weeks

    Pages that should stand alone need content that differs clearly and significantly; then the signals can do their job.

Interview questions

Warm-up (fresher to 2–5 years): does Google penalise duplicate content?

Not as such. Google says some duplicate content on a site is normal, and its starter guide says duplicate content is not a violation of its spam policies and will not cause a manual action. What Google does is group the copies and show one address of each group, the canonical, which it chooses from the site’s signals: redirects, canonical tags, the sitemap, HTTPS and internal links. The real costs are control and waste: if the signals disagree, Google may show an address the site did not want, and crawling the copies takes time that new pages could use. Copying content from other sites or publishing near-identical pages to catch many searches is a different matter, which the spam policies cover. So the fix for ordinary duplicates is a redirect or a canonical, never a fear of penalties.

Two more questions follow, about a shop whose filter addresses Google chose as canonical and a product sold in three categories. Make the first decision of each before you open it:

Checkpoint

Your first decision

  1. Question 1 of 2 Interview brief: Search Console reports "Duplicate, Google chose different canonical than user" for 180 product pages, and Google chose their ?colour= addresses. What do you look at first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: URL Inspection of a sample, to compare the declared and the selected canonical, then every signal that names the ?colour= addresses

    Read the evidence before changing anything; the fix is to make every signal name the product's preferred address.

  2. Question 2 of 2 Interview brief: one cake is sold under /birthday/, /eggless/ and /anniversary/, at three addresses with the same page. What do you decide first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: One preferred address for the cake, which every category links to and every copy names

    The cleanest fix gives the product one address, such as /cakes/rasmalai-cake, and lets each category link to it.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Search Console reports "Duplicate, Google chose different canonical than user" for 180 product pages of a fashion shop, and Google chose their ?colour= addresses instead. How do you investigate, and what do you change?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A bakery's shop software gives a cake one address per category it is in, so the rasmalai cake answers at /birthday/rasmalai-cake, /eggless/rasmalai-cake and /anniversary/rasmalai-cake. How do you choose the canonical, and what else has to change?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Google groups pages with the same main content and shows one address of each group, the canonical; duplicates are normal and not a penalty, and a page in another language is not a duplicate.
  • Redirects and canonical tags are strong signals and a sitemap is a weak one; HTTPS and internal links count too, and robots.txt, noindex and the removal tool are not ways to choose a canonical.
  • A copy nobody needs gets one 301; a copy that must keep working, such as a tracking link or a print view, keeps its canonical tag; the preferred page names itself.
  • A canonical counts only in a valid head, written as a full address, declared once, and naming a page that answers 200 without noindex.
  • When signals disagree, Google chooses for itself: URL Inspection shows the declared and the selected canonical, and the fix is to make every signal name one address, then wait for the recrawl.

Terms in this lesson

canonicalisation
How Google picks one representative address from a group of pages with the same main content. A site can state its choice with redirects, canonical tags and its sitemap, but Google treats the choice as a hint.
duplicate content
The same or nearly the same main content at more than one address. Within a site it is normal and not a spam-policy violation: Google groups the copies and shows one address of each group.
Google-selected canonical
The address that URL Inspection reports as the canonical Google chose for a page's group of duplicates. It can differ from the user-declared canonical, the one the site names.
rel=canonical
A link element in a page's head, or a Link header for files such as PDFs, that names the address a site prefers for a group of duplicate pages. Google counts it as a strong signal, not a rule.
self-referencing canonical
A canonical tag on the preferred page that names the page itself, so that copies created by tracking or sorting parameters still name the right address.
tracking parameter
A query parameter such as utm_source, gclid or fbclid that records where a visit came from and does not change what the page shows. It belongs in links from outside, never in a site's own links or sitemap.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.