Introduction
If you’re reading this, you’ve probably just done what I did: changed a URL using Permalink Manager Pro (say, shortening yoursite.com/blog/gdpr to yoursite.com/gdpr), hit a 404 on both the old and new URL even after clearing your cache, and then — trying to fix it — used the plugin’s “Regenerate/Reset custom permalinks” tool.
If hundreds of your URLs suddenly changed back to their default, longer structure right after that, this post is for you. Here’s the full path to recovering them, and how to avoid it happening again.
What "Regenerate Custom Permalinks" Actually Does
The regenerate/reset tool doesn’t just fix one broken link — it rebuilds custom permalinks for every post (or whichever post type/taxonomy you select) based on your site’s default Permastructure settings, not your manual edits.
In practice, that means:
- It’s a bulk operation.If you run it against “Posts” without limiting to specific IDs, it touches every post of that type on your site — not just the one you meant to fix.
- It overwrites manual customizations.Any custom short URL you’d typed in by hand gets replaced with whatever the default pattern generates (often a longer /blog/post-name style structure).
- It doesn’t touch your actual WordPress post slug(post_name in the database) — only the plugin’s own custom-permalink data.
- It does not create redirects automatically.Old URLs won’t forward to the new ones unless you have that separately enabled in plugin settings.
- There’s no built-in undo.Once you click regenerate, the only way back is a database backup — the plugin doesn’t log a “before” state you can revert to with a button.
Why the Original 404 Happened in the First Place
Before even touching regenerate, the root cause of the original problem was almost certainly this: WordPress permalink/rewrite rule changes need the rewrite rules flushed to take effect. Clearing your cache plugin’s cache doesn’t do this. Go to Settings → Permalinks in wp-admin and click Save (no changes needed) — this forces a flush and is often the actual fix for a “changed the URL and now it 404s” situation, without ever needing to regenerate anything.
Where Permalink Manager Pro Actually Stores Your Custom URLs
This was the hardest part to pin down. According to Permalink Manager’s own documentation, instead of creating a database row per permalink, the plugin stores all custom permalinks as a single serialized PHP array in one row of the wp_options table (or your site’s equivalent options table, if you’re using a non-default prefix), under the option name:
permalink-manager-uris
The array maps each post/term ID to its custom permalink string, e.g.:
array(
10 => ‘custom-uri/for-a-post’,
12 => ‘another-custom-uri’,
‘tax-20’ => ‘custom-uri-for-a-term’,
)
Knowing this option name is the key that unlocks everything else — you don’t need to touch post meta or hunt for a dedicated custom table.
A quick note on table prefixes
If a search for permalink-manager-uris comes up empty in wp_options, check whether your database actually uses a different table prefix (e.g. wpss_options instead of wp_options). This can happen after certain migrations or hosting setups. Confirm the real prefix your site uses by checking the $table_prefix line in wp-config.php, or just search across all tables:
SHOW TABLES LIKE ‘%options%’;
The Recovery Process, Step by Step
1. Restore a pre-regenerate backup to a staging environment
Don’t restore directly to live. Spin up (or restore into) a staging copy of your site from a backup taken before you ran the regenerate tool. Most managed WordPress hosts (Kinsta, WP Engine, SiteGround, etc.) keep automatic daily snapshots — check there first.
2. Locate the option row on staging
SELECT option_name FROM wp_options WHERE option_name = ‘permalink-manager-uris’;
If that returns nothing, broaden the search:
SELECT option_name FROM wp_options WHERE option_value LIKE ‘%some-known-custom-slug%’;
Use an actual URL you know existed pre-regenerate as the search term — this is far more reliable than guessing option names.
3. Pull the full value from both staging and live
SELECT option_value FROM wp_options WHERE option_name = ‘permalink-manager-uris’;
Run this on both environments and save each result to its own text file (staging.txt and live.txt) — copy the value exactly, with no reformatting.
4. Don’t just overwrite live’s row with staging’s — merge them
This is the step that’s easy to get wrong. If any pages were published on live after your backup was taken, they won’t exist in the staging data at all — a straight overwrite would silently delete their custom permalinks too, even though the regenerate never touched them.
The right approach is a merge: for every post ID, use staging’s value (the correct, pre-regenerate URL) where it exists, but preserve any ID that’s only present in live (a newer page).
Here’s a small Python script to do this safely:
import phpserialize
with open(‘staging.txt’, ‘rb’) as f:
staging = phpserialize.loads(f.read())
with open(‘live.txt’, ‘rb’) as f:
live = phpserialize.loads(f.read())
merged = dict(live) # start with live’s current data
merged.update(staging) # overwrite with staging’s values for any ID present in both
print(f”Staging entries: {len(staging)}”)
print(f”Live entries: {len(live)}”)
print(f”Merged entries: {len(merged)}”)
print(f”Changed (restored): {sum(1 for k in staging if k in live and live[k] != staging[k])}”)
output = phpserialize.dumps(merged)
with open(‘merged.txt’, ‘wb’) as f:
f.write(output)
print(“Done — merged.txt is ready to use in the UPDATE statement.”)
Install the one dependency it needs first:
pip install phpserialize
macOS tip: if you hit an externally-managed-environment error installing packages, use a virtual environment instead of forcing the install system-wide:
python3 -m venv venv
source venv/bin/activate
pip install phpserialize
python3 merge.py
The “Changed (restored)” number this script prints is also your answer to “how many URLs actually got affected by the regenerate” — a genuinely useful number to have before you tell anyone (your SEO team, stakeholders, etc.) the scope of the incident.
5. Sanity-check a few known URLs before writing anything
import phpserialize
with open(‘merged.txt’, ‘rb’) as f:
merged = phpserialize.loads(f.read())
for pid in [3951, 5583]: # replace with IDs you know
print(pid, merged.get(pid, ‘NOT FOUND’))
Confirm the values match what you expect before moving to the write step.
6. Back up live’s current value first
Before writing anything, run the same SELECT on live one more time and save the output somewhere safe. This is your undo path if anything goes wrong.
7. Generate a properly-escaped SQL update, don’t hand-paste it
Serialized PHP strings are extremely sensitive to formatting — a single reformatted quote or line-wrap will corrupt the whole array and can white-screen your site. Generate the exact SQL with a script rather than typing it by hand:
with open(‘merged.txt’, ‘r’) as f:
merged_value = f.read().strip()
escaped = merged_value.replace(“‘”, “””)
sql = f”UPDATE wp_options SET option_value = ‘{escaped}’ WHERE option_name = ‘permalink-manager-uris’;”
with open(‘update.sql’, ‘w’) as f:
f.write(sql)
8. Run it on live, then flush and verify
Paste update.sql’s contents into your database tool’s SQL console (phpMyAdmin, Kinsta’s database manager, Adminer, etc.) and run it.
Then:
- Go to Settings → Permalinksin wp-admin and click Save to flush rewrite rules.
- Load a few of your known custom URLs directly in an incognito window to confirm they resolve.
- Spot-check several more from your “changed” list at random, not just the ones you were originally tracking.
Why Not Just Restore the Whole wp_options Table?
It’s tempting to just restore the entire table from backup instead of isolating one row, but this carries real risk:
- siteurland home live in this same table. If staging has a different domain saved in these fields, a full-table restore can point your live site’s core URL settings at the wrong domain — a much bigger outage than the one you started with.
- Any settings changes made by anyplugin between the backup and now (SEO settings, caching config, active plugins list, cron schedules) get silently wiped.
- There’s no easy partial undo if something in that huge blast radius breaks.
Isolating just the permalink-manager-uris row gets you the fix with essentially none of that risk.
Preventing This From Happening Again
Once you’ve restored your custom permalinks, protect the ones you care about:
- Open the permalink editor for that page in wp-admin.
- Look for the option to exclude it from the Regenerate/Reset tool(sometimes phrased as “Don’t auto-update Custom permalink”).
- Enable it for any page with a manually-shortened or SEO-important URL.
This one setting is what would have prevented the entire incident — worth doing for every custom URL you care about keeping.
Quick Reference Checklist
Flush permalinks (Settings → Permalinks → Save) before assuming anything is broken
Locate a pre-regenerate backup and restore it to staging, not live
Find the permalink-manager-uris row in wp_options (check for a different table prefix if not found)
Pull the value from both staging and live
Merge (don’t overwrite) — preserve any live-only IDs
Sanity-check known URLs in the merged result
Back up live’s current row before writing
Generate the UPDATE via script, not hand-typed SQL
Run it, flush permalinks again, verify URLs load
Mark important pages as excluded from future regenerate runs
