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=false -confirm=REPLACEShown in advanced mode, where commands start with tp. In easy mode you type the same command without the tp prefix.
Preview first with -dry-run=true. Apply only with -dry-run=false -confirm=REPLACE. Live runs scan at most 500 candidates and update at most 100 matching posts per batch. A successful live run returns a one-hour actor-bound recovery token that restores exact original fields independently of revisions.
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=false -confirm=REPLACE. At most 500 candidates are scanned and 100 matching posts updated per batch. Preserve the one-hour recovery token and inspect the receipt.| Parameter | Details |
|---|---|
-from | Text (or regex pattern when -regex=true) to find. |
-to | Replacement text. Leave empty to delete the matched text. |
-dry-run | Defaults to true and reports matches without touching posts. A live run must set false and provide -confirm=REPLACE. |
-case_sensitive | Default true. Set false to match regardless of case. |
-whole_word | Only match standalone words (adds \b boundaries). Mutually exclusive with regex. |
-regex | Treat -from as a PCRE pattern (no delimiters. pattern is wrapped in ~...~u internally). Mutually exclusive with whole_word. |
-fields | Comma-separated: content, title, excerpt. Default: content. |
-post_type | Exact comma-separated public editor post type slug(s). Default: supported public UI types excluding attachments. Unknown or unsupported values fail closed. |
-post_status | Exact comma-separated safe status(es): publish, draft, private, pending, future. Unknown values fail closed. |
-post_ids | Restrict to at most 500 positive whole-number IDs. Type, status, field, authority, scan, and update bounds still apply. |
-limit | Candidate posts scanned in this batch: whole number from 1 to 500. Default 500. At most 100 matching posts are updated. |
| Can be used in | |
-confirm | Exact value REPLACE required together with -dry-run=false. |
-restart | Start this exact actor/mode/scope again at the first eligible ID. Defaults false; use deliberately because replacements may be reapplied. |
-restore | Restore exact original fields using an unexpired one-hour token from a live run. Cannot be combined with replacement parameters. |
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.
For example, preview the old hostname in eligible product content. Confirm the scope and matches, then apply using -dry-run=false -confirm=REPLACE. Inspect the receipt, continue any remaining batch, and preserve the short-lived recovery token.
Replacement targets supported post fields rather than arbitrary custom metadata. Use the unexpired recovery token with tp replace -restore=TOKEN if you need to restore original fields. Keep a site backup for broader recovery.
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 write changes, explicitly pass -dry-run=false -confirm=REPLACE. Review the result, bounded batch, and recovery-token receipt.tp replace -restore=TOKEN. Recovery is scoped and does not replace an independent backup.