September 2026 · ~9 min read · Cloudflare Workers, DNS, migration recovery
The whole point in one picture: stop letting every deep link 404, and send each one where it belongs.
Around ten years ago this brand ran its store on a different domain, then moved to a new one and set up no redirects at all. Every link, ranking and bit of trust the old domain had earned stayed behind, and every deep URL on it became a dead end, a 404 that the links pointing at it now resolved to nothing. When I took the project on there was a lot to do at once, so my first move was a deliberate stopgap: I set up a GoDaddy domain forward myself, which at least made the domain resolve and sent the root to the new homepage. But a domain forward maps the whole domain to one place, not path by path, so the individual old URLs still dead-ended. That was fine as a day-one holding measure. The real job, and the subject of this piece, was to give every old page its own 301 to the page that replaced it, ten years late, without renting a server to hold the rules.
What I walked into
The situation was specific. The legacy domain had no website and no hosting, just a registration at GoDaddy and a domain forward. It still carried live email on Microsoft 365, so whatever I did to DNS could not break mail. And the only record of the old site's URLs was in third-party archives, because the old store, a PrestaShop build I could identify from its URL patterns, had been gone for years with no sitemap or database to export. So the task split into four honest problems: find the old URLs, decide where each should go, serve the redirects with no server, and move DNS without knocking out email.
Why a blanket forward is not a migration
It helps to be precise about what was wrong, because "the domain forwards, so we're fine" is a common and costly misread. A GoDaddy domain forward can only send the whole domain to one destination. It has no way to say "send /content/19-wholesale to /wholesale/." The root reached the new homepage, but every deeper URL, the old product categories, the wholesale page, the refund policy, resolved to a 404. A 301 is meant to tell search engines "this specific page moved here"; a domain forward gives the individual pages no treatment at all, so the links that still point at them lead to a dead end and their accumulated signal goes nowhere. The fix is page-to-page 301s, one per old URL, each landing on the live page that carries on its purpose.
Rebuilding the old URL inventory
No sitemap survived, so the URL list was rebuilt from three archives and ranked by the links each URL still holds.
With no export to work from, I treated this like an archaeology dig across three sources. The Wayback Machine showed which pages existed and how the old URLs were structured. A Semrush export gave historic URLs that once had search visibility. Ahrefs showed which old URLs still had backlinks pointing at them, and how many were dofollow. I merged the three lists, normalised them to lowercase with no trailing slash, removed duplicates, and ranked every URL by the link equity it still carried. That ranking is what tells you where to spend care: the single highest-equity legacy URL here was one old category page holding over a hundred dofollow links, so getting that one mapping right mattered more than any other.
Mapping by intent, not by keyword
For each URL I asked one question: what was a person looking for when they landed here, and which live page answers that same need? Category pages went to the matching category, retired product URLs went to their parent category, policy pages went to the current policy page, and the scattered account screens (addresses, order history, identity) all went to the single account area. A store-locator page with no modern equivalent went to contact, the closest intent, rather than dumping it on the homepage. Only genuinely unmappable URLs fall back to the homepage, and even that is better than the 404 they used to hit, while everything mappable now lands exactly where it belongs.
The decision that made it cheap: a Worker, not a host
The Worker answers at the edge before any origin is contacted, so the domain needs no hosting at all.
Normally redirect rules live in an .htaccess file, an Nginx config or a plugin, and all of those need a running website. This domain had nothing behind it but DNS, and buying hosting plus a CMS just to store 35 rules would have added a bill, setup time and something to maintain forever. So I moved the domain onto Cloudflare (Free plan) and wrote a Cloudflare Worker that holds the whole map and returns a 301 at Cloudflare's edge. Attached to a route like legacy-domain.com/*, the Worker runs for every request to that hostname before Cloudflare tries to reach any origin, so the domain does not need an origin at all. The Worker is the entire "website." The logic runs in three steps: query-string rules first (the PrestaShop contact page lives in a parameter, not a path), then an exact path lookup, then a homepage fallback for anything unmapped.
const TARGET = "https://new-domain.com";
// legacy path (lowercase, no trailing slash) -> final destination (single hop, 200)
const MAP = {
"/": "/",
"/5-category-a-on-sale": "/shop/category-a/",
"/6-category-b-for-sale": "/shop/category-b/",
"/content/19-wholesale": "/wholesale/",
"/content/13-money-back": "/refund-policy/",
"/authentication": "/my-account/",
"/order-history": "/my-account/",
// ...the rest of the map
};
const QUERY_RULES = [
{ path: "/index.php", param: "controller", value: "contact", to: "/contact-us/" },
];
export default {
async fetch(request) {
const url = new URL(request.url);
let path = url.pathname.toLowerCase();
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
// 1. query-string rules first
for (const r of QUERY_RULES) {
if (path === r.path &&
(url.searchParams.get(r.param) || "").toLowerCase() === r.value) {
return Response.redirect(TARGET + r.to, 301);
}
}
// 2. exact path map (params like ?redirect=... are ignored on purpose)
if (MAP[path]) return Response.redirect(TARGET + MAP[path], 301);
// 3. anything unmapped -> homepage (a safe fallback, better than the old 404s)
return Response.redirect(TARGET + "/", 301);
},
};
Two small details there do a lot of work. Every destination ends with the trailing slash the new site uses, so the new site never adds a second redirect of its own, and the Worker never copies old query strings onto the new URL, which keeps junk parameters out of the new site's index. Both keep every redirect a clean single hop.
Moving DNS without breaking email
An import is a copy, not a review. Proxy only what the Worker must catch; leave everything email touches on DNS only.
When Cloudflare imported the domain, it set everything to Proxied, including the Microsoft 365 CNAMEs (autodiscover, mail, msoid, lyncdiscover, sip and the device-enrollment records). Those need direct DNS answers, and left proxied they break Outlook account setup and Teams/Skype discovery quietly, the kind of failure nobody notices until a new laptop cannot set up mail. So I moved every one of them to DNS only, and kept only the root and www proxied, because those are the two the Worker route must intercept. The two old GoDaddy forwarding IPs on the root became a single 192.0.2.1 placeholder, still proxied, an address reserved for documentation that never answers, which is exactly the point: it only exists so Cloudflare has a hostname to proxy, and the Worker replies before any origin is contacted. MX, SPF, SRV and the verification TXT records stayed exactly as they were, including the Search Console verification I will need for the change-of-address step.
Proving it, not assuming it
Deployed clean, both hostnames routed, sub-millisecond CPU time, zero errors.
A redirect that looks right in a dashboard is not proof. I wrote a small bash script that checks every legacy URL against three conditions: it returns 301, the Location header matches the expected destination exactly, and that destination returns 200, which proves a single hop with no chain.
T="https://new-domain.com"
while IFS='|' read -r SRC EXP; do
LOC=$(curl -s -o /dev/null -w "%{redirect_url}" "$SRC")
CODE=$(curl -s -o /dev/null -w "%{http_code}" "$SRC")
DEST=$(curl -s -o /dev/null -w "%{http_code}" "$LOC")
if [ "$CODE" = "301" ] && [ "$LOC" = "$T$EXP" ] && [ "$DEST" = "200" ]; then
R="PASS"; else R="FAIL"; fi
echo "$R | $CODE -> $DEST | $SRC"
done <<'EOF'
https://legacy-domain.com/5-category-a-on-sale|/shop/category-a/
https://legacy-domain.com/index.php?controller=contact|/contact-us/
https://legacy-domain.com/authentication?redirect=my-account|/my-account/
EOF
The live list runs the full set of URLs plus deliberate edge cases: a login URL with a changing parameter, an http:// request and a www request. For a quick check without a terminal, the same test works in any HTTP status checker, one 301 hop then a 200.
What it changed
The legacy domain now behaves the way it should have from the day of the migration. Every mapped old URL returns a single-hop 301 to its matching live page instead of a 404 dead end, the Worker deployed with zero errors and serves each redirect in well under a millisecond of CPU time, both hostnames are covered, and email and Microsoft 365 stayed intact because the proxied records were caught during the move. The whole redirect layer is one Worker file and a DNS zone, both on Cloudflare's free tier, no hosting, no CMS, no server to patch.
What this does not undo
An honest limit, because a redirect set up ten years late does not restore everything the domain once had. Some signal fades with time, and search engines take a while to recrawl old links and process the new destinations, so this is not a switch that lights up rankings overnight. What it does is give the remaining signal a clear, correct path: every backlink that still points at an old page now tells search engines exactly which new page carries its purpose, instead of dead-ending on a 404. That is far stronger than a blanket forward and far better than leaving a decade of links stranded. Better late than never is the honest framing, and it is still worth doing.
What I'd carry forward
- Domain forwarding is not a migration. If a domain ever had pages that earned links, those pages need their own 301s, not a blanket redirect to the homepage.
- You do not need hosting to run redirects. An edge function like a Cloudflare Worker can be the entire site for a redirect-only domain.
- Old URLs never truly disappear. The Wayback Machine, SEO-tool exports and backlink indexes hold enough history to rebuild an inventory years later.
- Review every imported DNS record. Proxying email and Microsoft 365 records breaks them quietly; proxy only what the edge must handle.
- Prove it with a script. Status code, exact destination and a 200 on arrival, for every URL. A dashboard is not a test.