Moving a WordPress site (e.g., from dev.site.com to site.com) seems simple: just run a “Find & Replace” on the database, right?
WRONG.
If you try to run a raw SQL query like:
UPDATE wp_options SET option_value = replace(option_value, 'old.com', 'new.com')
…you will break your site. Specifically, you will lose Widgets, Theme Options, and some Plugin configurations.
The common mistake: Simple find & replace
Many developers (and even some hosting providers) suggest using a simple SQL REPLACE() function to update URLs. This approach seems logical but is fundamentally flawed.
The Temptation:
-- This looks safe, but it's NOT
UPDATE wp_options
SET option_value = REPLACE(option_value, 'https://old.com', 'https://new.com');
What Happens:
- Some URLs get updated correctly
- Serialized data gets corrupted
- Widgets disappear
- Theme options break
- Plugin settings lost
- Site becomes partially broken
Why It Fails: WordPress doesn’t store data as plain text. Much of it is stored as serialized PHP data, which requires special handling.
Understanding serialized data: The core problem
What is serialization?
WordPress stores complex data (arrays, objects) in the database as Serialized Strings. Serialization converts PHP data structures into strings that can be stored in the database.
Example Serialized Data:
// Original PHP array
array(
'home' => 'https://old.com',
'siteurl' => 'https://old.com',
'admin_email' => 'admin@old.com'
)
// Serialized string (stored in database)
a:3:{s:4:"home";s:17:"https://old.com";s:7:"siteurl";s:17:"https://old.com";s:11:"admin_email";s:15:"admin@old.com";}
Breaking down the serialized string
Let’s decode a:3:{s:4:"home";s:17:"https://old.com";...}:
a:3= array with 3 elementss:4:"home"= string, 4 characters, value “home”s:17:"https://old.com"= string, 17 characters, value “https://old.com”
The Critical Part:
The number 17 represents the exact character count of the string "https://old.com".
What happens with simple replace?
Original:
s:17:"https://old.com"
After Simple Replace (old.com → new-domain.com):
The Problem:
- String length changed from 17 to 23 characters
- Serialization still says
s:17(expects 17 characters) - PHP tries to read 17 characters:
"https://new-domain" - Missing the
.compart - Entire array becomes invalid
- Data is corrupted
Result:
- Widgets disappear (widget data corrupted)
- Theme options reset (options corrupted)
- Plugin settings lost (settings corrupted)
- Site partially broken
Where serialized data appears
Common locations
1. wp_options Table:
siteurlandhomeoptions- Widget data (
sidebars_widgets) - Theme mods (
theme_mods_*) - Plugin options (most plugins)
2. wp_postmeta Table:
- Custom fields
- ACF field data
- Post metadata
- Attachment metadata
3. wp_usermeta Table:
- User preferences
- Capabilities
- User metadata
4. wp_post Table:
- Post content (sometimes)
- Post excerpts
Real-World example
Widget Data (Before):
array(
'sidebar-1' => array(
'widget_pages' => array(
2 => array('title' => 'Pages', 'sortby' => 'menu_order')
)
)
)
Serialized:
a:1:{s:10:"sidebar-1";a:1:{s:12:"widget_pages";a:1:{i:2;a:2:{s:5:"title";s:5:"Pages";s:6:"sortby";s:10:"menu_order";}}}}
After Simple Replace (if URL in widget):
- Serialization breaks
- Widget disappears
- Sidebar becomes empty
The solution: Serialization-Aware tools
You need a tool that:
- Unserializes the data (converts string back to PHP array)
- Replaces the text within the array
- Recalculates character counts
- Reserializes the data (converts back to string)
- Updates the database
Method 1: Wp-CLI (recommended)
The Professional Way: If you have SSH access, WP-CLI is the best tool for the job.
Basic Usage:
wp search-replace 'https://old.com' 'https://new.com' --all-tables
Advanced Options:
## DRY run (see what would change)
wp search-replace 'https://old.com' 'https://new.com' --all-tables --dry-run
## Specific tables only
wp search-replace 'https://old.com' 'https://new.com' wp_posts wp_postmeta
## Skip specific columns
wp search-replace 'https://old.com' 'https://new.com' --all-tables --skip-columns=guid
## Case-insensitive
wp search-replace 'https://old.com' 'https://new.com' --all-tables --regex
## Export changes to sql file first
wp search-replace 'https://old.com' 'https://new.com' --all-tables --export=changes.sql
Why WP-CLI is Best:
- ✅ Handles serialization correctly
- ✅ Fast and efficient
- ✅ Can preview changes (dry-run)
- ✅ Command-line control
- ✅ Scriptable and automatable
- ✅ No plugin overhead
Installation:
## Download wp-CLI
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
## Make executable
chmod +x wp-cli.phar
## Move to path
sudo mv wp-cli.phar /usr/local/bin/wp
## Verify installation
wp --info
Method 2: Better search replace plugin
For Non-Command-Line Users: If you don’t have SSH access, use the Better Search Replace plugin by WP Engine.
Installation:
- Go to Plugins > Add New
- Search for “Better Search Replace”
- Install and activate
Usage:
- Go to Tools > Better Search Replace
- Enter old URL:
https://old.com - Enter new URL:
https://new.com - Select tables (or check “Select All”)
- Important: Check “Run as dry run?” first
- Click “Run Search/Replace”
- Review the results
- Uncheck “Run as dry run?”
- Click “Run Search/Replace” again to execute
Safety Features:
- Dry run mode (preview changes)
- Shows affected rows
- Can select specific tables
- Creates backup (recommended)
Limitations:
- Requires plugin installation
- Web interface (slower than CLI)
- May timeout on large databases
Method 3: Interconnect/IT search-Replace-DB script
External PHP Script: A standalone PHP script that you upload via FTP.
Download: Available from Interconnect/IT
Usage:
- Download the script
- Upload to your site root (via FTP)
- Access via browser:
yoursite.com/search-replace-db.php - Enter database credentials
- Run search and replace
- CRITICAL: Delete the script immediately after use
Security Warning:
- ⚠️ This script gives full database access
- ⚠️ Must be deleted after use
- ⚠️ Can be accessed by anyone with the URL
- ⚠️ Use only on staging/development sites
- ⚠️ Never leave on production
When to Use:
- No SSH access
- No plugin installation possible
- One-time migration
- Staging environment only
Method 4: Manual PHP script
For Advanced Users: Create a custom script using WordPress functions.
<?php
// Custom search-replace script
require_once('wp-load.php');
function safe_search_replace($old_url, $new_url) {
global $wpdb;
$tables = $wpdb->get_col("SHOW TABLES");
foreach ($tables as $table) {
$rows = $wpdb->get_results("SELECT * FROM $table");
foreach ($rows as $row) {
foreach ($row as $field => $value) {
if (is_serialized($value)) {
$unserialized = unserialize($value);
$replaced = recursive_replace($unserialized, $old_url, $new_url);
$serialized = serialize($replaced);
$wpdb->update(
$table,
array($field => $serialized),
array('ID' => $row->ID)
);
} else {
$replaced = str_replace($old_url, $new_url, $value);
$wpdb->update(
$table,
array($field => $replaced),
array('ID' => $row->ID)
);
}
}
}
}
}
function recursive_replace($data, $old, $new) {
if (is_array($data)) {
return array_map(function($item) use ($old, $new) {
return recursive_replace($item, $old, $new);
}, $data);
} elseif (is_string($data)) {
return str_replace($old, $new, $data);
}
return $data;
}
// Usage
safe_search_replace('https://old.com', 'https://new.com');
Note: This is a simplified example. Use WP-CLI or Better Search Replace for production.
Complete migration checklist
Before migration
1. Backup Everything:
## Database backup
wp db export backup.sql
## Files backup
tar -czf site-backup.tar.gz /path/to/wordpress
2. Test on Staging:
- Never test on production
- Use staging environment
- Verify all functionality
- Test all plugins
3. Document Current URLs:
- List all old URLs
- Note any hardcoded URLs
- Check .htaccess rules
- Review configuration files
During migration
1. Update Database URLs:
## Using wp-CLI
wp search-replace 'https://old.com' 'https://new.com' --all-tables --dry-run
wp search-replace 'https://old.com' 'https://new.com' --all-tables
2. Update wp-config.php:
define('WP_HOME','https://new.com');
define('WP_SITEURL','https://new.com');
3. Update .htaccess (if needed):
## Redirect old domain
RewriteEngine On
RewriteCond %{HTTP_HOST} ^old.com [NC]
RewriteRule ^(.*)$ https://new.com/$1 [R=301,L]
4. Update DNS:
- Point domain to new server
- Wait for propagation
- Verify DNS changes
After migration
1. Verify URLs:
- Check homepage
- Test internal links
- Verify images load
- Check admin area
2. Update Search Console:
- Add new property
- Submit sitemap
- Request indexing
- Set up redirects
3. Monitor for Issues:
- Check error logs
- Monitor 404 errors
- Test all functionality
- Verify plugins work
4. Set Up Redirects:
## .Htaccess redirects
Redirect 301 /old-page/ https://new.com/new-page/
Common migration scenarios
Scenario 1: Staging to production
Old: staging.site.com
New: site.com
Steps:
- Backup staging database
- Export files
- Import to production
- Run search-replace
- Update DNS
- Test thoroughly
Scenario 2: HTTP to HTTPS
Old: http://site.com
New: https://site.com
Steps:
- Install SSL certificate
- Update database URLs
- Update .htaccess redirects
- Test all pages
- Update Search Console
Scenario 3: Domain change
Old: old-domain.com
New: new-domain.com
Steps:
- Backup everything
- Update database URLs
- Set up domain redirects
- Update DNS
- Submit to Search Console
Scenario 4: Subdomain to root
Old: www.site.com
New: site.com
Steps:
- Update database URLs
- Set up redirects
- Update DNS
- Test all functionality
Troubleshooting common issues
Issue 1: Widgets disappeared
Cause: Serialized data corrupted
Solution:
## Restore from backup
wp db import backup.sql
## Or manually fix widgets
wp option get sidebars_widgets
## Fix serialized data manually or restore
Issue 2: Theme options reset
Cause: Theme mods corrupted
Solution:
## Check theme mods
wp option get theme_mods_twentytwentyfour
## Restore from backup if corrupted
Issue 3: Images not loading
Cause: Image URLs not updated
Solution:
## Update attachment urls
wp search-replace 'https://old.com/wp-content' 'https://new.com/wp-content' --all-tables
## Or use attachment url updater plugin
Issue 4: Mixed urls (some updated, some not)
Cause: Incomplete search-replace
Solution:
## Check for remaining old urls
wp db query "SELECT * FROM wp_options WHERE option_value LIKE '%old.com%'"
## Run search-replace again with different patterns
wp search-replace 'http://old.com' 'https://new.com' --all-tables
wp search-replace 'old.com' 'new.com' --all-tables
Best practices
1. Always backup first
## Full backup
wp db export backup-$(date +%Y%m%d).sql
2. Use DRY run first
## Always test first
wp search-replace 'old' 'new' --all-tables --dry-run
3. Update multiple patterns
## HTTP and HTTPS
wp search-replace 'http://old.com' 'https://new.com' --all-tables
wp search-replace 'https://old.com' 'https://new.com' --all-tables
## With and without www
wp search-replace 'www.old.com' 'new.com' --all-tables
4. Verify after migration
## Check for remaining old urls
wp db query "SELECT * FROM wp_options WHERE option_value LIKE '%old.com%'"
5. Update configuration files
- wp-config.php
- .htaccess
- wp-config.php (WP_HOME, WP_SITEURL)
Security considerations
Protecting search-Replace scripts
If Using Search-Replace-DB Script:
- Use strong authentication
- Restrict IP access
- Delete immediately after use
- Never leave on production
If Using Plugins:
- Use trusted plugins only
- Deactivate after migration
- Remove if not needed
- Keep updated
Performance considerations
Large database handling
For Large Sites:
## Process IN batches
wp search-replace 'old.com' 'new.com' wp_posts --limit=1000
wp search-replace 'old.com' 'new.com' wp_postmeta --limit=1000
For Very Large Sites:
- Use WP-CLI (faster than plugins)
- Process during low-traffic hours
- Consider maintenance mode
- Monitor server resources
Summary: The safe way to update urls
Never:
- ❌ Use simple SQL REPLACE()
- ❌ Edit database with text editor
- ❌ Skip backups
- ❌ Test on production
Always:
- ✅ Use serialization-aware tools
- ✅ Backup before changes
- ✅ Test on staging first
- ✅ Use dry-run mode
- ✅ Verify after migration
Recommended Tools (in order):
- WP-CLI - Best for developers
- Better Search Replace - Best for non-developers
- Search-Replace-DB Script - Last resort, staging only
Key Takeaway:
WordPress uses serialized data extensively. Simple find-and-replace breaks this data. Always use tools that understand serialization. WP-CLI’s search-replace command is the gold standard – it’s fast, safe, and handles all edge cases correctly.
In 2026, with more complex WordPress sites and more serialized data, using the right tool isn’t optional – it’s essential for a successful migration.
SSL/HTTPS Migration Considerations
When migrating from HTTP to HTTPS, there are additional considerations beyond simple URL replacement:
Mixed Content Issues
After updating URLs, you may encounter mixed content warnings where some resources still load over HTTP:
# Update all HTTP references to HTTPS
wp search-replace 'http://' 'https://' --all-tables
# Also update protocol-relative URLs
wp search-replace '//old-domain.com' '//new-domain.com' --all-tables
Hardcoded URLs in Theme Files
Some themes and plugins hardcode URLs in CSS or JavaScript files. Search for these patterns:
# Find hardcoded URLs in your theme
grep -r "http://your-domain.com" wp-content/themes/your-theme/
# Find protocol-relative URLs
grep -r "//your-domain.com" wp-content/themes/your-theme/
CDN and External Resources
If using a CDN, update your CDN configuration:
// In wp-config.php or mu-plugin
define('CDN_URL', 'https://cdn.new-domain.com');
SSL Certificate Verification
After migration, verify your SSL certificate is properly configured:
# Check SSL certificate
curl -I -v https://new-domain.com 2>&1 | grep -i ssl
# Test HTTPS response
curl -s -o /dev/null -w "%{http_code}" https://new-domain.com
Post-Migration Testing Checklist
Critical Functionality Tests
-
Frontend Verification:
- Homepage loads correctly
- All pages accessible
- Navigation menus work
- Images display properly
- Forms submit successfully
-
Admin Panel Tests:
- Login works
- Dashboard loads
- Media library accessible
- Plugins can be activated/deactivated
- Theme customization works
-
E-commerce Sites (WooCommerce):
- Product pages load
- Add to cart works
- Checkout process complete
- Payment gateways functional
- Order emails sent correctly
Automated Testing with WP-CLI
# Verify no old URLs remain
wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%old-domain.com%'"
# Check options table
wp db query "SELECT option_name FROM wp_options WHERE option_value LIKE '%old-domain.com%'"
# Test postmeta
wp db query "SELECT COUNT(*) FROM wp_postmeta WHERE meta_value LIKE '%old-domain.com%'"
Browser Testing
Test across multiple browsers and devices:
# Use curl to test response codes
curl -s -o /dev/null -w "%{http_code}" https://new-domain.com/page-1
curl -s -o /dev/null -w "%{http_code}" https://new-domain.com/page-2
# Check for 404s
wp cron event run wp_site_health_scheduled_check
Advanced Migration Scenarios
Multisite Migration
For WordPress Multisite networks, additional steps are required:
# Update network tables
wp search-replace 'old-domain.com' 'new-domain.com' --all-tables --network
# Update individual site tables
wp search-replace 'subsite1.old-domain.com' 'subsite1.new-domain.com' --all-tables
wp search-replace 'subsite2.old-domain.com' 'subsite2.new-domain.com' --all-tables
# Update network options
wp network meta update 1 siteurl 'https://new-domain.com'
wp network meta update 1 home 'https://new-domain.com'
Database Table Prefix Changes
If changing table prefixes during migration:
# Export with new prefix
wp db export --tables=$(wp db tables --all-tables-with-prefix | sed 's/wp_/new_/')
# Note: This requires manual SQL editing for prefix changes
Serialized Data Deep Dive
For complex serialized data issues, here’s how to manually inspect:
<?php
// Inspect serialized data
require_once('wp-load.php');
$option_value = get_option('theme_mods_yourtheme');
echo "Is serialized: " . (is_serialized($option_value) ? "Yes" : "No") . "\n";
if (is_serialized($option_value)) {
$unserialized = unserialize($option_value);
print_r($unserialized);
}
Troubleshooting Advanced Issues
Issue: URLs in Custom Tables Not Updated
Some plugins create custom tables that may be missed:
# List all tables
wp db tables --all-tables
# Search for specific patterns in all tables
wp search-replace 'old-domain.com' 'new-domain.com' $(wp db tables --all-tables | grep -v "wp_")
Issue: JSON-Encoded Data
Modern WordPress stores some data as JSON instead of PHP serialization:
# JSON data is handled automatically by WP-CLI
# But verify with:
wp db query "SELECT meta_value FROM wp_postmeta WHERE meta_value LIKE '%old-domain.com%' AND meta_value LIKE '{%'"
Issue: Base64-Encoded URLs
Some page builders encode URLs in Base64:
<?php
// Decode and replace Base64 encoded URLs
function replace_base64_urls($content, $old_url, $new_url) {
if (base64_encode(base64_decode($content, true)) === $content) {
$decoded = base64_decode($content);
$decoded = str_replace($old_url, $new_url, $decoded);
return base64_encode($decoded);
}
return $content;
}
Issue: URLs in Transients
Transients may cache old URLs:
# Clear all transients
wp transient delete --all
# Or specifically search transients
wp search-replace 'old-domain.com' 'new-domain.com' wp_options --precise
Performance Optimization During Migration
Batch Processing for Large Databases
# Process posts table in batches
wp search-replace 'old.com' 'new.com' wp_posts --limit=1000 --offset=0
wp search-replace 'old.com' 'new.com' wp_posts --limit=1000 --offset=1000
wp search-replace 'old.com' 'new.com' wp_posts --limit=1000 --offset=2000
# Script for automation
#!/bin/bash
TABLE="wp_posts"
BATCH_SIZE=1000
OFFSET=0
while true; do
COUNT=$(wp db query "SELECT COUNT(*) FROM $TABLE LIMIT $OFFSET, $BATCH_SIZE" --skip-column-names)
if [ "$COUNT" -eq 0 ]; then
break
fi
wp search-replace 'old.com' 'new.com' $TABLE --limit=$BATCH_SIZE --offset=$OFFSET
OFFSET=$((OFFSET + BATCH_SIZE))
done
Maintenance Mode During Migration
// Enable maintenance mode
wp maintenance-mode activate
// Run your migration commands
wp search-replace ...
// Disable maintenance mode
wp maintenance-mode deactivate
Database Optimization After Migration
# Optimize tables after large updates
wp db optimize
# Repair if needed
wp db repair
# Check table status
wp db query "SHOW TABLE STATUS"
FAQ
Why can’t I just use SQL REPLACE() to update URLs?
Simple SQL REPLACE() breaks serialized data because it doesn’t update the character count stored in the serialization. WordPress stores complex data (arrays, objects) as serialized strings where each string has a length prefix. When you change a URL from ‘old.com’ to ‘new-domain.com’, the length changes but the serialized data still shows the old length, causing PHP to read the wrong number of characters and corrupt the entire data structure.
What is serialized data in WordPress?
Serialized data is PHP’s way of converting arrays and objects into strings that can be stored in a database. WordPress uses serialization extensively for widget data, theme options, plugin settings, and post metadata. The format includes type indicators and length prefixes (e.g., s:17:“string”) that must remain accurate for the data to be readable.
Which method is best for large WordPress sites?
WP-CLI is the best method for large sites because it’s fast, handles serialization correctly, can process in batches, and doesn’t have the timeout issues that web-based plugins face. For a 1GB+ database, WP-CLI can complete in minutes while plugins may timeout or crash.
What should I do if widgets disappear after migration?
If widgets disappear, your serialized data was corrupted. Restore from your backup immediately using ‘wp db import backup.sql’. Then re-run the migration using a serialization-aware tool like WP-CLI or Better Search Replace. Never try to manually fix serialized data.
Do I need to update URLs when moving from HTTP to HTTPS?
Yes, you should update all URLs from HTTP to HTTPS in the database to avoid mixed content warnings and ensure all resources load securely. Use the same search-replace methods described in this guide, replacing ‘http://’ with ‘https://’ across all tables.
How do I handle multiple domain changes (staging, dev, production)?
For multi-environment setups, run search-replace for each environment transition. Always use dry-run first to preview changes. Consider using environment-specific wp-config.php configurations with WP_HOME and WP_SITEURL defines to make future migrations easier.
Can I undo a search-replace operation?
No, search-replace cannot be undone without a backup. Always create a database backup before running any search-replace operation. If something goes wrong, restore from your backup and try again with corrected parameters.
How do I verify all URLs were updated?
Run verification queries to check for remaining old URLs:
wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%old-domain.com%'"
wp db query "SELECT COUNT(*) FROM wp_options WHERE option_value LIKE '%old-domain.com%'"
wp db query "SELECT COUNT(*) FROM wp_postmeta WHERE meta_value LIKE '%old-domain.com%'"
If all counts return 0, all URLs have been successfully updated.
What about URLs in page builder content?
Page builders like Elementor, Beaver Builder, and Divi store content as serialized or JSON-encoded data. WP-CLI’s search-replace handles these automatically. However, some page builders may require you to regenerate CSS or clear their cache after migration:
# Elementor
wp elementor flush-css
# Beaver Builder
wp bb flush
Should I update the GUID column in wp_posts?
No, never update the GUID column. GUIDs (Globally Unique Identifiers) are meant to be permanent and should not change, even when migrating to a new domain. WordPress uses GUIDs for feed readers and other systems that need a stable identifier.
Summary: The Complete Migration Workflow
-
Pre-Migration:
- Create full backup (database + files)
- Document current URLs and configuration
- Set up staging environment for testing
- Verify SSL certificate (if applicable)
-
Migration:
- Run dry-run first
- Execute search-replace with serialization-aware tool
- Update wp-config.php constants
- Configure redirects
- Update DNS
-
Post-Migration:
- Clear all caches
- Test critical functionality
- Verify no old URLs remain
- Update Search Console
- Monitor error logs
-
Optimization:
- Optimize database tables
- Clear transients
- Test performance
- Set up monitoring
Following this comprehensive guide ensures your WordPress migration is smooth, safe, and complete. Remember: always backup, always test, and always verify.



