A client moved from their old WordPress site to a new host, and my task was simple on paper: set up a site-wide 301 redirect so every old URL lands somewhere valid. The new site was a single-page site on a JAMstack host, so there was no WordPress on the destination side at all. I opened the Redirection plugin on the old site, enabled regex mode, and wrote one wildcard rule that looked tidy:
Source: /(.*)
Target: https://newsite/$1
The logic is migration boilerplate: capture whatever path comes in, forward it to the new host with the same path. I deployed it, hit the old homepage, and got redirected cleanly to the new homepage. Looked done. I closed the laptop.
A few hours later the client reported that a lot of pages had "gone missing." Not the homepage — the deep URLs. I opened old-site.com/contact and instead of landing on the new contact page, I was thrown onto a 404 served by the destination host. I tried old-site.com/services, another 404. old-site.com/about, 404 again. The homepage was the only survivor.
The symptom
What threw me off at first: the redirect was obviously working. This was not a "the rule does not fire" case. curl -I showed a clean 301 and a correct Location pointing at the new host. The chain behaved exactly as I had written it.
curl -I https://old-site.com/contactI got a 301 to https://newsite/contact, and then the new host answered 404 for /contact. So the redirect succeeded; it was the final destination that failed. Every deep path the wildcard captured was forwarded to a URL that simply did not exist on the destination.
The investigation
I briefly suspected the wildcard was being overridden by another rule. The old site already had a handful of internal redirects set up long before me, and the plugin processes rules in order: earlier rules are evaluated before the wildcard. I went through them one by one in case an old rule was clobbering a specific path. It was not. Those legacy rules targeted other specific paths and did not collide with the ones now 404ing.
I also had a moment of panic about /wp-admin, worried the /(.*) wildcard was catching the admin panel and locking me out. It turned out the Redirection plugin excludes /wp-admin by default, so I could still reach the dashboard. That was not it either.
It was only when I actually looked at the Location header and compared it against the structure of the new site that the penny dropped. https://newsite/contact was a URL that had never existed. The new site was not a replica of the old one with the content moved over. It was a single-page site with a completely different information architecture: no /contact, no /services, no /about. Everything lived on one page.
The root cause
The $1 capture group only makes sense if the destination host has the same path structure as the origin. That is the default assumption of every WordPress migration guide: change the domain, keep the path. But that assumption collapses the moment the destination is a genuinely different site.
Source: /(.*) captures everything after the domain into $1. Target: https://newsite/$1 then reattaches that path onto the new host. For /contact, the result is https://newsite/contact — a URL with no counterpart on that single-page site, so the host answers with its own 404. My redirect worked perfectly; it just faithfully delivered every visitor to an address that did not exist.
Path preservation is correct for a like-for-like migration. Here it was exactly the wrong thing. I was forcing an old street map onto a city whose roads had been completely redrawn.
The fix
The fix is to drop the capture group and collapse every old URL onto the flat new homepage. Because the new site has a different information architecture, path preservation is not what you want:
Source: /(.*) (regex enabled)
Target: https://newsite/
Note the Target no longer ends in /$1. Whatever old path comes in — /contact, /services, /about — now lands on https://newsite/, the single page that actually hosts all the content. No more invented destination URLs, no more 404.
Then I re-verified from the terminal:
curl -I https://old-site.com/contactNow Location points at https://newsite/ and the host answers 200. I tried several other deep paths and all of them landed on the new homepage with a healthy status.
Checklist for cross-host migrations
- Ask first: does the destination host share the same path structure as the origin? If yes,
Target: https://newsite/$1fits. If no, drop the$1. - For a single-page destination or a different IA, collapse everything to the homepage:
Target: https://newsite/with no capture group. - Verify with
curl -I— do not just confirm it redirects. Check thatLocationpoints at a URL that actually exists and that the destination host answers200, not404. - Review pre-existing internal redirect rules; the plugin processes them before the wildcard, and an old rule can clobber a specific path.
- Confirm
/wp-adminis excluded (Redirection does this by default) so the/(.*)wildcard does not lock you out of the dashboard.
The lesson I took home: a redirect that "works" is not necessarily a redirect that is correct. A clean 301 is still wrong if it delivers people to a door that is not there. Since then, whenever I set up a cross-host wildcard, my first question is not "does the redirect fire" but "does the destination address actually exist".
