WordPress Domain Migration: Why Search-Replace Misses Your URLs

Date:

You moved the site from staging to the live domain, ran a database search-replace for the old host, and most links updated – but some images 404, a widget points at the old URL, and a page-builder block still loads from staging. The replace didn’t fail. It reached one kind of data and silently skipped two others.

The short answer: When you change a WordPress site’s domain, URLs live in three different forms, and a plain database find-and-replace only reaches the first. Plain-text URLs in post_content update cleanly. Serialized data – PHP arrays used by widgets, theme mods, and plugin settings – carries a character-length prefix in front of every string, so a raw replace changes the text but not the length and corrupts the record; you need a serialization-aware tool (the WP-CLI search-replace command) that unpacks, replaces, and re-serializes. Base64-encoded blocks – page-builder bodies – are invisible to both, so you decode and fix those separately. Miss the last two and the migration looks done while URLs quietly point at the old host.
Three ways a URL hides from a migration search-replaceThe same domain change, three data forms, three levels of opacity
1. Plain text URLs

A URL in post content. A raw DB find-replace reaches it fine.

2. Serialized data

PHP arrays carry a length prefix. Use a serialization-aware tool (WP-CLI search-replace).

raw replace corrupts it
3. Base64 blocks

Page-builder bodies are encoded. Decode and fix these separately.

invisible to both

Run a serialization-aware tool, then re-check encoded blocks by hand – one pass rarely reaches all three.

Layer one: plain-text URLs

The easy case. A URL written directly into a post’s body, a menu item, or a settings row as ordinary text is reached by any find-and-replace, including a raw SQL UPDATE ... REPLACE(). If every URL on the site lived like this, a one-line query would finish the migration. Most don’t – which is why a raw query looks like it worked and still leaves broken links behind.

Layer two: serialized data, where a raw replace corrupts

WordPress stores many structured settings – widget configs, theme mods, transients, and a lot of plugin options – as PHP-serialized strings. Serialization records each string with its byte length in front of it, like s:19:"https://old.example". Change the URL with a blind replace and the text gets shorter or longer while the 19 stays put; the string no longer matches its declared length, and PHP can’t unserialize it. The setting comes back empty or throws – a widget vanishes, a slider loses its slides. This is the single most common way a “simple” migration breaks a site.

The fix is a tool that understands the format: WP-CLI’s wp search-replace walks every row, unserializes each value, replaces inside it, recalculates the length, and re-serializes. Good migration plugins do the same. The rule is blunt: never run a raw SQL find-replace across the whole database – use a serialization-aware tool, every time.

A raw find-replace doesn’t skip serialized data – it damages it. The length prefix that made the format fast is the same prefix your blind replace leaves wrong.

Layer three: base64 blocks, invisible to both

Even a serialization-aware tool has a blind spot. Many page builders store a block as a base64-encoded string, so a URL baked into that block isn’t text the tool can see – it’s an opaque token. wp search-replace walks straight past it, because there is no https://old... in the row to match; the old host is hidden one encoding layer down. This is the launch counterpart of the search-replace-changes-nothing trap: the replace runs, matches nothing in the encoded body, and reports success while the staging URL rides along inside it.

There’s no clean one-pass fix. You decode the affected block bodies, replace the host in the decoded text (matching the exact stored form), and re-encode – or, more often, re-save the block in the builder after the move so it regenerates with the new domain. These bodies frequently live where the standard database tools and REST don’t reach, which is why they’re the last URLs anyone finds.

A migration that actually lands

Sequence the three layers instead of hoping one command covers them. Use a serialization-aware tool (WP-CLI or a trusted migration plugin) for the whole database – never a raw SQL replace – so plain text and serialized data are handled correctly in one safe pass. Then hunt the encoded stragglers: load the key pages after the move and watch the network panel for any request still going to the old host, and check page-builder templates specifically. Fix those by decoding or by re-saving the block. Finally, re-crawl the site for mixed content and old-host links so nothing silently points back at staging.

After a domain move, “the search-replace finished” is not “the migration is done.” The URLs that break are the ones your replace never saw.

The short version

WordPress keeps URLs in three forms, and a plain database search-replace only reaches plain text. Serialized data – widget, theme, and plugin settings – stores a length prefix, so a raw replace corrupts it; use a serialization-aware tool like WP-CLI search-replace that re-serializes correctly, and never a raw SQL find-replace across the database. Base64-encoded page-builder blocks are invisible to both, so decode and fix (or re-save) those separately. After the move, load key pages, watch for requests to the old host, and re-check encoded templates – the migration is done when nothing points back at staging, not when the command finishes.

Frequently Asked Questions

Because a plain search-replace only reaches URLs stored as plain text. URLs inside PHP-serialized settings need a serialization-aware tool or they corrupt, and URLs inside base64-encoded page-builder blocks are invisible to search-replace entirely. Any of the three that you skip leaves links pointing at the old domain.

What is serialized data and why does a raw replace break it?

Serialized data is how PHP stores structured values like arrays – each string is recorded with its byte length in front, e.g. s:19:"...". A blind replace changes the string’s length without updating that number, so the value no longer matches its declared length and PHP can’t unserialize it, and the setting breaks. A serialization-aware tool recalculates the length.

What tool should I use to change URLs during a migration?

A serialization-aware one. WP-CLI’s wp search-replace unserializes, replaces, and re-serializes every value safely across the database; reputable migration plugins do the same. Avoid a raw SQL UPDATE ... REPLACE() across the whole database – it is exactly what corrupts serialized settings.

Why is a URL still pointing at my old domain after search-replace?

Most likely it’s inside a base64-encoded page-builder block, which search-replace can’t read. The old host is hidden one encoding layer down, so the replace matches nothing and leaves it. Decode the block and fix it, or re-save it in the builder so it regenerates with the new domain.

How do I confirm a migration is complete?

Don’t trust the command’s success message. Load the key pages after the move, watch the browser’s network panel for any request still going to the old host, check page-builder templates specifically, and re-crawl for mixed content and old-domain links. The migration is done when nothing resolves back to staging.

Share post:

Subscribe

Popular

More like this
Related

How to Merge Spreadsheets by URL (and Why Our Tool Reported 5 Misses That Weren’t)

Matching two spreadsheets on a URL column is the same job as matching a stock list to a price list by product code. It goes wrong in the same two places: how you clean the code, and how you count what didn't match.

Many-to-One: Why a Mapping Backlog Outgrows Its Target Taxonomy

You're mapping a messy source vocabulary onto a clean target taxonomy. The target has about 5,600 slots, so the job must be roughly 5,600 decisions — right? The backlog is already past 8,900, and still climbing.

The Fix That Didn’t Take: When a Content-Hash Gate Skips Your Code Change

You shipped a fix to how one field is computed, re-ran the pipeline across everything, and it reported a clean run. Then the consistency check still flags those records as wrong. The fix is in the code, the rebuild "ran" — and the records never changed. Your own optimisation skipped them.

Your Dedup Scan Is Over-Counting — Because It Forgot What You Already Fixed

The report said 80 duplicate groups still to resolve. You'd been resolving them for days. You run it again after a big batch and it *still* says 80-ish. The backlog isn't stubborn — the scan is counting work you already finished.