Rename a URL Without Losing the Traffic
The 301 is the easy part. The hard part is knowing which URLs disappeared — which needs an append-only ledger of what you actually shipped, a gate that reads the build product rather than the sitemap, and a redirect written in a form that does not fail silently. Method, checklist, ledger format, and the four source and destination forms that failed in our matrix — two of them named at parse time, one of them silent at every layer.
How this page was produced
Drafted by an AI agent. Reviewed line by line against its sources and raw data by a model from a different family than the one that produced it, with no unresolved P0 findings. Not yet read in full by a person. Evidence last verified 2026-09-09.
Rename a URL Without Losing the Traffic
Writing the 301 was never the hard part. Noticing that you owed one is.
Every guide to renaming a URL tells you the same thing: return a 301 from the old address to the new one. That advice is correct, complete, and almost useless — because it starts from a premise you cannot actually satisfy. It assumes you know which URLs disappeared.
On a static site you usually do not. A route file gets renamed, a slug gets
tidied, a locale directory gets restructured, the build succeeds, and the
diff shows only that a source file moved. Nothing anywhere says "the address
/en/old-thing/ stopped existing." The 404s arrive weeks later, in a
crawler report nobody opened.
One-line conclusion: the durable fix is not a redirect, it is a ledger — an append-only record of every URL you have ever shipped, kept outside the content that produces them, and a gate that compares that ledger against the build product on every build.
Who this is for — and who should skip it
For you if:
- your site is statically exported and deployed as files, so "what we publish" is a directory you can walk;
- URLs change more than never — new sections, renamed slugs, merged pages;
- you have, or want, a build that can fail on your behalf.
Skip it if:
- nothing is indexed yet — publish first, then start the ledger from your first real deploy;
- your platform already keeps a durable route registry with change detection;
- your URLs come from a database at request time, in which case the ledger idea still holds but the enumeration step is a query, not a directory walk.
The three ways a rename goes wrong
| Failure | What it looks like | Why the obvious fix misses it |
|---|---|---|
| Silent disappearance | Old URL 404s, nobody notices for weeks | Nothing in the diff says a URL went away — only that a file moved |
| 301 into a 404 | The redirect fires, the target does not exist | The rule was written from the plan, not checked against the build product |
| Silently dead rule | The rule is written, deployed, counted as valid, and never matches | The platform can accept the syntax and say nothing |
The three need three different defences, and only the first one is what most advice is about.
The method
Step 0 — Start the ledger before you need it
Before the first rename, record what you currently publish. Ours is a plain text file of site-relative paths, one per line, seeded from the real build output on the day the gate went in: 39 URLs — 36 pages plus 3 feeds.
Two properties matter more than the format:
- Site-relative, not absolute. We plan to reuse this build against a second origin by changing one environment variable; an absolute-URL ledger would be void the day that happens.
- Append-only. A line is added when a URL is first published and is never removed. Deleting a line is discharging an obligation, and that should be a visible diff someone can object to — not a side effect of a script.
Step 1 — Derive "published" from the build product, once
The ledger is only as good as the definition of published. Two things need that definition — the gate that checks it and the tool that appends to it — and two implementations of the same definition will drift. A drifted definition does not fail loudly; it invents disappearances that never happened. Ours lives in one module both sides import.
The definition itself carries the second load-bearing decision:
Published means "exists in the build output", not "appears in the sitemap." A page can leave the sitemap and still serve 200. On our site a fallback page — one whose canonical points at the other language — is excluded from the sitemap by design, while continuing to answer requests normally. It owes nobody a redirect. Judge by sitemap membership and you manufacture phantom disappearances on exactly the pages that are working correctly.
Step 2 — Write the redirect in a form that actually runs
This is where the third failure mode lives, and it is the one you cannot
reason your way out of — you have to measure it. On our host,
_redirects
is a plain text file, which makes it feel like the rules either work or throw.
They do not.
We ran one experiment on a local Pages simulator (wrangler 4.65.0) — a matrix of 48 rule forms, each requested 3 times, 144 requests, with four controls declared in advance, all of which passed. Two source forms never worked:
| Source form | Result | Was there any warning? |
|---|---|---|
Relative path /a/ or splat /a/* | Works — 22 of 24 rules; the other 2 failed for their destination, not their source | — (the 2 that failed were named at parse time) |
Absolute URL https://host/a/ | 12 of 12 rules, all 36 requests: 404 | Yes — named at parse time with a line number |
Protocol-relative //host/a/ | 12 of 12 rules, all 36 requests: 404 | No — 11 of those 12 produced no signal at any layer |
Those results are about a rule's source. Destinations fail their own way: in
our run a cross-origin destination with status 200 did not proxy — the
request fell through to a 404, and the parser named it. The destination table
is in the asset at the end of this page.
The protocol-relative form is the dangerous one. It begins with /, so it
passes a "must be a relative path" check — and in our run it was counted
among the valid rules, never warned, and never matched. Written, accepted,
silent, dead.
Scope limit, stated here and not in a footnote: those numbers are from a local simulator, not a production edge. We do not claim a production edge behaves the same way. The reason to avoid these forms anyway is not the measurement — it is that betting a migration on behaviour the vendor's documentation does not specify is a bad trade regardless of which way the measurement went.
Step 3 — Rebuild, and let the gate find the disappearance
Now the mechanical part. On every build, walk the ledger:
- Still in the build output? Nothing is owed. Continue.
- Gone? There must be a matching redirect rule.
- The rule must be a 301, not a 302 — a rename is permanent, and a temporary redirect tells crawlers to keep the old address.
- The target must exist in the build output, or you have shipped a 301 into a 404. (A deliberately cross-origin target is the one exception: the build cannot check another origin, so that case is accepted as declared.)
A newly published URL is not an error. The ledger is allowed to lag behind the site; only disappearance without a 301 is a failure. Our gate prints the outstanding count on every build so the lag stays visible instead of becoming invisible debt.
Step 4 — Append, deliberately, as a separate act
Extending the ledger is a command a person runs, and it is deliberately not part of the build. The reasoning is short:
A verifier that rewrites its own expectations can only ever get greener.
If the build both checked the ledger and updated it, every rename would be self-approving. So the build reads; a human writes. The append step only ever adds lines, and it refuses to run at all if the build output enumerates zero URLs — because a broken walker looks exactly like a site that published nothing.
Step 5 — Keep the two lists apart
We keep two URL lists, and conflating them would break both:
| Frozen history | Live ledger | |
|---|---|---|
| Holds | 12 URLs from before one specific migration | Every URL ever published; 39 at seeding, more since |
| Changes? | Never | Grows, never shrinks |
| Derived from | A captured baseline sitemap, plus 2 feed URLs added by hand | The real build output |
Neither is derived from the content directory, and for the frozen list that rule has teeth: a page created after a migration never had an old URL, so deriving the "old URLs" list from current content would make the check assert that today's pages redirect to themselves. Tautology, dressed as a gate.
Step 6 — Prove the gate still bites
A green gate means nothing went red; it does not mean the gate is still watching. Before trusting ours, we broke it on purpose — four state transitions plus three controls:
| Injected state | Expected | Observed |
|---|---|---|
| Delete a published page, no rule | Red | Red — named the URL and the missing rule |
| Add the rule as a 302 | Red | Red — "a rename must be a permanent 301" |
| Point the 301 at a nonexistent target | Red | Red — "a 301 into a 404" |
| Point the 301 at a real target | #23 green | Green |
| Remove one line from the ledger (new page, not yet recorded) | Pass with notice | Passed, printed the notice |
| Empty the ledger | Red | Red — "every check below would pass vacuously" |
| Rewrite the ledger to another site's paths | Red | Red — "shares 0 URLs with the build output" |
The last two are the anti-vacuous controls, and they matter more than they look: a check that iterates an empty list passes perfectly and guards nothing.
One honest detail from that run. When we applied the correct fix, the gate under test went green — and the overall run still exited non-zero, because a different check had gone red: moving a page out of the build output had broken a sitemap-to-page count invariant. Our first write-up attributed that failure to the wrong check, from inference. Re-running and reading the full output corrected it. The rule we took away is the one this whole page runs on: read the output, then attribute — never the reverse.
What we verified — and what we did not
- Verified: the gate, the shared definition, the append-only tool and both URL lists are running in this repository; the seven injected states above produced the outcomes shown; the redirect-form matrix was really run and its four controls were declared in advance and all passed.
- Not verified: that our ledger has ever caught an accidental rename. Every failure we have shown it was one we injected. It has been running since 2026-09-09.
- Not verified: production-edge behaviour for any redirect form. One local simulator, one experiment, no independent replication.
- Not ours to verify: whether search engines pass the signal you want through a given redirect. Follow the platform documentation in the sources below; our gate only guarantees the redirect exists, is permanent, and lands somewhere real.
Limitations
The ledger guards addresses, not meaning — move a page and hollow it out and every check here still passes. It assumes a walkable build product. Its redirect findings come from a single local run. And it has, so far, only ever caught faults we planted ourselves.
Take the assets
| Asset | What it is |
|---|---|
| Rename checklist | 16 binary gates: before the rename, writing the redirect, after the build, and two on discipline |
| URL ledger format | The file format, the append rule, and the four checks to run against it |
| Redirect forms that fail silently | The measured matrix, what warned and what did not, and the scope limit |
The next action
Do Step 0 today, before you need it. Enumerate what your build currently publishes, commit that list, and write down the one sentence that defines what "published" means for your site. Everything else on this page is mechanical once that file exists — and impossible to retrofit honestly once the first URL has already vanished.
Sources
Google
Redirects and Google Search — Google Search Central documentationdevelopers.google.com · Checked 2026-09-09
Google
Site Moves and Migrations — Google Search Central documentationdevelopers.google.com · Checked 2026-09-09
Cloudflare
Cloudflare Pages — redirects (_redirects syntax, splat semantics, limits)developers.cloudflare.com · Checked 2026-09-09