The migration looked clean. Files copied, database imported, DNS pointed at SkyServer, site loads. Then you start clicking around and something's off: the logo is a broken image icon, a few internal links redirect back to the old host, and a widget you know you configured is just blank. None of that is a coincidence, and none of it means the migration failed — it means WordPress stored some of your URLs in a format a plain find-and-replace can't touch safely.
Symptom: The Site "Mostly" Works, But Pieces Are Wrong
This almost never looks like a total failure. Posts and pages usually render fine, because their content is stored as plain text in wp_posts. What breaks is scattered and inconsistent:
- The site logo, favicon, or a theme background image shows a broken icon
- Some links go to
olddomain.cominstead of the new one, even though you changedsiteurlandhome - A text widget, menu item, or Customizer setting (theme mods) just disappeared
- Page builder layouts (Elementor, Divi, WPBakery) look stripped down or half-rendered
- Plugin settings reset to defaults even though you didn't touch them
If you or whoever handled the migration ran an SQL query like UPDATE wp_options SET option_value = REPLACE(option_value, 'olddomain.com', 'newdomain.com') in phpMyAdmin, that's almost certainly the cause.
Cause: WordPress Serializes Data, and Replace Breaks the Length Count
A lot of WordPress data isn't stored as plain text — it's stored as serialized PHP. Theme settings, widget content, some plugin options, and more all get saved as a single string that looks like this:
a:2:{s:6:"header";s:29:"https://olddomain.com/logo.png";s:4:"text";s:12:"Welcome here";}
Look closely at s:29: right before the URL. That number is the exact character length of the string that follows. PHP uses it to know where that value ends and the next one begins. A plain text find-and-replace swaps olddomain.com for newdomain.com just fine — but if the new domain is a different length, that 29 is now wrong. PHP tries to unserialize the array, hits a length that doesn't match the actual string, and gives up. Depending on the WordPress version and where the data lives, that field silently returns empty, throws a warning, or drops the whole array.
This is why the damage is so uneven. Plain wp_posts content survives because it's just text. Anything serialized — widgets, theme mods, some page builder data, several popular plugins' settings — gets corrupted the moment the string length changes.
Fix: Use a Serialization-Aware Search & Replace
The tool has to understand PHP serialization, recalculate those length prefixes, and rebuild the array correctly. Don't run another raw REPLACE() query — it'll make the corruption worse, not fix it. Here are three ways to do it properly, in order of preference.
Option 1: WP-CLI (fastest, if you have SSH access)
If your SkyServer plan includes SSH, this is the cleanest route. First run it as a dry run so you can see exactly what would change before committing:
wp search-replace 'https://olddomain.com' 'https://newdomain.com' --all-tables --precise --dry-run
Review the output — it lists every table and the number of replacements it would make. If that looks right, drop --dry-run and run it for real:
wp search-replace 'https://olddomain.com' 'https://newdomain.com' --all-tables --precise
--all-tables catches custom tables some plugins create outside the standard WordPress set (WooCommerce, form plugins, page builders often do this). --precise avoids some edge-case matching bugs with multibyte strings.
Option 2: A Migration Plugin (no SSH needed)
If you're on shared hosting without terminal access, install Better Search Replace (or a similar tool) from the WordPress plugin repo through your dashboard. Go to Tools > Better Search Replace, enter the old and new domain, select all tables, and tick Run as dry run first. Check the preview, then run it for real. It handles the serialization math internally, so widgets and theme settings come back intact.
Option 3: A Standalone Script via phpMyAdmin
If the site won't load at all (so plugins aren't an option) and you don't have SSH, you can temporarily upload the Interconnect/IT Search-Replace-DB script to a folder in public_html, run it through the browser, and point it at the same database credentials from wp-config.php. This works entirely outside WordPress, which is useful when the site is broken enough that the admin dashboard won't even load.
Delete the script file immediately after you're done. It has full read/write access to your database with no login of its own — leaving it sitting in public_html is a real security hole, and bots do scan for it by name.
Comparing the Three Methods
| Method | Needs SSH | Works if wp-admin is broken | Best for |
|---|---|---|---|
| WP-CLI | Yes | Yes | VPS or SSH-enabled cPanel accounts |
| Better Search Replace plugin | No | No — needs dashboard access | Shared hosting, site otherwise loads fine |
| Search-Replace-DB script | No | Yes | Site is down and you need a quick fix, then delete it |
Prevention: Get It Right on the Next Migration
- Never run a raw SQL
REPLACE()onwp_options,wp_postmeta, or any table you know holds serialized data. - Always do the URL swap with WP-CLI or a proper search-replace plugin, and always dry-run it first.
- If you're cloning through Softaculous's built-in clone/staging tool, it already handles this correctly — you don't need to run search-replace again afterward.
- After the swap, clear any object cache and page cache (Redis, LiteSpeed Cache, or a caching plugin) — stale cached HTML can still show old URLs even after the database is fixed.
- Check
wp-config.phptoo. If it hardcodesWP_HOMEorWP_SITEURLconstants, a database search-replace won't touch those — you'll need to edit the file directly.
If you migrated to SkyServer and you're seeing this, open a support ticket with your domain and we can run a serialization-safe search-replace on the account directly — it's usually a five-minute fix once we're in.
Frequently Asked Questions
Will WP-CLI's search-replace break anything else on my site?
Not if you run --dry-run first and review the table of changes. It only touches exact string matches, so unrelated content is untouched. The main risk is running it without reviewing the dry run and catching an unintended match, like a domain name that also appears as plain text somewhere unrelated.
Why did Softaculous's clone feature not have this problem?
Softaculous's built-in clone tool already runs a serialization-aware search-replace as part of the cloning process. The corruption described here happens when someone bypasses that and edits the database directly with a generic SQL tool instead.
My widgets are gone completely, not just showing wrong URLs. Is this the same issue?
Yes, usually. Widget data is stored as one large serialized array in wp_options under widget_text, widget_custom_html, and similar keys. If the length prefix for any single widget's content gets thrown off, PHP can fail to parse the entire array, which makes every widget in that sidebar disappear rather than just the one with the broken URL.
Do I need to fix this in the database, or can I just edit pages in the WordPress editor?
Fix it in the database. Editing pages one by one only fixes what you can see and doesn't touch theme mods, widget data, or plugin settings sitting in wp_options. A proper search-replace run fixes the whole site in one pass instead of chasing broken links page by page.
Is it safe to run search-replace on a live production site?
Take a database backup first — export it from phpMyAdmin or run wp db export before you start. The operation itself is quick, but if the dry run's replacement count looks way higher or lower than expected, you want a backup to fall back to rather than trying to reverse a bad run by hand.
