Menu
All schedule 11 min read

Permalinks: Changing URL Structure Without Losing Rankings

Faraz Frank

Faraz Frank

November 4, 2026

Permalinks Changing URL Structure Without Losing Rankings

The Save Changes button on Settings → Permalinks is the most dangerous control in the WordPress admin. It has no confirmation step, no preview, and no warning that every URL on the site is about to change. Click it and WordPress writes the new rewrite rules immediately.

The URLs that used to work do not follow. A post at /hello-world/ becomes /blog/hello-world/, and the old address returns a 404 unless something redirects it. On a site with 400 published posts, that is 400 new 404s in a single afternoon.

This is not a reason to avoid permalinks forever. It is a reason to treat the change as a migration with a map, not as a setting you flip.

What WordPress Redirects For You, and What It Doesn’t

WordPress has a built-in redirect for one specific case: when a post’s slug changes. The function wp_old_slug_redirect() runs on a 404, looks for the current slug in the post’s stored past slugs, and sends a 301 to the new permalink. The function reference is explicit about the scope: it redirects old slugs to the correct permalink.

It does not cover a structure change. When you move from /%postname%/ to /blog/%postname%/, the slug is unchanged. Nothing writes an old-slug record, because from WordPress’s point of view the post never had a different name. The old path simply stops resolving.

Two more limits in the same function:

  • It only runs when the request is already a 404 and a name query variable exists. A URL that resolves to an archive or a page never reaches it.
  • It returns early for hierarchical post types. Pages do not get old-slug redirects.

That leaves the work to you: a server-side redirect for every old URL, pointing at the new one.

Google’s guidance on redirects is the reference for what to use. Permanent redirects, 301 or 308, are the signal that a URL has moved. Temporary redirects are for temporary moves and should not be used for a permalink change.

The site move documentation adds the operational rules: redirect to the final destination instead of chaining hops, test the redirects, keep them for as long as possible, and generally at least a year.

One thing you do not need: the Change of Address tool. Google’s documentation is direct about it. “You only need this tool when moving from one domain or subdomain to another… You don’t need it for HTTP to HTTPS moves, switching between www and non-www on the same domain, or moving paths within the same domain.” A permalink change is a path move. There is no Change of Address for it, and submitting one would be the wrong tool.

Pick the Structure Before You Build the Map

The structure you choose determines how many redirects you need now, and how many you need later. The options on the Permalinks settings screen are:

StructureExampleTrade-off
Plain/?p=123Works everywhere, tells readers nothing, and puts the ID in the query string
Day and name/2026/11/04/post-name/Good for news archives, noisy for everything else
Month and name/2026/11/post-name/Same problem with less precision
Post name/post-name/Short, readable, stable if you do not rename slugs
Custom/blog/%postname%/Same as post name, with a section prefix

For most sites, /%postname%/ or /blog/%postname%/ is the right answer. The choice between them is about separation, not ranking. A /blog/ prefix keeps posts out of the page namespace, which matters if you also publish landing pages with short slugs. Without it, a post and a page can end up with the same path, and the rewrite rules decide which one wins.

Three structures to avoid unless you have a reason:

  • Dates in the URL. They make an evergreen article look old in the search result, and they add two path segments that carry no information for the reader. Keep dates if the date is part of the content, such as a daily news archive.
  • %category% in the path. The URL changes whenever the post’s primary category changes. That turns an editorial decision into a redirect. It also creates a second place to maintain a slug. Keywords in the path are not what creates relevance; our guide to semantic SEO in WordPress covers where that comes from.
  • %post_id% as the only identifier. It survives slug edits, which is a real benefit for a site that renames often. It is also unreadable, and most sites do not rename enough to justify it.

Pick the structure you expect to keep for five years. Every later change costs another redirect map.

The GUID Is Not the URL

While you are in the database, leave the guid column alone. It is an internal identifier used in feeds, not the address WordPress resolves. Updating it to the new structure does not create a redirect, and changing it can confuse feed readers that use it to tell entries apart.

The permalink is generated from the post’s slug, date, and the structure setting. The GUID is a separate value. They are not supposed to match.

Export Every URL Before You Save

You cannot build a redirect map after the change from the old URLs, because WordPress only knows the new ones. Export first.

This WP-CLI command prints the post ID and the current permalink for every published post and page, one pair per line:

wp eval 'foreach (get_posts(["post_type" => ["post", "page"], "numberposts" => -1, "post_status" => "publish"]) as $p) { echo $p->ID, ",", get_permalink($p), "\n"; }' > before.csv

On a site with tens of thousands of posts, run the loop in batches rather than one unbounded query; the list has to be complete, so do not sample. Run it on production before touching the setting. Then apply the new structure on staging, or on production after a backup, and run the same command again:

