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

Steps

  1. Step 1.

    Read the first hop for each of the four entry points on a site that has this configured. Measured against cloudflare.com on 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 200

    Three 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. The 103 is an early hints response sent before the 200, which is why that entry shows two status lines for one request.

  2. 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 200

    Two 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.

  3. 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.pem
    
    subject=CN = localhost
    X509v3 Subject Alternative Name:
      DNS:localhost, IP Address:127.0.0.1

    localhost stands in for the www host and 127.0.0.1 for the apex, which gives two hostnames without touching DNS. In Git Bash, prefix the first command with MSYS_NO_PATHCONV=1 or the -subj value is rewritten as a path.

  4. Step 4.

    Start the target. Save as host-canon-target.js and run node host-canon-target.js. It upgrades plain requests to HTTPS, then sends the apex to the www host with a hardcoded http:// 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 OK

    Three 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:.

  5. 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"; done
    
    http://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.

  6. 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://')"; done
    
    http://127.0.0.1:8934/ 1
    http://localhost:8934/ 0
    https://127.0.0.1:8935/ 1
    https://localhost:8935/ 0

    The pattern ^location: http:// cannot match https://, 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"; done
    
    https://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/pricing

    Two 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

Sign: A monitoring check asserts the final URL for all four entry points and passes, and a request still goes out in clear text.Cause: The final URL says nothing about the hops before it. Step 5 shows all four entries ending on the same HTTPS URL while two of them pass through http://localhost on the way. Count the intermediate Location values as in step 6.
Sign: www canonicalisation works on the home page and not on the rest of the site.Cause: The rule was written to match the root path. Step 6 shows /pricing answering 200 on both hostnames while / redirects correctly. Always test one deep path in the matrix.
Sign: A canonical tag names the www host, and both hostnames are still indexed.Cause: A canonical is a hint about which URL to prefer. It does not stop the other hostname answering 200, and it does not move a visitor. The redirect is the part that enforces the decision, and it has to exist on both schemes.
Sign: Testing from a browser shows one hostname and hides the problem.Cause: HSTS and the browser cache rewrite an http entry to https before a request leaves the machine, so the cleartext hop never appears in the Network panel on a second visit. curl carries no HSTS state between runs, which is why the matrix is run from the command line.

What to check next

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.

intermediate9 minpublished updated Maks Verny