How to check www vs non-www canonical URL
Request all four combinations of scheme and host prefix and compare where each one lands: curl -sIL -o /dev/null -w 'hops=%{num_redirects} final=%{url_effective}\n' <url>. One final URL for all four is correct. Two different finals, or any hop on plain http:, is the defect.
Checker offline. Follow the manual steps below, they give the same answer.
Why check this
Run this after a DNS change, a CDN change, a certificate renewal that adds or drops a hostname, and on the day a site goes live. Four entry points exist whether or not anyone configured them: http and https, with and without the www prefix. If two of them answer 200 instead of redirecting, the site has two addresses for every URL, and links, cache entries and ranking signals split between them.
The second failure this finds is a chain that dips back to plain http on its way to the canonical host. The final URL still looks right, while one request per visit travels in clear text.
Prerequisites
- curl 7.x or later.
-Isends HEAD,-Lfollows,-kaccepts the local self-signed certificate. - Node 22 and openssl 3 for the local target in steps 3 to 6.
- RFC 9110 section 15.4 for the redirect status codes.
Steps
- Step 1.
Read the first hop for each of the four entry points on a site that has this configured. Measured against
cloudflare.comon 2026-09-11.for u in http://cloudflare.com/ http://www.cloudflare.com/ https://cloudflare.com/ https://www.cloudflare.com/; do echo "### $u"; curl -sI "$u" | grep -i -E '^HTTP/|^location'; done### http://cloudflare.com/ HTTP/1.1 301 Moved Permanently Location: https://www.cloudflare.com/ ### http://www.cloudflare.com/ HTTP/1.1 301 Moved Permanently Location: https://www.cloudflare.com/ ### https://cloudflare.com/ HTTP/2 301 location: https://www.cloudflare.com/ ### https://www.cloudflare.com/ HTTP/2 103 HTTP/2 200Three entry points name the same destination in one hop, and the fourth is that destination. No hop lands on
http:, so scheme and host are fixed together rather than one after the other. The103is an early hints response sent before the200, which is why that entry shows two status lines for one request. - Step 2.
Compare with a domain that resolves both hostnames and canonicalises neither.
for u in https://example.com/ https://www.example.com/; do echo "### $u"; curl -sI "$u" | grep -i -E '^HTTP/|^location'; done### https://example.com/ HTTP/2 200 ### https://www.example.com/ HTTP/2 200Two hostnames, two 200 responses, no redirect and no canonical on either page. That is the duplicate a www check looks for, and on a site with real content it doubles every URL.
- Step 3.
Build a local target carrying the failure mode. Generate a certificate covering both local hostnames.
openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 2 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"openssl x509 -noout -subject -ext subjectAltName -in cert.pemsubject=CN = localhost X509v3 Subject Alternative Name: DNS:localhost, IP Address:127.0.0.1localhoststands in for thewwwhost and127.0.0.1for the apex, which gives two hostnames without touching DNS. In Git Bash, prefix the first command withMSYS_NO_PATHCONV=1or the-subjvalue is rewritten as a path. - Step 4.
Start the target. Save as
host-canon-target.jsand runnode host-canon-target.js. It upgrades plain requests to HTTPS, then sends the apex to the www host with a hardcodedhttp://prefix, the mistake being reproduced.const http = require('node:http'), https = require('node:https'), fs = require('node:fs'); const APEX = '127.0.0.1', WWW = 'localhost'; const send = (res, loc) => { res.writeHead(301, { location: loc }); res.end(); }; const route = (req, res, secure) => { const host = req.headers.host.split(':')[0]; if (!secure) return send(res, `https://${host}:8935${req.url}`); if (host === APEX && req.url === '/') return send(res, `http://${WWW}:8934/`); res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); res.end(`<!doctype html><title>Host canonicalisation</title>served by ${req.headers.host}${req.url}`); }; http.createServer((q, s) => route(q, s, false)).listen(8934); https .createServer({ key: fs.readFileSync('key.pem'), cert: fs.readFileSync('cert.pem') }, (q, s) => route(q, s, true)) .listen(8935, () => console.log('http on 8934, https on 8935'));curl -ksIL http://127.0.0.1:8934/ | grep -i -E '^HTTP/|^location'HTTP/1.1 301 Moved Permanently location: https://127.0.0.1:8935/ HTTP/1.1 301 Moved Permanently location: http://localhost:8934/ HTTP/1.1 301 Moved Permanently location: https://localhost:8935/ HTTP/1.1 200 OKThree hops to move one prefix. The middle hop is the one to look at: the chain was already on HTTPS and the host rule put it back on
http:. - Step 5.
Reduce all four entry points to one line each.
for u in http://127.0.0.1:8934/ http://localhost:8934/ https://127.0.0.1:8935/ https://localhost:8935/; do echo -n "$u "; curl -ksIL -o /dev/null -w 'hops=%{num_redirects} final=%{url_effective}\n' "$u"; donehttp://127.0.0.1:8934/ hops=3 final=https://localhost:8935/ http://localhost:8934/ hops=1 final=https://localhost:8935/ https://127.0.0.1:8935/ hops=2 final=https://localhost:8935/ https://localhost:8935/ hops=0 final=https://localhost:8935/All four agree on the final URL, so a test asserting only on
%{url_effective}passes. The hop counts are the warning: one prefix and one scheme should not cost three hops. - Step 6.
Count the hops that used plain HTTP, then repeat the matrix on a path below the root.
for u in http://127.0.0.1:8934/ http://localhost:8934/ https://127.0.0.1:8935/ https://localhost:8935/; do echo "$u $(curl -ksIL "$u" | grep -c -i '^location: http://')"; donehttp://127.0.0.1:8934/ 1 http://localhost:8934/ 0 https://127.0.0.1:8935/ 1 https://localhost:8935/ 0The pattern
^location: http://cannot matchhttps://, so a count above zero means a hop was sent to a cleartext URL. Two of the four do that, and the final URL never showed it.for u in https://127.0.0.1:8935/pricing https://localhost:8935/pricing; do echo -n "$u "; curl -ksIL -o /dev/null -w 'hops=%{num_redirects} final=%{url_effective}\n' "$u"; donehttps://127.0.0.1:8935/pricing hops=0 final=https://127.0.0.1:8935/pricing https://localhost:8935/pricing hops=0 final=https://localhost:8935/pricingTwo final URLs for one page, because the host rule in step 4 matches the root only. Test a deep path as well as the home page. Stop both listeners afterwards:
netstat -ano | grep -E ':893[45] 'names the PID to stop.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Four entry points, one final URL, at most one hop each | Scheme and host are fixed in the same rule | Nothing. Pin the four finals in a release test. |
| Two entry points answer 200 on different hostnames | No host canonicalisation at all | Redirect one hostname to the other with 301, as in step 2's counterexample. |
| hops=3 for one entry point | Rules are chained instead of combined | Rewrite as a single redirect to the final scheme and host. |
| A location: value starting http:// | The chain drops to cleartext mid-way | Fix before release. See How to check HTTP to https redirect. |
| Root agrees, a deep path does not | The rule matches / only | Widen the rule and retest with a path, as in step 6. |
| Redirects are 302 | The host move is advertised as temporary | Use 301. Check with How to check if a redirect is 301 or 302. |
Common mistakes
What to check next
- How to check canonical tag: the tag on the surviving URL has to agree with the redirects.
- How to check a self-referencing canonical: the host difference inside a canonical value.
- How to check redirect chain: the general walk, loops and the hop ceiling.
- How to check if a redirect is 301 or 302: the status a permanent host move needs.
- How to check HTTP to https redirect: the scheme half of the matrix on its own.
- Redirect chain checker: the same walk run from a server, hop by hop.
FAQ
How do I check duplicate content on a website?
Start with the URL shapes rather than the text. Request each hostname, each scheme, and one path with and without a trailing slash. Any pair answering 200 with the same body is a duplicate, as in step 2.
Should I use www or non-www?
Either, as long as one redirects to the other everywhere. The choice matters for cookie scope and for pointing the apex at a CDN, not for ranking. Pick one, redirect the other with 301, and make every canonical name the survivor.
Does a canonical tag replace a www redirect?
No. The tag asks a crawler to prefer one URL. The redirect moves every client and stops the duplicate being served at all. Sites with this defect usually have the tag and are missing the redirect.
How many hops should www canonicalisation take?
One from any entry point. Step 5 shows three hops for one entry because the scheme rule and the host rule run in sequence. Combining them into one redirect to the final scheme and host removes two round trips.
Verified
Verified by Maks Vernycurl 8.21.0node 22.23.2openssl 3.1.1
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
- Checker: redirect-chain hops, status codes, final URL, mixed http/https
- All crawlability and indexing checks
intermediate9 minpublished updated Maks Verny