wp eval 'foreach (get_posts(["post_type" => ["post", "page"], "numberposts" => -1, "post_status" => "publish"]) as $p) { echo $p->ID, ",", get_permalink($p), "\n"; }' > after.csv

Joining on the post ID is what makes this reliable. You do not need to parse dates, categories, or prefixes out of the old path. The ID is the same on both sides, and get_permalink() returns whatever the current structure produces.

If you do not have WP-CLI, the same pair can be produced with a small PHP file that boots WordPress and loops over get_posts(). The important part is the export, not the tool.

Generate the Redirect Map by Joining on Post ID

This script reads both exports, matches rows by ID, skips URLs that did not change, and writes Apache RedirectMatch rules:

import re

def load(path):
    rows = {}
    with open(path, encoding="utf-8") as handle:
        for line in handle:
            line = line.strip()
            if not line or "," not in line:
                continue
            post_id, url = line.split(",", 1)
            rows[post_id] = url
    return rows

def path_of(url):
    match = re.match(r"https?://[^/]+(/.*)?$", url)
    return match.group(1) or "/"

def escape_path(path):
    return re.sub(r"([.^$*+?()\[\]{}|\\])", r"\\\1", path)

before = load("before.csv")
after = load("after.csv")

rules = []
pairs = []
skipped = 0

for post_id, old in before.items():
    new = after.get(post_id)
    if not new or new == old:
        continue

    old_path = path_of(old)
    new_path = path_of(new)

    if "?" in old_path:
        skipped += 1
        continue

    rules.append(f"RedirectMatch 301 ^{escape_path(old_path)}$ {new_path}")
    pairs.append((old, new))

with open("redirects.conf", "w", encoding="utf-8") as handle:
    handle.write("\n".join(rules) + "\n")

with open("pairs.csv", "w", encoding="utf-8") as handle:
    for old, new in pairs:
        handle.write(f"{old},{new}\n")

print(f"{len(rules)} redirects, {skipped} skipped")

Two details in that script matter more than the rest.

The query-string skip. If the old structure was Plain, the old URLs look like /?p=123. The path is / and the identifier is in the query string, which RedirectMatch cannot see. Those URLs do not need a rule anyway: WordPress resolves ?p=123 to the post and redirect_canonical() sends a 301 to the new pretty permalink. Skipping them avoids writing a rule that would match every URL on the site.

Escaping the old path. The rule is anchored with ^ and $, so it matches the exact old path and not a longer path that starts with the same characters. The escape_path() helper escapes the characters that matter in a regex, so a slug containing a dot is matched literally rather than as a wildcard.

The output looks like this:

RedirectMatch 301 ^/hello-world/$ /blog/hello-world/
RedirectMatch 301 ^/2024/05/second-post/$ /blog/second-post/

Paste the rules into .htaccess above the # BEGIN WordPress block. RedirectMatch comes from mod_alias and does not need the WordPress rewrite block. If your host runs nginx, the equivalent is a location = /old/ { return 301 /new/; } block per URL, or a map file at the server level.

The Cutover, in Order

The order matters because each step depends on the previous one.

  1. Back up the database and files. A permalink change is reversible, but only if you can restore the settings and the redirect rules together.
  2. Export before.csv. Do this before the change, from the live site.
  3. Apply the new structure on staging. Run the export there, generate the map, and paste the rules into the staging .htaccess.
  4. Test a sample on staging. Pick the oldest post, a post with a date-based URL, a page, and a category archive. Each old URL should return one 301 to the new URL, and the new URL should return 200.
  5. Apply on production. Use the settings screen or WP-CLI:
wp rewrite structure '/blog/%postname%/' --hard
wp rewrite flush --hard

The --hard flag rewrites .htaccess as well as the rules in the database. WP-CLI only does that if mod_rewrite is listed under apache_modules in wp-cli.yml; without it, the command updates the database and leaves the file alone. The settings screen has the same dependency: if .htaccess is not writable, WordPress shows the rules and asks you to paste them in yourself.

  1. Purge the page cache and the CDN. Cached HTML still contains the old canonical tags and the old internal links. A purge is not optional after a URL change.
  2. Resubmit the sitemap. The SEO plugin generates it from get_permalink(), so it should already list the new URLs. Submit it anyway to speed up discovery. If the plugin’s canonical or sitemap settings are unfamiliar, the free SEO plugin roundup covers where each one keeps those controls.

Internal Links Are Half the Migration

Redirects catch external links and bookmarks. Internal links should point at the new URL directly, because a chain of “link → 301 → destination” wastes a request and tells Google the old URL is still the canonical path.

Start with the navigation and the posts that link to the old URLs. The safest command is per URL, from the pairs file, with a dry run first:

wp search-replace 'https://wpfrank.com/hello-world/' 'https://wpfrank.com/blog/hello-world/' --dry-run

