Migrating WordPress Permalinks Without Losing Rankings

Problem Statement

You need to change a WordPress site’s permalink structure — for example from /?p=123 or /2019/03/post-name/ to /category/post-name/ — without dropping rankings or stranding inbound links. WordPress will silently 404 every old URL the moment you save the new structure unless you generate a deterministic 301 map and serve it from a layer that fires before WordPress renders. This page is part of the broader CMS & Framework Routing Changes section.

WordPress permalink redirect flow Old dated permalink passes through a server-level 301 map to the new category permalink, bypassing the WordPress 404 handler. Permalink Change: Old URL to New Permalink Old dated URL 301 map (server) New permalink /2019/03/post/ .htaccess / Nginx /category/post/ Server rule fires before PHP, so WordPress never returns a 404
The server-level 301 map intercepts the old URL before WordPress can 404 it.

When to Use This Approach

  • You are changing the permalink structure on a live, indexed WordPress site.
  • Old URLs have backlinks, bookmarks, or organic traffic you cannot afford to lose.
  • You want the old-to-new mapping in version control, not buried in plugin tables.
  • You are deciding between the Redirection plugin (application layer) and server rules (.htaccess/Nginx).
  • You are moving WordPress behind a new domain or onto a different host at the same time.

Step-by-Step Instructions

1. Export the Current URL List Before Changing Anything

Capture every published URL under the old structure so you can map it deterministically. WP-CLI gives you a clean export without touching the database.

# Export post ID, status, and current permalink for all published posts and pages
wp post list --post_type=post,page --post_status=publish \
  --fields=ID,post_title,url --format=csv > old-urls.csv

Export from the database rather than by crawling, and export more than the URL. Take the post ID, the current permalink, the post type and the publication date — the ID is what lets you reconstruct the new URL deterministically once the structure changes, and the date is what most permalink structures are built from. A crawl-based export gives you the URLs and not the identifiers, which means the mapping afterwards has to be inferred from slugs rather than computed, and inference fails wherever two posts share a slug across different dates.

Verify the export is complete before proceeding, by comparing its row count against the published post count for each post type. This is the one irreversible step on the page: once the structure changes, the previous permalinks are no longer derivable for posts whose slug or date has since been edited.

2. Change the Permalink Structure and Flush Rules

Set the new structure once, then flush rewrite rules so WordPress writes the new internal routing. The --hard flag updates .htaccess on Apache.

# Apply new permalink structure and regenerate rewrite rules
wp rewrite structure '/%category%/%postname%/' --hard
wp rewrite flush --hard

3. Generate the 301 Map

Build an explicit old→new mapping. Because post IDs are stable, you can resolve each old URL to its new permalink and emit redirect lines.

# Emit "old_path new_path" pairs for each published post (IDs are stable across the change)
wp post list --post_type=post --post_status=publish --field=ID | \
  while read id; do
    printf '%s %s\n' "/?p=$id" "$(wp post url $id)"
  done > redirect-map.txt

Generate the map by computing new URLs from the exported identifiers rather than by pairing old and new crawls. Because the new permalink is a deterministic function of the post ID, date and slug under the structure you have chosen, the mapping can be derived exactly — no fuzzy matching, no near-miss pairings, and no ambiguity where two posts share a slug. Pairing two crawls, by contrast, requires guessing which new URL corresponds to which old one, and it guesses wrong precisely on the posts with duplicate or edited slugs.

4. Deploy the Map at the Server Layer

Server rules fire before PHP loads, so they are faster and survive plugin failures. On Apache, place rules in .htaccess above the WordPress block; on Nginx, add a map and a redirect.

# .htaccess — must sit ABOVE the "# BEGIN WordPress" block so it wins
Redirect 301 /2019/03/old-post-name/ /technical/old-post-name/
RedirectMatch 301 ^/\?p=123$ /technical/old-post-name/
# Nginx — map old paths to new, then redirect on a hit
map $request_uri $wp_redirect {
    /2019/03/old-post-name/  /technical/old-post-name/;
}
server {
    if ($wp_redirect) { return 301 $wp_redirect; }
}

5. Choose Plugin Rules Only Where Server Rules Cannot Reach

If you lack server access (managed WordPress hosts often block .htaccess edits), import the same map into the Redirection plugin. It works but adds a PHP round-trip per redirect, so reserve it for hosts where server rules are impossible.

# Export the Redirection plugin's rules so the map stays in version control
wp redirection export 1 json > redirection-rules.json

The order of operations here is unusually unforgiving, because the information you need to build the redirect map stops existing the moment you change the structure.

