Moving a WordPress site from a development environment to the production domain, renaming a brand or rolling out an SSL certificate (moving from HTTP to HTTPS) are routine jobs for every administrator and developer. At first glance the operation looks trivial: export the database to a .sql file, open it in a code editor, run “Find and replace”, then import the file back. Or run a quick SQL query in phpMyAdmin.
Doing that to a production WordPress database breaks the site straight away. Widgets vanish from the footer and sidebars, the theme’s colour and typography settings reset, advanced fields (ACF) return empty values, and e-commerce plugin settings are wiped.
The cause is the way WordPress core and plugins store complex data structures in a relational MySQL or MariaDB database. The WordPress documentation warns about it directly: a plain search and replace across the whole database can break serialization, because some themes and widgets store a URL together with its length (Migrating WordPress, developer.wordpress.org). Understanding PHP serialization and knowing the right tools, above all the WP-CLI command line, is the foundation of managing WordPress sites safely. For the wider process around the database, see the WordPress database migration guide.
What is serialized data in the WordPress database
Relational SQL databases are designed to store scalar data: numbers, short strings and dates. WordPress, however, often needs to save a complex, multi-level structure in a single table row, for example an array holding the list of active widgets, their order, assigned icons and configuration parameters.
Instead of creating dozens of extra normalised tables for every plugin, WordPress developers chose a native PHP function: serialize().
// Example theme configuration array in PHP
$theme_settings = array(
'site_name' => 'My WordPress Shop',
'logo_url' => 'https://old-domain.com/logo.png',
'contact_mail' => '[email protected]'
);
// Result of serialize($theme_settings)
// Stored directly in the option_value column of wp_options:
a:3:{s:9:"site_name";s:17:"My WordPress Shop";s:8:"logo_url";s:31:"https://old-domain.com/logo.png";s:12:"contact_mail";s:21:"[email protected]";}How to read a serialized string
Take a closer look at the fragment that holds the logo URL:
s:31:"https://old-domain.com/logo.png";Every element in PHP’s serialized format has a strict structure:
sis the data type: string.31is the declared length of the string (the exact number of bytes)."https://old-domain.com/logo.png"is the actual value in quotes.
The number 31 is the key to the whole puzzle. When PHP reads the data from the database it calls unserialize(). The PHP parser does not search blindly for the closing quote: it counts exactly 31 bytes from the given position.
Why SQL REPLACE breaks serialized data
Imagine moving the site to a new, longer domain: https://new-and-much-longer-company-domain.com. The new logo URL, https://new-and-much-longer-company-domain.com/logo.png, is 55 characters long.
If you run a classic SQL query:
-- CRITICAL ERROR: this query destroys serialization!
UPDATE wp_options
SET option_value = REPLACE(option_value, 'https://old-domain.com', 'https://new-and-much-longer-company-domain.com');the database ends up with this string:
s:31:"https://new-and-much-longer-company-domain.com/logo.png";What happens when a visitor loads the site:
- WordPress fetches the row from the database and passes it to
unserialize(). - The function reads the
s:31header and expects exactly 31 bytes. - PHP cuts off the first 31 characters of the new address and finds random letters inside the string where the closing quote and semicolon should be.
- The PHP parser raises
PHP Notice: unserialize(): Error at offset...and returnsfalse. - The theme or plugin receives
falseinstead of the expected options array. - WordPress treats the data as missing and restores the theme’s factory defaults or drops the widgets.
Where WordPress stores URLs in the database
URLs in a WordPress install do not live in one place. They are spread across many core tables and tables created by plugins:
graph TD
A["WordPress database"] --> B["wp_options"]
A --> C["wp_posts"]
A --> D["wp_postmeta"]
A --> E["wp_comments / wp_commentmeta"]
A --> F["wp_usermeta"]
A --> G["Plugin tables (WooCommerce, forms, SEO)"]
B --> B1["siteurl and home (main addresses)"]
B --> B2["sidebars_widgets (serialized)"]
B --> B3["theme_mods (serialized)"]
C --> C1["post_content (links, images)"]
C --> C2["post_excerpt"]
C --> C3["guid (do NOT change on published posts)"]
D --> D1["_elementor_data (escaped JSON)"]
D --> D2["_wp_attached_file (media paths)"]
D --> D3["ACF fields (serialized)"]1. The wp_options table
This is the most important configuration table. It holds two critical records that decide whether the site works at all:
siteurl: where the WordPress core files physically live.home: the main address of the site that visitors type into the browser.
Besides those, the table stores in serialized form:
sidebars_widgets: the widget layout in sidebars and footers.theme_mods_[theme_name]: colours, header layout, typography and logo from the Customizer.- Settings of SEO plugins (Rank Math, Yoast), security plugins and cache plugins.
2. The wp_posts table
Stores the content of posts, pages, WooCommerce products and media library items.
- The
post_contentcolumn: absolute links to other pages, embedded<img>tags, links to PDF files and Gutenberg block comments. - The
guidcolumn (Globally Unique Identifier): the unique identifier of a post. Important rule: the WordPress documentation says: “Never, ever, change the contents of the GUID column, under any circumstances.” (Migrating WordPress, developer.wordpress.org). Feed readers use theguidcolumn to detect new articles. Changing it makes feed readers treat every old post as new and send repeat notifications to subscribers.
3. The wp_postmeta table
Holds extra attributes of posts and pages. This is where the serialized arrays of Advanced Custom Fields (ACF), attachment metadata (_wp_attachment_metadata) and page builder configuration from Elementor or Divi end up.
4. The wp_usermeta and wp_comments tables
They hold the website URLs of comment authors and user profile metadata.
How to change the WordPress domain with WP-CLI search-replace
The WordPress command line interface (WP-CLI) is the fastest, most reliable and fully scriptable tool for migrating URLs. It runs directly on the server over SSH and works on PHP data structures in memory.
If WP-CLI is not installed yet, the official installation steps in the WP-CLI handbook download the phar file, make it executable and move it into the path:
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --infoStep 1: back up the database
Before you touch a production database, dump it to an SQL file:
# Database dump with DROP TABLE statements
wp db export backup-before-migration-$(date +%Y%m%d_%H%M%S).sql --add-drop-tableIf the database gets damaged, you can restore the original state in seconds:
wp db import backup-before-migration-*.sqlStep 2: run a dry run (—dry-run)
The --dry-run flag shows how many rows in which tables would change, without changing anything in the database:
wp search-replace 'https://old-domain.com' 'https://new-domain.com' --all-tables --dry-runThe output is a table with the columns Table, Column, Replacements and Type, plus a summary line with the total number of replacements. The Type column shows whether a column was processed in SQL or in PHP. If the count is zero, the old address usually differs from the one stored in the database by protocol, the www prefix or a trailing slash.
Step 3: run the real replacement
For the final replacement we recommend this set of flags:
wp search-replace 'https://old-domain.com' 'https://new-domain.com' \
--all-tables \
--precise \
--recurse-objects \
--skip-columns=guid \
--report-changed-onlyWhat each parameter does:
--all-tables: searches every table in the database, including custom tables from e-commerce, form and logging plugins.--precise: forces PHP instead of SQL for all columns. The WP-CLI documentation describes this as slower but more reliable with complex serialized data.--recurse-objects: recurses into serialized objects. It is on by default and turned off with--no-recurse-objects, so writing it out only documents the intent.--skip-columns=guid: skips theguidcolumn inwp_posts, which protects feed readers from duplicate entries.--report-changed-only: lists only the tables where something actually changed and hides hundreds of empty rows.
To restrict the run to a few tables, pass their names as arguments, for example wp search-replace 'https://old-domain.com' 'https://new-domain.com' wp_posts wp_postmeta. For a case-insensitive match, combine --regex with --regex-flags='i'; the documentation notes that regex mode is much slower.
How to change the domain in a WordPress multisite network
In multisite installs (a network of sites) the core tables carry per-site prefixes (for example wp_2_posts, wp_3_options), and the domains of the individual subsites live in the wp_blogs and wp_site tables.
Here you use --network, which covers every table registered in $wpdb for the whole network, or --all-tables-with-prefix, which adds prefixed tables that are not registered in $wpdb:
wp search-replace 'https://old-network.com' 'https://new-network.com' --network --all-tables-with-prefixThe WordPress documentation admits that for a multisite domain change “This step cannot yet be easily automated.” (Migrating WordPress, developer.wordpress.org). After the replacement, check by hand:
- the
wp_siteandwp_blogstables, which store the domain and path of the network and of every subsite, - the
home,siteurlandfileupload_urloptions in each site’s options table (wp_options,wp_2_optionsand so on), - when switching between subdomains and subdirectories: the
.htaccessfile and theSUBDOMAIN_INSTALLconstant inwp-config.php.
How to export the database with new URLs to an SQL file
If you prepare a database for a client on a local environment and want a ready SQL file with the domains replaced, without touching your local working database:
wp search-replace 'https://localhost/project' 'https://client-domain.com' \
--all-tables \
--export=database-ready-to-import.sqlHow to move from www to the bare domain
When only the prefix changes, replace the full address including the protocol, so you do not hit other strings that contain the same name:
wp search-replace 'https://www.yourdomain.com' 'https://yourdomain.com' --all-tables --precise --skip-columns=guid --dry-runHow to change URLs with the Better Search Replace plugin
If you manage a site on shared hosting without SSH, or prefer to work from the WordPress dashboard, the safest choice is the Better Search Replace plugin, maintained by WP Engine (formerly Delicious Brains).
Step by step in the WordPress dashboard:
1. Go to Plugins > Add Plugin and search for "Better Search Replace".
2. Install and activate the plugin.
3. Go to Tools > Better Search Replace.
4. In the "Search for" field enter the old URL (e.g. https://old-domain.com).
5. In the "Replace with" field enter the new URL (e.g. https://new-domain.com).
6. Select every table in the list (Ctrl+A on Windows or Cmd+A on macOS).
7. Make sure "Run as dry run?" is CHECKED.
8. Click "Run Search/Replace" and review the report.
9. Uncheck "Run as dry run?" and run it again to save the changes.Better Search Replace errors and how to fix them
- 504 Gateway Timeout or exceeded script execution time (
max_execution_time): On large databases the web server can cut the HTTP request short. In the plugin’s Settings tab, lower Max Page Size. The default is 20000 rows per chunk, as the plugin source code shows (class-bsr-admin.php); a smaller chunk means more, shorter requests.
How to change siteurl and home in WordPress
Two options in the wp_options table decide whether you can even log in at the new address. The WordPress documentation (Changing The Site URL) separates them like this: home is the “Site Address (URL)”, the address visitors type into the browser, and siteurl is the “WordPress Address (URL)”, the place where the core files live. Both fields in Settings > General should include https:// and should not end with a slash.
The documentation describes several ways to change these values. Each fits a different situation:
| Method | When to use it | What to watch |
|---|---|---|
wp option update in WP-CLI | You have SSH, the dashboard is down | Changes only the two options, wp search-replace fixes the rest |
WP_HOME and WP_SITEURL constants in wp-config.php | You need the site up quickly | Values are hard-coded, the fields in Settings > General can no longer be edited |
update_option() in the theme’s functions.php | You only have FTP | Remove the code right after the dashboard loads |
The RELOCATE constant | The site moved to another directory or server | Fixes only the admin URLs, and is insecure if left in the file |
Editing wp_options in phpMyAdmin | You only have the hosting panel | Back up first; the table prefix may not be wp_ |
How to change siteurl and home with WP-CLI
The documentation gives two commands, followed by a content replacement that skips the guid column:
wp option update home 'https://new-domain.com'
wp option update siteurl 'https://new-domain.com'
wp search-replace 'https://old-domain.com' 'https://new-domain.com' --skip-columns=guidHow to change the site URL in functions.php
When you only have FTP access, the documentation suggests adding two lines to the active theme’s functions.php, right after the opening <?php:
update_option( 'siteurl', 'https://new-domain.com' );
update_option( 'home', 'https://new-domain.com' );Upload the file, load the login screen or the dashboard a couple of times, then remove the lines. The documentation is explicit: “Do not leave this code in the functions.php file.” (Migrating WordPress, developer.wordpress.org). The code would run on every request and overwrite options you later want to change from the dashboard.
How to use the RELOCATE constant after moving a directory
The constant define( 'RELOCATE', true );, added to wp-config.php above the That's all, stop editing! comment, makes WordPress accept the current address after you log in through wp-login.php on the new server. According to the documentation it fixes only the admin URLs, not the rest of the configuration, and leaving it in place is insecure: “Always remove the RELOCATE line from wp-config.php after you’re done.” (Migrating WordPress, developer.wordpress.org).
Transients in a migrated database
The documentation points out transients, the temporary cached values stored in wp_options. They can hold old URLs, and since WordPress rebuilds them on its own, they are safe to delete from the migrated copy of the database. WP-CLI does it with wp transient delete --all, and the documentation also gives a query for phpMyAdmin, to be run after a backup:
DELETE FROM `wp_options` WHERE `option_name` LIKE '%\_transient\_%';How to change URLs when the WordPress dashboard is down
In an emergency, when there is no SSH and the WordPress dashboard is down because of a redirect loop or damaged files, the long-standing tool is the Search-Replace-DB script (known as SRDB) made by the agency Interconnect/it.
How to use the SRDB script:
1. Download the official zip archive from interconnectit.com.
2. Unpack it and rename the folder to a unique, hard-to-guess string (e.g. srdb-sec-8f7a9d/).
3. Upload the folder over FTP / SFTP to the WordPress root (where wp-config.php is).
4. Open https://your-domain.com/srdb-sec-8f7a9d/ in the browser.
5. The script reads the database credentials from wp-config.php automatically.
6. Fill in the "replace" (old value) and "with" (new value) fields.
7. Click "dry run", and after checking, "live run".
8. CRITICAL: when finished, click the red "delete me" button so the script removes itself from the server.[!CAUTION] Leaving the
Search-Replace-DBscript on a live server is a critical security hole. Anyone who guesses the path gets full read and write access to the database without a password. Always confirm that the folder is gone once the work is done!
The same applies to migration plugins: deactivate and remove them after the move if you no longer need them.
How to change URLs in Elementor, Divi and Gutenberg
Modern page builders and the Gutenberg block editor add extra layers of data encoding that you need to remember when updating URLs.
graph LR
subgraph WP_CLI_Execution["Step 1: database"]
A["wp search-replace"] --> B["wp_posts, wp_postmeta, wp_options tables"]
end
subgraph Builder_Sync["Step 2: page builders"]
B --> C["Elementor: Tools > Replace URL"]
B --> D["Elementor: regenerate CSS files"]
end
subgraph Edge_Purge["Step 3: cache and edge"]
C & D --> F["wp cache flush (Redis/Memcached)"]
F --> G["Purge CDN (Cloudflare)"]
end1. URLs in Elementor JSON data
Elementor stores page layouts in the _elementor_data meta field as JSON, where the slashes in URLs are often escaped:
{\"url\":\"https:\/\/old-domain.com\/wp-content\/uploads\/2026\/08\/banner.jpg\"}A plain search for https://old-domain.com may not match the escaped https:\/\/old-domain.com. Elementor also compiles style rules into static CSS files in wp-content/uploads/elementor/css/.
The right procedure after migrating an Elementor site:
- Run the standard
wp search-replaceon the database. - Log in to the WordPress dashboard and go to Elementor > Tools > Replace URL.
- Enter the old and new URL and click Replace URL.
- On the General tab click Regenerate Files & Data.
2. URLs in Gutenberg blocks
Gutenberg blocks store media attributes directly in HTML comments inside post_content:
<!-- wp:image {"id":412,"sizeSlug":"full","linkDestination":"custom"} -->
<figure class="wp-block-image size-full"><a href="https://old-domain.com/offer/"><img src="https://old-domain.com/wp-content/uploads/2026/08/graphic.png" alt="Offer"/></a></figure>
<!-- /wp:image -->Both wp search-replace and Better Search Replace handle this without trouble, as long as the post_content column of wp_posts is searched.
3. URLs hard-coded in theme files
Some themes and plugins store URLs directly in PHP, CSS or JavaScript files. No database replacement reaches them, but they are easy to find on the server:
grep -rn "old-domain.com" wp-content/themes/your-theme/How to change the domain of a WooCommerce shop
When changing the domain of a WooCommerce shop, pay special attention to modules that talk to external payment systems, carriers and ERP systems:
- Payment gateways (Stripe, PayPal and others):
- Most payment gateways register callback notification URLs (webhooks or IPN) in their developer dashboards. If you move from
old.comtonew.com, notifications about paid orders keep going to the old address and produce transaction errors. - After running
wp search-replace, log in to each payment provider’s dashboard and update the webhook URL.
- Most payment gateways register callback notification URLs (webhooks or IPN) in their developer dashboards. If you move from
- WooCommerce REST API keys and webhooks:
- In WooCommerce > Settings > Advanced > Webhooks check that the Delivery URLs have been updated. If webhooks sync stock levels with an ERP system, update the endpoint addresses in the integration software too.
- Sessions and customer carts:
- The
wp_woocommerce_sessionstable stores temporary cart data. After a domain change it is worth clearing sessions in WooCommerce > Status > Tools > Clear customer sessions to avoid session token errors after the domain cookies change.
- The
How to run search-replace on a large database or MySQL cluster
On very large databases a single wp search-replace pass over all tables takes a long time, and for that whole time it writes to tables the live site is using. The WP-CLI documentation lists two limits worth knowing: tables without a primary key are skipped, and the --regex and --log options slow the run down considerably.
1. How to split search-replace into smaller parts
wp search-replace has no --batch-size or --limit option; the option list in the WP-CLI documentation does not include them, and instructions that use them will not work. You split the work differently: pass table names as arguments (wildcards are allowed) or exclude tables with --skip-tables:
# Content first, then metadata, in separate runs
wp search-replace 'https://old.com' 'https://new.com' wp_posts --precise --skip-columns=guid
wp search-replace 'https://old.com' 'https://new.com' wp_postmeta --precise
# Everything except a large plugin log table
wp search-replace 'https://old.com' 'https://new.com' --all-tables --skip-tables='wp_*_log' --precise --skip-columns=guidThe --include-columns option narrows the replacement to the columns you list, and with --export the number of rows per INSERT statement is set by --export_insert_size (default 50).
2. Search-replace with replication (Amazon Aurora, MySQL Galera)
If the database runs in a primary and replica setup or a Galera cluster, a bulk change to millions of rows produces a heavy stream of data in the binary log (binlog). That can cause significant replication lag on the read nodes.
Recommended approach for high-availability environments:
- Run the operation in a maintenance window with the lowest traffic.
- Temporarily detach the replica nodes for the bulk write.
- Use the
--exportflag to prepare the processed SQL file on a separate work machine and import it during the maintenance window.
3. Maintenance mode during the migration
WP-CLI can switch WordPress into maintenance mode, so visitors do not see a site that is halfway moved to the new address:
wp maintenance-mode activate
wp search-replace 'https://old.com' 'https://new.com' --all-tables --precise --skip-columns=guid
wp maintenance-mode deactivateHow to switch WordPress from HTTP to HTTPS
If the goal of the migration is not a new domain but an SSL certificate and a switch from the unencrypted protocol to the encrypted one:
wp search-replace 'http://yourdomain.com' 'https://yourdomain.com' \
--all-tables \
--precise \
--skip-columns=guidThis removes the mixed content problem, where the browser blocks scripts, fonts or images served over the old http:// protocol. Keep the domain in the search string: replacing a bare http:// would also rewrite links to other sites.
301 redirects in Nginx and Apache
After changing the domain in the database, the critical SEO step is redirecting all traffic from the old address to the new one with HTTP status 301 (Moved Permanently).
Nginx configuration (/etc/nginx/sites-available/old-domain.conf):
server {
listen 80;
listen 443 ssl http2;
server_name old-domain.com www.old-domain.com;
ssl_certificate /etc/letsencrypt/live/old-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/old-domain.com/privkey.pem;
return 301 https://new-domain.com$request_uri;
}Apache configuration (.htaccess on the old server):
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(?:www\.)?old-domain\.com$ [NC]
RewriteRule ^(.*)$ https://new-domain.com/$1 [L,R=301]
</IfModule>To confirm the redirect and the new site, check the response codes: curl -s -o /dev/null -w "%{http_code}" https://old-domain.com/ should return 301, and the same command against the new domain 200.
wp-config.php constants versus database values
A common emergency fix is forcing the URLs in wp-config.php:
define('WP_HOME', 'https://new-domain.com');
define('WP_SITEURL', 'https://new-domain.com');How these constants work in practice:
- Constants defined in
wp-config.phpoverride thesiteurlandhomevalues WordPress reads, but do not change the records in the database. The documentation calls this hard-coding the values and says it is not necessarily the best fix. - The address fields in Settings > General can no longer be edited.
- Links in post content, attachments and widgets still point to the old address.
- Treat the constants in
wp-config.phpas a temporary fix; the database itself should always be updated withwp search-replace.
WordPress domain change checklist
Before you start, follow a structured plan:
| Stage | Task | Tool / command |
|---|---|---|
| Before migration | Full database dump | wp db export backup-$(date +%s).sql --add-drop-table |
| Before migration | Copy of wp-content/uploads and .htaccess | rsync / tar -czf uploads.tar.gz |
| Before migration | Disable cache and minification plugins | WP dashboard / wp plugin deactivate |
| During | Dry run of the replacement | wp search-replace 'old' 'new' --all-tables --dry-run |
| During | Real replacement in the database | wp search-replace 'old' 'new' --all-tables --precise --skip-columns=guid |
| During | Replace domains in builders (Elementor) | Dashboard > Elementor > Tools > Replace URL |
| After migration | Flush object cache and transients | wp cache flush and wp transient delete --all |
| After migration | Rebuild permalinks | wp rewrite flush --hard |
Bash script to sync a WordPress database between environments
In modern workflows built on Docker or local development and staging environments (DDEV, Lando, LocalWP), typing commands by hand is slow and error-prone. Engineering teams usually keep a reusable Bash script that pulls the production database, imports it locally, replaces the domains and anonymises sensitive customer data.
#!/usr/bin/env bash
# Automatic database sync script: sync-db.sh
set -eo pipefail
PROD_HOST="your-production-server.com"
PROD_USER="deploy"
PROD_PATH="/var/www/html"
PROD_URL="https://yourcompany.com"
LOCAL_URL="https://project.local"
BACKUP_DIR="./backups"
mkdir -p "${BACKUP_DIR}"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
DUMP_FILE="${BACKUP_DIR}/dump_prod_${TIMESTAMP}.sql"
echo "==> 1/4 Pulling the production database over SSH..."
ssh "${PROD_USER}@${PROD_HOST}" "cd ${PROD_PATH} && wp db export - --add-drop-table" > "${DUMP_FILE}"
echo "==> 2/4 Importing the database locally..."
wp db import "${DUMP_FILE}"
echo "==> 3/4 Replacing domains with serialization support..."
wp search-replace "${PROD_URL}" "${LOCAL_URL}" \
--all-tables \
--precise \
--recurse-objects \
--skip-columns=guid \
--report-changed-only
echo "==> 4/4 Flushing caches and resetting permalinks..."
wp cache flush
wp rewrite flush --hard
echo "==> Done. The local environment is synced at ${LOCAL_URL}."How to verify serialization after the migration
To be certain that no field was damaged during the migration, run a short PHP snippet through WP-CLI that checks every serialized record in the wp_options table:
wp eval '
global $wpdb;
$options = $wpdb->get_results("SELECT option_name, option_value FROM {$wpdb->options} WHERE option_value LIKE \"a:%\" OR option_value LIKE \"O:%\"");
$broken = 0;
foreach ($options as $opt) {
if (is_serialized($opt->option_value) && @unserialize($opt->option_value) === false) {
echo "Broken option: " . $opt->option_name . PHP_EOL;
$broken++;
}
}
if ($broken === 0) {
echo "All serialized options in wp_options are valid." . PHP_EOL;
} else {
echo "Found " . $broken . " broken records!" . PHP_EOL;
}
'To find old URLs left behind, count them directly, for example wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_content LIKE '%old-domain.com%'", and the same for wp_postmeta.meta_value and wp_options.option_value.
WordPress errors after a domain change and how to fix them
1. Redirect loop after a domain change (ERR_TOO_MANY_REDIRECTS)
If the site falls into an endless redirect loop after the domain change:
- Check that
siteurlandhomeinwp_optionsuse exactly the same protocol (https://) and prefix (wwwor bare domain) as the web server or Cloudflare is configured for. - Check the SSL setting in Cloudflare (SSL/TLS mode should be Full (Strict), not Flexible).
- Reset
.htaccessto the WordPress default with:wp rewrite flush --hard
2. CSS styles and icons missing after migration
- If the page loads as raw text without styles, the usual causes are stylesheets and scripts still pointing to the old address, mixed content after the move to HTTPS, or resources blocked by a CORS policy.
- Open the browser developer tools (F12 > Console tab) to find files loaded from the old
http://address.
3. Broken widgets or ACF fields
- If the serialization rules were ignored and a raw SQL
REPLACEwas run, the only safe way out is restoring the database from the earlier dump (wp db import backup.sql) and repeating the operation withwp search-replace --precise.
4. Allowed memory size exhausted in WP-CLI
During search-replace on tables with large amounts of text (for example big log tables or post revisions), the PHP process in the terminal can run out of its memory limit:
- To raise the memory limit for a single WP-CLI command, run it with a PHP setting:
php -d memory_limit=1024M /usr/local/bin/wp search-replace 'https://old.com' 'https://new.com' --all-tables - It also helps to delete unneeded post revisions and expired transients before the migration:
wp transient delete --all && wp post delete $(wp post list --post_type='revision' --format=ids) --force
5. Character set and collation mismatch
When moving a database dump to an older or different database server, the target server may not know the collation used in the dump, for example utf8mb4_unicode_520_ci:
- If the import of the
.sqlfile fails with#1273 - Unknown collation: 'utf8mb4_unicode_520_ci', replace that string in the SQL file before importing:sed -i 's/utf8mb4_unicode_520_ci/utf8mb4_unicode_ci/g' backup.sql - Both collations store
utf8mb4, so accented characters are kept; only the sorting and comparison rule changes.
Summary
Updating URLs in a WordPress database correctly requires knowing how PHP and relational databases handle the data. Raw SQL queries or editing dump files in a text editor are the shortest route to a broken site and lost configuration.
WP-CLI with the flags --all-tables --precise --skip-columns=guid, preceded by a backup and a --dry-run pass, keeps serialized data and the guid column intact.
Planning a full rebuild, a move to a headless architecture or a migration to a newer platform? See our services for migrating from WordPress to Astro and Next.js and the dedicated Core Web Vitals audit.







