A canonical tag is a strong signal, not an instruction. Google says redirects and rel="canonical" are strong signals and sitemap inclusion is a weak one, and it also prefers HTTPS and uses hreflang clusters. When they disagree, Google chooses. Check that your canonical URL returns 200 itself, that redirects land on the same URL, that the sitemap lists it, and that internal links and duplicates agree.

Key takeaways

  • Canonicalization uses several signals. Per Google: redirects and rel="canonical" are strong signals, sitemap inclusion is a weak one, and Google also prefers HTTPS and uses hreflang clusters.
  • None of the methods is required, and Google decides which pages are duplicates by the similarity of their content.
  • The most common self-inflicted bug is a canonical that redirects. If the URL in your canonical tag answers with a 301 to somewhere else, the signals contradict each other.
  • This site had exactly that. The host redirects .html URLs to the clean form, and 38 of 44 sitemap URLs had a canonical and a sitemap entry pointing at the redirecting version.
  • A script finds it in one pass. It checks every sitemap URL for redirects, canonical mismatch, a redirecting canonical and a canonical missing from the sitemap.
  • Fix the signals, not the status. Make the canonical, the sitemap and the redirects all name the same final URL.

The signals, and how much weight each has

From Google's documentation on consolidating duplicate URLs and the Page indexing report.
Signal Weight Google gives it Basis
A redirect to the targetA strong signal that the target should be canonicalGoogle's duplicate-URL guide
rel="canonical" annotationA strong signal that the specified URL should become canonicalSame guide
Inclusion in a sitemapA weak signal that helps included URLs become canonicalSame guide
HTTPS over HTTP, and hreflang clustersAlso used as canonicalization signalsSame guide
Content similarityGoogle decides which pages are duplicatesSame guide

Google also says none of these methods is required. A canonical is therefore a request that Google weighs against everything else it sees. Search Console records the outcome in the Page indexing report: "Duplicate, Google chose different canonical than user" means the page is marked as canonical for a set of pages but Google thinks another URL is better; "Page with redirect" is a non-canonical URL that redirects to another page. The URL Inspection tool shows the user-declared canonical and the Google-selected canonical side by side.

Six checks, in the order that finds the cause

  1. Does the canonical URL return 200 itself? Request it without following redirects. If it answers 301, the tag points at a redirect.
  2. Does the page's served URL equal its canonical? After redirects, the URL you land on should be the one the tag names.
  3. Does the sitemap list the canonical form? A sitemap full of URLs that redirect is a list of non-canonical URLs.
  4. Do internal links use the canonical form? Linking to a variant votes for the variant.
  5. Are there near-duplicates? Two pages with the same content under different URLs are the case canonicals exist for; see the Crawled guide for merging them.
  6. Which variants exist? Check HTTP and HTTPS, www and non-www, trailing slashes, /index.html, upper and lower case, and tracking parameters, and make each redirect to one form.

A real case: this site

While writing this guide the checker below was run against this site's live sitemap on 30 October 2026. The host (Cloudflare) answers /seo/seo-audit-checklist.html with a 301 to /seo/seo-audit-checklist, and serves the clean URL with a 200. Every page, however, declared the .html URL as its canonical, listed the .html URL in the sitemap, and linked to it internally.

Checker results on the live sitemap, before the fix. 44 URLs.
Check URLs failing
Sitemap URL itself redirects38
Canonical differs from the URL actually served38
The canonical URL itself redirects38
URLs with no problem6 (the home page and directory-style section pages)

That is the contradiction in its purest form: each page's canonical named a URL that redirected back to the page that declared it. Nothing in Google's documentation says how it resolves a loop like that, and I have no Search Console data showing which URL it picked, so the effect is untested. The signals were inconsistent, and that is reason enough to fix them.

The fix applied was to make the canonical, og:url, structured-data URLs, sitemap and llms.txt all use the clean URL the host serves. The checker was then run against the fixed files, served with the same redirect rules on a local test server: 0 of 51 sitemap URLs had an issue. The live site has not yet been redeployed with that change as this was written, so the "before" numbers above describe what is live.