Read the table counts, then run it again without --dry-run. WP-CLI handles serialized data, which a raw SQL UPDATE does not.

Do not run a single global replacement of the origin for a prefix change. wp search-replace 'https://wpfrank.com/' 'https://wpfrank.com/blog/' would also rewrite URLs that already start with /blog/, producing /blog/blog/, and it would touch media and plugin URLs that should not move.

For a site with hundreds of changed URLs, a redirect plugin that stores rules in the database is easier to audit than a thousand lines in .htaccess. .htaccess is parsed on every request; a long file adds latency to every page, including the ones that do not redirect. Past a few hundred rules, move the map to the server config or a plugin. The broken link guide covers the follow-up pass: finding references the map missed after the redirects are live.

Verify Every Hop, Not a Sample

A spot check of three URLs is how migrations ship with 40 broken pages. Loop the whole pairs file.

while IFS=, read -r old new; do
  code=$(curl -s -o /dev/null -w '%{http_code}' "$old")
  location=$(curl -s -o /dev/null -w '%{redirect_url}' "$old")
  final=$(curl -s -o /dev/null -w '%{http_code}' "$new")
  printf '%s -> %s [%s] final=%s\n' "$old" "$location" "$code" "$final"
done < pairs.csv

What you want on every line:

  • 301 for the old URL, not 302.
  • A location that matches the new URL exactly, with the trailing slash you chose.
  • 200 for the destination.
  • No second redirect. If location points at a URL that redirects again, the map has a chain; fix it before Google finds it.

Then check Search Console after a few days:

  • Page indexing. Old URLs should move to “Page with redirect.” A 404 means a missing rule.
  • Sitemap. The new URLs should be listed; the old sitemap URL may 404, which is fine.
  • Performance. Clicks on the old URLs should consolidate into the new ones. A short dip is normal while Google recrawls. If the decline lasts longer than the recrawl, the ranking guide covers the other variables worth checking before you blame the permalinks.

Keep the rules for at least a year. Google’s site move guidance says “as long as possible,” and a year is the shorter end of that. Old links live in forums, newsletters, and other people’s bookmarks far longer than most site owners expect.

When Not to Change

A permalink change is a migration. If the current URLs work, are indexed, and have earned links, the default should be to leave them alone.

Change the structure when:

  • The current URLs contain dates that misrepresent evergreen content.
  • The path includes a category that changes often.
  • The site is new, with little indexed traffic and few external links, and the current structure will be expensive to keep.
  • A planned section, such as a blog or a store, needs a namespace it does not currently have.

Do not change it because a checklist says “post name is best.” URL structure is a usability and maintenance decision. The ranking risk is in the migration, not in the words in the path. Our SEO basics for developers separate the structural choices from the signals that actually move pages. Our guide to structuring WordPress pages for SEO covers how to assign one job per URL; permalinks are the mechanical layer underneath that plan.

FAQ

Does WordPress automatically redirect old permalinks after a structure change?

No. wp_old_slug_redirect() handles changes to a post’s slug, not to the site’s permalink structure. When the structure changes, the old path has no stored old-slug record, so it returns a 404 unless you add a redirect.

Will changing permalinks hurt my rankings?

Not automatically, but the migration can. Permanent redirects pass signals to the new URL, and Google’s documentation says to keep them for at least a year. The risk is missing redirects, redirect chains, and internal links that still point at the old paths.

Does the Change of Address tool apply to a permalink change?

No. Google says the tool is only for moves between domains or subdomains. It explicitly excludes path changes within the same domain, along with HTTP to HTTPS and www to non-www moves.

Should posts live under /blog/ or at the site root?

Either works. /blog/ separates posts from pages and avoids collisions between a post and a page with the same slug. /%postname%/ is shorter. Pick one and keep it; the difference is structural, not a ranking factor.

What happens to pages when I change the post permalink structure?

Pages use their own hierarchical paths and are not rewritten by the post structure setting. A page at /about/ stays at /about/. The category and tag base fields are separate settings and change archive URLs, not page URLs.

Do I need to update the GUID in the database?

No. The GUID is an internal identifier used in feeds, not the permalink. Changing it does not create a redirect and can confuse feed readers. Leave it as it is.

How many redirects can .htaccess handle?

There is no hard limit, but .htaccess is parsed on every request. A few dozen rules are fine. Hundreds add measurable overhead and become hard to audit, so move the map to a plugin that stores rules in the database or to the server configuration.

Do redirects expire?

They stay until you remove them. Keep them for at least a year, and longer if the old URLs still receive external links. There is no penalty for keeping a permanent redirect in place.

Share: tw in
Faraz Frank

About Faraz Frank

Author at WP Frank. Writing about WordPress development, design, and best practices.

View all posts by Faraz Frank →
edit_note

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.