DEV Community

clauxel
clauxel

Posted on

How I Consolidated HTTP, WWW, and index.html URLs with a Cloudflare Worker

Google Search Console's "Page with redirect" report often looks more alarming than it is.

On a static site I maintain, Google discovered several historical forms of the same pages:

http://www.example.com/index.html
http://example.com/index.html
https://www.example.com/index.html
https://example.com/index.html
https://example.com/
Enter fullscreen mode Exit fullscreen mode

Only the last URL was supposed to be indexed. The others were not separate pages; they were alternate entry points left behind by older links, browser history, and previous deployments.

The goal was simple:

Every legacy URL should reach one canonical URL in one permanent redirect, and the canonical page should return 200.

This post shows the Cloudflare Worker pattern I used, the signals I aligned around it, and the checks that kept the redirect logic from creating new problems.

1. Define the URL invariant first

Before writing code, choose one representation for every page.

For this example:

  • HTTPS is mandatory.
  • The apex domain is canonical; www redirects away.
  • The homepage is /, never /index.html.
  • Directory pages end with a slash.
  • Known legacy paths map to their closest equivalent page.

Writing these rules down matters. If the Worker, canonical tags, sitemap, and internal links disagree, crawlers receive conflicting signals.

2. Run the Worker before static assets

With Cloudflare Workers Static Assets, the Worker needs to see the request before an asset is served:

{
  "name": "canonical-url-worker",
  "compatibility_date": "2026-07-27",
  "assets": {
    "directory": "./public",
    "binding": "ASSETS",
    "run_worker_first": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Cloudflare documents the ASSETS binding and run_worker_first behavior in its Static Assets documentation.

3. Normalize host, protocol, and legacy paths in one pass

Here is a reduced version of the redirect middleware:

const CANONICAL_HOST = "example.com";

const legacyPaths = new Map([
  ["/index.html", "/"],
  ["/old-guide", "/guides/new-guide/"],
  ["/old-guide/", "/guides/new-guide/"],
  ["/guides/new-guide/index.html", "/guides/new-guide/"],
]);

export default {
  async fetch(request, env) {
    const incoming = new URL(request.url);
    const target = new URL(incoming);
    let redirect = false;

    if (
      target.protocol !== "https:" ||
      target.hostname === `www.${CANONICAL_HOST}`
    ) {
      target.protocol = "https:";
      target.hostname = CANONICAL_HOST;
      target.port = "";
      redirect = true;
    }

    const mappedPath = legacyPaths.get(target.pathname);
    if (mappedPath) {
      target.pathname = mappedPath;
      redirect = true;
    }

    if (redirect) {
      return Response.redirect(target.toString(), 301);
    }

    return env.ASSETS.fetch(request);
  },
};
Enter fullscreen mode Exit fullscreen mode

The important detail is that protocol, host, and path are normalized before returning a response. A request for:

http://www.example.com/index.html
Enter fullscreen mode Exit fullscreen mode

goes directly to:

https://example.com/
Enter fullscreen mode Exit fullscreen mode

That avoids a redirect chain such as HTTP -> HTTPS -> non-WWW -> root.

The URL copy also preserves the query string by default. Do not delete arbitrary parameters unless you know they are non-functional. Removing a parameter that changes page content can merge pages that are not actually equivalent.

4. Keep redirects specific

It is tempting to redirect every unknown path to the homepage. I avoid that.

A removed page should redirect only when there is a genuinely equivalent destination. Otherwise it should return a proper 404 or 410. Sending every missing URL to / confuses users, hides broken links, and can be treated as a soft 404.

The explicit Map makes migrations reviewable. It also prevents an innocent path rewrite from affecting API endpoints or static files.

5. Align every canonicalization signal

A 301 is strong, but it should not be the only correct signal.

Each final HTML page includes a self-referencing canonical:

<link rel="canonical" href="https://example.com/guides/new-guide/">
Enter fullscreen mode Exit fullscreen mode

The XML sitemap lists only final HTTPS, non-WWW URLs:

<url>
  <loc>https://example.com/guides/new-guide/</loc>
</url>
Enter fullscreen mode Exit fullscreen mode

Internal navigation also links directly to the final URL, not to a URL that needs a redirect.

Google's canonicalization guidance recommends self-referencing canonicals and consistent internal links. Its sitemap documentation likewise says to include the preferred canonical URLs rather than every duplicate form:

6. Test the entire redirect matrix

I use HEAD requests to verify both the source and destination:

curl -I http://www.example.com/index.html
curl -I https://www.example.com/index.html
curl -I https://example.com/index.html
curl -I https://example.com/
Enter fullscreen mode Exit fullscreen mode

The first three should return something like:

HTTP/2 301
location: https://example.com/
Enter fullscreen mode Exit fullscreen mode

The final request should return:

HTTP/2 200
Enter fullscreen mode Exit fullscreen mode

Also test a real 404, query parameters, nested legacy paths, and any API routes. A redirect system is not finished until its negative cases behave correctly.

For automated checks, a small table-driven test is enough:

const cases = [
  ["http://www.example.com/index.html", "https://example.com/"],
  ["https://www.example.com/index.html", "https://example.com/"],
  ["https://example.com/index.html", "https://example.com/"],
];
Enter fullscreen mode Exit fullscreen mode

Assert that every source returns 301, has the exact expected Location, and reaches a 200 response without another hop.

7. Interpret Search Console correctly

After deployment, old URLs may remain in Search Console under Page with redirect for a while. That category is normally expected: Google discovered the source URL, followed the redirect, and chose not to index the source.

Google explicitly describes permanent server-side redirects as a signal that the target should be canonical in its redirect documentation.

The useful inspection target is the final URL:

  • Does it return 200?
  • Is it indexable?
  • Does its canonical point to itself?
  • Is it present in the sitemap?
  • Do internal links use it directly?
  • Does URL Inspection show the intended canonical?

Running "Validate fix" on correctly redirected source URLs does not make those sources indexable, nor should it.

Common mistakes

  1. Using 302 for a permanent move. Temporary redirects do not communicate the same long-term intent.
  2. Creating redirect chains. Normalize all dimensions in one response.
  3. Leaving old URLs in the sitemap. The sitemap should describe destinations, not historical routes.
  4. Using a canonical tag instead of a redirect. If a duplicate URL should never be visited, redirect it at the server or edge.
  5. Redirecting unrelated 404s to the homepage. Map only equivalent content.
  6. Forgetting the final 200 check. A perfect 301 that lands on an error page is still broken.
  7. Mixing slash conventions. Pick one format and make routes, canonicals, and links agree.

A small real-world implementation

I applied this pattern to the live informational site I maintain. Its old HTTP, WWW, and index.html forms converge on the HTTPS apex homepage, while the final page returns 200. The linked site is mine and contains product links; I include it here only as a disclosed live implementation of the redirect pattern.

The main lesson was not "remove every URL from the Search Console report." It was to make every signal agree on one destination and then evaluate the destination itself.

Once that invariant is enforced at the edge, duplicate URL cleanup becomes predictable instead of mysterious.

Top comments (0)