Order of operations for a permalink change Five steps in strict order — export the current URLs, change the structure, flush rules, generate the map, deploy it at the server — with the consequence of taking each one out of sequence. The old URLs stop being knowable once you change the structure Step If taken out of order 1. Export current URLs nothing to map from — the old paths are gone 2. Change the structure 3. Flush rewrite rules new permalinks 404 until a cache clears 4. Generate the 301 map mapping guessed rather than derived 5. Deploy at the server layer every request pays a database lookup Step 1 is the one with no recovery: WordPress does not retain the previous permalink for every post in a form you can export afterwards.
Export first, verify the export is complete, and only then touch the settings page.

Deploy at the server rather than in the application for the same reason as any bulk map: it costs nothing per request, it can be staged and tested before the structure changes, and it is visible in version control. WordPress installations accumulate redirect mechanisms — the built-in guessing, one or more plugins, sometimes rules in .htaccess from a previous migration — and consolidating the bulk map into one server-level file is what makes the estate reviewable at all.

Worked Example

Before: https://www.example.com/2019/03/migrating-dns/ returns HTTP/1.1 200. After changing the structure to /%category%/%postname%/, that URL would 404. With the .htaccess rule Redirect 301 /2019/03/migrating-dns/ /dns/migrating-dns/ in place:

$ curl -sIL https://www.example.com/2019/03/migrating-dns/
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/dns/migrating-dns/
HTTP/1.1 200 OK

The old dated URL now resolves in a single hop to the new category-based permalink, preserving the inbound link equity.

WordPress will handle some of this for you, and understanding exactly how much is what stops the platform and your server rules from fighting.

What WordPress handles itself versus what needs a server rule Built-in slug redirection, plugin-managed rules and server-level rules compared by coverage, cost per request, and whether the rule is visible in version control. The built-in handling is a backstop, not a plan Mechanism Covers Cost In version control? Built-in guessing some slug changes a DB query n/a Redirect plugin what you enter a DB query no — rules live in the DB Server rule map everything you map none yes Plugin rules are the common source of a hop nobody can find, because they exist in no file anyone reviews.
Use the server map as the primary mechanism and leave the built-in handling as a backstop for what the map misses.

Flush the rewrite rules explicitly after the change rather than waiting for it to happen. WordPress regenerates its internal rules lazily, so the new permalinks can return 404 for a period that depends on caching layers nobody is thinking about at that moment — and a 404 on the new structure during the window when you are testing the redirect map produces a confusing set of results. Flush, then confirm a new-format URL returns 200 before testing any redirect.

Verification

  • Confirm a single-hop 301 on a sample of old URLs: curl -sIL https://www.example.com/2019/03/migrating-dns/ | grep -iE '^HTTP|^location'.
  • Re-crawl the old URL list (old-urls.csv) and confirm zero 404s and zero chains longer than one hop.
  • In Google Search Console, watch the legacy URL pattern move from Indexed to Redirect over the following crawl cycles.

FAQ

Should I use the Redirection plugin or server rules? Use server rules (.htaccess or Nginx) when you have server access — they fire before PHP loads, so they are faster and keep working even if WordPress is down. Use the Redirection plugin only on managed hosts that block server-level redirect edits.

Will changing permalinks lose my rankings? Not if every old URL 301s to its exact new equivalent in a single hop. Rankings drop when old URLs 404, redirect to the homepage, or pass through a chain — generate a deterministic map and verify it before you announce the change.

Do I still need server rules if WordPress redirects old slugs itself? Yes, for anything beyond the simplest cases. The built-in handling covers some slug changes by guessing from the database, which is genuinely useful as a backstop and unreliable as a plan: it costs a query on every miss, it does not cover structural changes such as removing a date prefix, and its behaviour is not something you can inspect, version, or test. Generate a server-level map from the export, deploy that, and let the built-in guessing catch whatever falls through.

How do I handle a redirect plugin that already has rules in it? Export them first, then decide per rule. Plugin rules live in the database rather than in any repository, which makes them invisible to review and a frequent source of a hop nobody can locate — but some of them will be genuinely load-bearing, added years ago for reasons nobody remembers. Fold the ones that still receive traffic into the mapping inventory so they become server rules like everything else, and disable the plugin’s handling for paths the server now owns rather than leaving both active.

How long should the old permalink redirects stay in place? Indefinitely for anything with inbound links, which on a blog of any age is most of the archive. Dated permalinks in particular accumulate links from other sites, email newsletters and social posts that will never be updated, so the rules that serve them are doing real work years later. Retire individual rules only when the access logs show traffic to that source has genuinely decayed to nothing — which is a question the logs answer directly and a schedule cannot.

Related

← Back to CMS & Framework Routing Changes