If you change a WordPress permalink structure, every old URL stops working unless something answers it with a 301 pointing at the new one. WordPress will not do that for you in the general case: it redirects a few things it can work out for itself, such as a changed slug or a ?p=123 address, and returns a 404 for the rest. The safe sequence is short. Export the list of current URLs, change the structure, add one rule that maps the old pattern to the new, test with curl -I, and only then trust it. The rest of this post covers the status codes, the rules for Apache-style .htaccess and for nginx, and the handful of causes behind nearly every redirect loop.
What a 301 is, and when to use something else#
A redirect is a normal HTTP response with a status code and a Location header. The browser or crawler reads the header and requests the new address. The status code tells it how long to remember the answer.
| Code | Meaning | Use it for |
|---|---|---|
301 | Moved permanently | A URL that has a new home for good |
302 | Found (temporary) | Testing a rule, short-lived campaigns |
307 | Temporary, keeps the method | A POST that must stay a POST |
308 | Permanent, keeps the method | The strict twin of 301 |
For permalinks, 301 is the answer. It tells search engines to move the old URL's standing to the new one, and browsers cache it. The caching is the dangerous half: a 301 you got wrong stays in visitors' browsers long after you fix the server, and you cannot reach those browsers. So write every new rule as 302 first, check it, and change one word to 301 when it is right. Redirecting between www and the apex domain makes the same point about host redirects, and everything below assumes you have read how that layer is meant to work.
A 301 is also not a permanent lease on the old address. Keep the rule for at least a year, and ideally for as long as the site exists. Old links on other sites and in other people's bookmarks do not expire when your search index catches up.
What WordPress redirects for you, and what it does not#
WordPress has two built-in behaviours that look like redirects, and it is worth knowing where they stop.
The first is canonical redirection (redirect_canonical()). If someone requests /?p=123, and post 123 has a pretty permalink, WordPress sends them to the pretty one with a 301. It also adds or removes the trailing slash to match your structure, and it redirects to the host in your home option. That is why the question of which layer owns redirects matters, and why two layers fighting is how loops start.
The second is the old-slug mechanism. When you change a post's slug in the editor, WordPress remembers the previous slug in post meta (_wp_old_slug) and redirects it. It also has a fallback for 404s that guesses a post from the last part of the URL, so /2023/04/my-post/ can sometimes end up at /my-post/ with no rule from you.
Do not build a migration on either of them. The guess only works when the slug is unique, only for posts, and only when WordPress decides to try. It does nothing for category bases, page hierarchy changes, custom post types, attachment URLs, or anything where the slug itself changed. Treat it as a bonus for the odd URL you forgot, not as the plan.
Before you touch the permalink setting#
Permalinks are in Settings, then Permalinks. The choices are Plain (/?p=123), Day and name, Month and name, Numeric, Post name, and Custom Structure. They are all built from tags such as %year%, %monthnum%, %postname% and %category%. Post name (/%postname%/) is what most sites end up wanting: short, readable, and free of dates that make a good article look old.
Changing the setting is instant and applies to the whole site. The old URLs are not renamed, they simply stop being generated, so do the preparation first.
- Take a backup. Files and database. A permalink change itself touches few things, but you will be editing
.htaccessor the server configuration next, and backups that actually restore is the standard to hold the copy to. - Write down every current URL. This is the list you will test against afterwards. With WP-CLI:
$ wp post list --post_type=post,page --post_status=publish \ --fields=ID,post_name,url --format=csv > old-urls.csv$ wc -l old-urls.csv- Check the current structure so you know exactly what you are replacing:
$ wp option get permalink_structure/%year%/%monthnum%/%postname%/- Check which search results matter. Open Search Console or whatever analytics you use and note the ten or twenty URLs that bring in traffic. Those are the ones to test by hand.
If you do not have shell access, which is the normal situation on panel-based hosting, skip WP-CLI and export the list from a plugin or copy it from the sitemap. The principle is the same: have the old list in a file before the change, because afterwards you can no longer ask WordPress what the old URLs were.
Changing the structure#
Make the change in Settings, then Permalinks, choose the new structure and save. With WP-CLI the equivalent is:
$ wp rewrite structure '/%postname%/'$ wp rewrite flush$ wp rewrite list | headSaving the page is what regenerates the rewrite rules. On a server that reads .htaccess, it also rewrites the WordPress block in that file, if the file is writable. If it is not writable, WordPress shows you the block to paste yourself.
One caution: if you also have a custom post type or a WooCommerce shop, check their base settings separately: they have their own slugs, and your sitewide rule must not swallow them.
The rule that maps old to new#
The goal is one rule that captures the part you want to keep and discards the part you do not. For a move from /2024/05/my-post/ to /my-post/, the pattern is "four digits, slash, two digits, slash, a slug".
On Apache-style servers, the redirect goes in .htaccess, above the # BEGIN WordPress block, because WordPress's own rule sends everything that is not a file to index.php and would otherwise get there first:
<IfModule mod_rewrite.c>RewriteEngine OnRewriteRule ^[0-9]{4}/[0-9]{2}/([^/]+)/?$ /$1/ [R=302,L]</IfModule># BEGIN WordPress<IfModule mod_rewrite.c>RewriteEngine OnRewriteBase /RewriteRule ^index\.php$ - [L]RewriteCond %{REQUEST_FILENAME} !-fRewriteCond %{REQUEST_FILENAME} !-dRewriteRule . /index.php [L]</IfModule># END WordPressThe leading slash is absent from the pattern on purpose: in .htaccess, the path the rule sees has the directory prefix stripped. In the server's main configuration the same rule needs ^/ at the start. Keep this rule in RewriteRule form. Mixing Redirect or RedirectMatch (a different Apache module) with RewriteRule in one file gives results that depend on module ordering and surprise people.
On nginx, the same job is a location with a regular expression. The curly braces force the pattern into quotes:
location ~ "^/[0-9]{4}/[0-9]{2}/([^/]+)/?$" { return 302 /$1/;}Place it in the server block, next to the existing location / that holds try_files $uri $uri/ /index.php?$args;. Regex locations are checked before the plain prefix match, so it wins. After checking, change 302 to 301; with nginx, rewrite ... permanent; does the same thing as return 301, and return is the simpler and preferred form.
Neither rule is complete as written, and the gaps are worth knowing:
- Date archives are not posts.
/2024/05/has no slug and does not match, which is correct. But/2024/05/feed/has the shape of a post and would be redirected to/feed/. Add an exclusion forfeed,pageand any other reserved words your site uses, or list them first and stop processing. - Pages with parents. A page at
/services/hosting/is not in the date pattern, so this rule never touches it, and if you change only the post structure it keeps working. - Category bases. Moving
/category/news/to/news/is a separate rule, and one that is easy to get wrong because it clashes with page slugs. Check for conflicts before you do it. - Query strings. Both rules above drop the query string on the redirected request, which is almost always what you want for a permalink move. If you rely on
?utm_parameters surviving, say so explicitly:[R=301,L,QSA]in Apache; nginx'sreturndoes not append the query unless you add$is_args$args.
Many individual URLs: maps and the Redirection plugin#
A pattern works when the change is systematic. It does not work when slugs themselves changed, or when you merged twenty old articles into five. That needs an explicit list, one old path and one new path per line.
For a list of a few dozen, plain rules are fine. For hundreds, nginx's map is the clean answer: it lives in the http block and is looked up in constant time.
map $request_uri $moved_to { default ""; /old-hosting-guide/ /guides/hosting/; /2023/11/vps-vs-shared/ /vps-vs-shared-hosting/;}server { # inside the existing server block if ($moved_to) { return 301 $moved_to; }}if is restricted in nginx and this is one of the few uses that is safe, since it contains only a return. On Apache there is RewriteMap, but it is only permitted in the main server configuration, never in .htaccess. On shared-style hosting you will not have access to that, so the practical choices there are many RewriteRule lines or a plugin.
The most common plugin is Redirection, which stores the list in the database, logs 404s so you can find the URLs you forgot, and can import a CSV. It runs inside PHP, so each redirected request costs a WordPress boot, where a server rule answers in microseconds. For a few thousand redirects that is acceptable; for a high-traffic site, use the plugin to find the URLs and then move the rules to the server. WordPress speed and caching covers why a full WordPress boot per request is the expensive part.
.htaccess or nginx: which one are you on?#
This matters because the two work differently, and the web server your host runs is not something to assume.
- Apache reads
.htaccesson every request, in every directory it passes through. It is slower than a central config, but it means you can change rules without a restart, and that a plugin can edit them. It only works if the server allows it (AllowOverride), which most hosting does. - LiteSpeed and OpenLiteSpeed read
.htaccesstoo, with compatible rewrite syntax for the common directives. - nginx has no
.htaccess. Rules live in the server configuration, and changes need a reload. A file called.htaccesssitting in a WordPress folder on an nginx server is read by nothing at all, which is the classic reason "my redirect does nothing".
To find out, look at the response headers:
$ curl -sI https://example.com/ | grep -i '^server'server: nginxThat header can be hidden or changed, so a missing or generic value is not an answer. The other tell is the file manager: if the site root has an .htaccess that WordPress rewrote when you saved permalinks, something is reading it. If saving permalinks produces a message that WordPress could not update .htaccess, you are either on a server that ignores it or on a read-only file.
RE:NODE's panel does not tell you here, and this post will not guess. Look at the actual Server header of your own site and at what is in the root of your files in the file manager, and follow the section that matches. On a host where the web server configuration is not yours to edit, the answer is the .htaccess route if the server reads it, the Redirection plugin if it does not, and a support ticket if you need a server-level rule you cannot write yourself.
Redirect loops: the causes, in order of likelihood#
ERR_TOO_MANY_REDIRECTS in Chrome, "The page isn't redirecting properly" in Firefox. A loop means two things disagree about what the canonical URL is, and each sends the visitor to the other's version. Find the disagreement and remove one side.
First, see the chain instead of guessing. curl follows nothing by default, so tell it to, and cap the hops:
$ curl -sIL --max-redirs 8 https://example.com/some-post/ \ | grep -i -E '^(HTTP|location)'Read the Location lines in order. A loop shows the same two addresses alternating. Then match what you see against these.
HTTPS behind a proxy. The most common cause on modern hosting. TLS ends at a reverse proxy, which passes plain HTTP to the web server. A rule or plugin sees http://, redirects to https://, the proxy strips TLS again, and the cycle repeats. The fix is to have WordPress trust the header the proxy sets, which in wp-config.php looks like this, placed above the line that says to stop editing:
if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) { $_SERVER['HTTPS'] = 'on';}Confirm your proxy actually sends X-Forwarded-Proto before relying on it; on RE:NODE the proxy slot is documented as passing the visitor's address in X-Forwarded-For, so verify the protocol header rather than assuming it. What a reverse proxy does explains these headers, and HTTPS and Let's Encrypt explained covers where the certificate sits.
The site URL disagrees with the rule. siteurl and home in the database say http://example.com, while a server rule forces https://www.example.com. WordPress redirects one way and the server the other. Look at what is stored:
$ wp option get home$ wp option get siteurlIf you cannot reach the dashboard, pin both in wp-config.php, which overrides the database values until you fix them properly:
define( 'WP_HOME', 'https://example.com' );define( 'WP_SITEURL', 'https://example.com' );The trailing slash fight. WordPress adds a trailing slash to pretty permalinks. A server rule that strips it, or a plugin that does, sends /post/ to /post and WordPress sends it straight back. Pick the slash. WordPress's own convention is to keep it, so remove the stripping rule.
Two redirect layers. A security plugin, a caching plugin, a redirect plugin, the .htaccess file and the host's proxy can each enforce HTTPS or the canonical host. They are individually correct and collectively a loop. Disable them one at a time from the outside: rename wp-content/plugins/the-plugin through the file manager, or run wp plugin deactivate --all and re-enable until the loop returns.
A rule that matches its own target. The simplest loop of all, and it appears in hand-written rules: a loose pattern such as ^(.*)$ that redirects every path to a slightly different path will match its own output and run again. Anchor patterns tightly, and make sure the output cannot match the input. With the date rule above, the target /my-post/ has no digits, so it cannot match again.
A cached redirect. After you fix the cause, the browser keeps replaying the cached 301. Test in a private window, or with curl, which has no cache. If you are testing for real visitors, remember that a CDN or page cache in front of the site may have cached the redirect too, so purge it. A blank page or a 500 after editing .htaccess is a different problem, and white screens and 500 errors in WordPress walks through it.
Testing every redirect#
Test with curl -I, never by clicking, because curl shows the status code and the destination without any browser cache or auto-correction. Run it over the list you saved before the change:
$ while read -r path; do printf '%s -> ' "$path" curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \ "https://example.com$path" done < old-paths.txtFor each old URL you want one 301 (or 302 while testing) to the new URL, and then a 200 at the destination. Anything that shows two hops is worth fixing: the first redirect sends visitors to a URL that itself redirects, which wastes time and dilutes the signal. Anything that shows 404 is a URL your rule missed. Anything that shows a 301 to the home page is a lazy rule that throws away the link's meaning; send each old page to its true successor.
When the output is clean, switch 302 to 301, reload the server where needed, and run the list once more.
Afterwards: links, sitemap and search#
Redirects handle people arriving from outside. Everything you control should stop relying on them.
- Update internal links. Posts that link to each other with full old URLs now take a redirect on every click. Preview first:
wp search-replace 'example.com/2024/05/' 'example.com/' --dry-run, read the count, and run it without the flag only after a backup. Pattern replacement across serialized data should always go through WP-CLI, not through a raw SQLUPDATE. Migrating WordPress to a new host covers why. - Regenerate the sitemap so it lists only the new URLs, and resubmit it in Search Console.
- Watch the 404s. For a few weeks, look at the web server log or your 404 monitor for old URLs you missed.
- Expect a dip. Search engines take days to weeks to process a move of this kind. A temporary wobble that settles is normal; a collapse that does not recover is the sign of a rule that redirects to the wrong place.
On RE:NODE, a WordPress site on the web line has the same file manager and SFTP as every other plan, which is all you need to edit .htaccess or wp-config.php and to copy a backup before you do.
FAQ#
Will changing permalinks hurt my search rankings?
Not if every old URL answers with a single 301 to its direct replacement. Expect a short wobble while crawlers reprocess the site. Rankings are lost when old URLs 404, redirect to the home page, or go through several hops.
Should I use 301 or 302 for a permalink change?
302 while you are testing, because a wrong 301 is cached in browsers for a long time, then 301 for the final rule. A permalink change is permanent, so the end state is always 301.
Why does my .htaccess redirect do nothing?
Usually one of three things: the server is nginx and does not read .htaccess, AllowOverride is off, or the rule sits below the WordPress block and never runs. Check the Server header with curl -I, and move the rule above # BEGIN WordPress.
Can I redirect without a plugin?
Yes, and for a systematic pattern you should. A server rule answers before PHP starts. Plugins make sense when you have hundreds of one-off URLs, or want a log of 404s to work from.
How long should I keep the redirects?
At least a year, and ideally indefinitely. They cost almost nothing to keep, and external links and bookmarks to the old URLs do not disappear when search engines update.
What does ERR_TOO_MANY_REDIRECTS mean?
The browser followed more redirects than it will accept, usually about twenty, because two rules send the visitor back and forth. Run curl -sIL --max-redirs 8 on the URL, read the Location lines, and remove one side of the disagreement.


