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
| Signal | Weight Google gives it | Basis |
|---|---|---|
| A redirect to the target | A strong signal that the target should be canonical | Google's duplicate-URL guide |
rel="canonical" annotation | A strong signal that the specified URL should become canonical | Same guide |
| Inclusion in a sitemap | A weak signal that helps included URLs become canonical | Same guide |
| HTTPS over HTTP, and hreflang clusters | Also used as canonicalization signals | Same guide |
| Content similarity | Google decides which pages are duplicates | Same 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
- Does the canonical URL return 200 itself? Request it without following redirects. If it answers 301, the tag points at a redirect.
- Does the page's served URL equal its canonical? After redirects, the URL you land on should be the one the tag names.
- Does the sitemap list the canonical form? A sitemap full of URLs that redirect is a list of non-canonical URLs.
- Do internal links use the canonical form? Linking to a variant votes for the variant.
- 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.
- 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.
| Check | URLs failing |
|---|---|
| Sitemap URL itself redirects | 38 |
| Canonical differs from the URL actually served | 38 |
| The canonical URL itself redirects | 38 |
| URLs with no problem | 6 (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.
.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
| Finding | Fix |
|---|---|
| Sitemap URL redirects | List the final URL in the sitemap instead |
| Canonical differs from the URL served | Change the tag to the URL the server actually returns with a 200 |
| Canonical URL itself redirects | Same change; the canonical must be a page that answers 200 |
| Canonical not in the sitemap | Add it, or confirm the page should not be indexed |
| No canonical at all | Add 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.