The old domain name is still in 800 posts
You rebranded last month. The domain changed, the plugin name changed, maybe even the company name changed. The content editor updated the homepage and the about page. The other 800 posts still say the old thing. Every one of them.
It isn't just branding. A partner's URL rotated, a product was discontinued, a typo slipped through a batch import and now lives in 300 WooCommerce product descriptions. Fixing them one by one is not a plan. It is a punishment.
What most people do
search-replace both touch the database directly and can corrupt serialized values. They also operate outside WordPress, so revisions are not created and there is no per-post rollback.How to find and replace in WordPress with SQL
The no-plugin route is a raw SQL UPDATE with the REPLACE() function, run from phpMyAdmin or Adminer. Take a full database backup first, this runs outside WordPress and has no undo:
-- back up the database first, there is no undo
UPDATE wp_posts
SET post_content = REPLACE( post_content, 'acme-widgets.com', 'acme.com' )
WHERE post_content LIKE '%acme-widgets.com%'; It works for a one-off on a plain-text column, but the caveats are exactly why people get burned:
- Serialized data breaks. Widgets, page-builder layouts, and many meta values store the byte length of each string. A
REPLACE()that changes a string's length leaves the stored length wrong and corrupts the whole blob. Only touch plain text fields likepost_contentandpost_title. - No hooks, no revisions, no cache clear. Because it bypasses WordPress, no revision is created, so there is no per-post rollback, and object/page caches keep serving the old text until purged.
- The table prefix may not be
wp_. Check$table_prefixinwp-config.phpbefore running, or you will update nothing (or the wrong table).
If you would rather preview first, keep revisions, and never think about serialization, the next section does the same job safely.
A better way: one command, dry run first
TrueCommander's replace command runs through wp_update_post, so WordPress handles serialization, fires the right hooks, and creates a revision for every changed post. You get a per-post rollback trail without any extra effort.
-dry-run=true to commitShown in advanced mode, where commands start with tp. In easy mode you type the same command without the tp prefix.
Always run with -dry-run=true first. The live run updates posts immediately via wp_update_post and creates revisions for rollback, but there is no cross-post undo. Preview the match count, confirm the right posts are in scope, then drop -dry-run=true to commit.
How it works
-dry-run=true The command scans every post in scope and reports how many would change. Nothing is written. Run this until the match count looks right.-dry-run=true and run again. Each matched post is updated through wp_update_post, which fires hooks and creates a revision automatically.| Parameter | Details |
|---|---|
-from | Text or regex pattern to find. Required. |
-to | Replacement text. Leave empty to delete matched text. |
-dry-run | Default false. Set to true to preview without writing any changes. |
-case_sensitive | Default true. Set to false for case-insensitive matching. |
-whole_word | Default false. Adds \b word boundaries so "cat" does not match "category". Mutually exclusive with -regex. |
-regex | Default false. Treats -from as a PCRE pattern (no delimiters needed). Mutually exclusive with -whole_word. |
-fields | CSV of fields to search: content, title, excerpt. Default content. |
-post_type | CSV of post type slugs. Default: all public types except attachments. |
-post_status | Default publish,draft,private,pending,future. |
-post_ids | Comma-separated post IDs. Overrides -post_type, -post_status, and -limit. Useful when chaining from filter posts. |
-limit | Max posts per run. Default 500, max 5000. |
| Can be used in |
Real example
You migrated a client's site from staging.example.com to example.com three weeks ago. Everything looked fine in the browser, but a spot-check of product descriptions today revealed the old staging URL in 63 posts. It is in the content field only, never in titles.
You run tp replace -from="staging.example.com" -to="example.com" -dry-run=true -post_type=product. The dry run comes back: 63 matches across 58 products. Good. You drop -dry-run=true and run again. Fifty-eight products updated, 58 revisions created. You open three at random in the editor, confirm the URL is gone, and you are done in under two minutes.
One product has a custom field you forgot about. No problem: the revision for that post is sitting in the WordPress revision screen, one click to restore.
Goes further with TrueCommander
filter posts Run filter posts to get the exact post IDs you want, then pass them to tp replace -post_ids=... so only those posts are touched.Frequently asked questions
UPDATE ... REPLACE() in phpMyAdmin (back up first, and only on plain-text columns), or the replace command, which runs through wp_update_post so WordPress handles serialization and creates a revision for every changed post.post_content and post_title, and only after a full backup. Running it across serialized data corrupts it, and there is no undo. If you are unsure what is serialized, use a method that goes through WordPress instead.REPLACE() can, because serialized values store each string's byte length and a length change leaves it invalid. The replace command sidesteps this by editing the content, title, and excerpt text fields through wp_update_post, not by rewriting serialized blobs.-dry-run=true to see how many posts would change without writing anything. Run it until the match count looks right, then drop the flag to commit. Always preview first, there is no all-at-once undo.