What the fix does not change. Internal links on this site still end in .html, so each click takes one redirect hop. They stay that way so the pages work when opened from disk, which is a deliberate trade-off, not an oversight. The cleaner alternative is internal links that match the canonical exactly.

The checker script

It reads a sitemap and checks each URL. Run it with the command below:

python3 canonical_check.py https://example.com/sitemap.xml
import re, sys, time, urllib.request, urllib.parse, urllib.error

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        return None

raw = urllib.request.build_opener(NoRedirect)
UA = {'User-Agent': 'canonical-check/1.0'}

def status(url):
    # status and Location of one request, redirects not followed
    try:
        with raw.open(urllib.request.Request(url, headers=UA), timeout=20) as r:
            return r.status, None
    except urllib.error.HTTPError as e:
        return e.code, e.headers.get('Location')

def page(url):
    # final URL and HTML after following redirects
    with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=20) as r:
        return r.geturl(), r.read().decode('utf-8', 'replace')

sitemap = urllib.request.urlopen(urllib.request.Request(sys.argv[1], headers=UA), timeout=20).read().decode()
urls = re.findall(r'<loc>([^<]+)</loc>', sitemap)
problems = 0
for u in urls:
    issues = []
    code, loc = status(u)
    if code in (301, 302, 307, 308):
        issues.append('sitemap URL redirects (%s) to %s' % (code, urllib.parse.urljoin(u, loc or '')))
    try:
        final, html = page(u)
    except Exception as e:
        print('ERROR  %s  %s' % (u, e)); problems += 1; continue
    m = re.search(r'<link[^>]+rel=["\']canonical["\'][^>]*href=["\']([^"\']+)', html)
    canon = urllib.parse.urljoin(final, m.group(1)) if m else None
    if not canon:
        issues.append('no rel=canonical')
    else:
        if canon.rstrip('/') != final.rstrip('/'):
            issues.append('canonical %s differs from the URL served (%s)' % (canon, final))
        ccode, cloc = status(canon)
        if ccode in (301, 302, 307, 308):
            issues.append('canonical URL itself redirects (%s) to %s' % (ccode, urllib.parse.urljoin(canon, cloc or '')))
        elif ccode != 200:
            issues.append('canonical URL returns %s' % ccode)
        if canon not in urls:
            issues.append('canonical is not listed in the sitemap')
    if issues:
        problems += 1
        print('FIX    ' + u)
        for i in issues: print('         - ' + i)
    time.sleep(0.1)
print('\n%d of %d sitemap URLs have at least one issue' % (problems, len(urls)))

On a local test server with a planted bad canonical and a redirecting sitemap URL, it flagged exactly those two. It only reads pages and sends a request or two per sitemap URL, with a short pause, so it is light enough to run on a small site.

What to fix

Checker finding and the usual fix.
Finding Fix
Sitemap URL redirectsList the final URL in the sitemap instead
Canonical differs from the URL servedChange the tag to the URL the server actually returns with a 200
Canonical URL itself redirectsSame change; the canonical must be a page that answers 200
Canonical not in the sitemapAdd it, or confirm the page should not be indexed
No canonical at allAdd a self-referencing canonical, particularly where several URL variants exist
Several hosts return 200 (www and non-www, HTTP and HTTPS)Redirect all but one to the preferred form

If URLs have changed as part of a move, the redirect-map guide has a script for the redirect side, and the SEO audit checklist lists canonical conflicts among its crawlability checks.

What this does not cover

The script finds contradictions, not Google's decision. Only Search Console's URL Inspection shows which canonical Google selected for a URL. The live numbers are a snapshot of one site on one day, taken before the fix was deployed, and the "after" figure comes from a local test server that mimics the host's redirect rules, not from the live site.
Before a redesign ships. A template change can alter every canonical at once; the redesign diff script catches that before launch.

Badri Dutta

Software engineer · 8 years technical SEO

Written after the checker found a real contradiction on this site: every page's canonical named a URL that redirected back to it. Each page and the sitemap looked correct on their own; the conflict only shows when you request the URLs and follow them.

Full background →