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.
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.A URL in post content. A raw DB find-replace reaches it fine.
PHP arrays carry a length prefix. Use a serialization-aware tool (WP-CLI search-replace).
Page-builder bodies are encoded. Decode and fix these separately.
Run a serialization-aware tool, then re-check encoded blocks by hand – one pass rarely reaches all three.
Table of Contents
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
Why did my WordPress domain migration leave broken links?
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.