Every rebuild, permalink change and content tidy-up leaves URLs behind. Someone has bookmarked them, other sites link to them, and Google has them indexed. A redirect tells all three where the page went, without losing the traffic or the links pointing at it.

Most of the damage I see comes from two habits. Redirecting every dead URL to the homepage, which looks tidy and quietly destroys the value of those links. And stacking one redirect on another over the years until a single old URL takes four hops before it lands anywhere.

The work is mostly a decision and a list. You decide what each old URL should now point at, write it down, then put the list somewhere the site will read it. There are four places it can go. The first is a plugin with an ordinary WordPress screen, which is what I would use on most sites; the other three involve server files.

Take those routes as alternatives, not steps. Read Step 1 and Step 2, pick one route, then come back for the testing section at the end. That last part matters, because your own browser will lie to you about whether a redirect works.

Before you start

You need WordPress admin for the plugin route. For the .htaccess route you need SFTP (a way of connecting to your server’s files with a program such as FileZilla) or your hosting panel’s File Manager, which is easier if SFTP is new to you. For nginx you need server access or a support ticket, because that file sits outside your WordPress install.

Apache and nginx are the two web servers almost every host runs, and they take their settings in different places. Apache reads .htaccess; nginx ignores it completely. If you do not know which you are on, ask your host. cPanel hosting is usually Apache.

Back up .htaccess before editing it, and this is the one genuinely risky step here. A malformed line returns a 500 error for the whole site, wp-admin included, and the fastest way back is putting the old file in place. Keep the copy on your machine, not just on the server.

If you are mid-migration, export a list of your current live URLs first. Google Search Console’s Pages report gives you one, as does your XML sitemap (at /wp-sitemap.xml in WordPress itself, or /sitemap_index.xml if you use Yoast), or a crawl with a tool like Screaming Frog. You cannot redirect what you did not write down, and once the new site is live the old URLs are much harder to recover.

Step 1: Pick the right status code

A redirect is a status code plus a destination. Every time a browser asks for a page, the server answers with a three-digit number saying how it went: 200 means “here it is”, 404 means “no such page”. The 300s mean “look somewhere else”, and the exact number tells browsers and search engines how permanent the move is. Picking the wrong one has real consequences.

301 Moved Permanently. The page has moved and is not coming back. Search engines transfer the ranking signals to the destination and eventually drop the old URL from the index. Browsers cache it hard, sometimes indefinitely. This is what you want for a rebuild, a permalink change or a merged page.

302 Found. Temporary. The old URL stays indexed because you are telling Google it will be back. Correct for a page under maintenance, a seasonal offer, or A/B testing. Wrong for a migration, and it is the default in a lot of hastily written code, which is how sites end up with hundreds of accidental 302s.

307 Temporary Redirect. Behaves like a 302 with one guarantee added: a 302 can quietly turn a form submission into an ordinary page request and lose what the visitor typed, and a 307 cannot. It matters for forms and APIs. Its permanent counterpart is 308, which I would not reach for on ordinary pages without a reason.

410 Gone. Not a redirect. It says the content was deliberately removed and has no replacement. Use it for a discontinued product line, an old event, a batch of thin posts. Google treats 410 as a stronger signal than 404 and drops the URL faster, which is what you want for content you meant to remove.

The decision is simpler than it looks. If there is a genuine equivalent page, 301 to it. If there is not, let the URL return 410 or a plain 404.

Step 2: Map old URLs to new ones before writing any rules

Put the mapping in a spreadsheet: old path, new path, status code. A path is the part of the URL after the domain, so /blog/some-post/ rather than https://example.com/blog/some-post/. Using paths rather than full URLs makes patterns easy to spot.

Patterns are what save you. If half the list is /blog/2019/05/some-post/ becoming /blog/some-post/, that is one rule rather than three hundred. Sort by old path and the shape of the job becomes obvious.

Mark the rows with no real equivalent while you are there. Those are your 410s, and resisting the urge to point them somewhere is the most useful decision in this whole job.

Route 1: A redirect plugin, for the everyday case

For most sites this is what I would use, and I say that as someone perfectly happy editing server config.

The Redirection plugin is the usual choice. Install it and you get a Tools, Redirection screen where you paste an old path and a new one and press Add. It stores the rules in the database, logs 404s so you can see which missing URLs are actually being requested, and exports the lot. Rank Math has an equivalent redirect manager in its free version and Yoast has one in Premium, so use that instead if you already run either.

The 404 logging is the real argument for it. Set up your mapping, come back in a fortnight, and look at what is still hitting 404s. That list is always different from the one you predicted, because it contains the URLs real people and real backlinks are using.

The trade-off is honest. A plugin redirect happens inside PHP after WordPress has loaded, so it is slower than a server rule and will not fire for requests that never reach WordPress. For a handful of redirects that is irrelevant. For several thousand, move the bulk to the server. And never install two redirect plugins, because working out which one produced a given hop is miserable.

Route 2: .htaccess, on Apache

Server rules are the fastest option, because Apache answers before WordPress starts at all. .htaccess is a small settings file Apache reads on every request, and yours sits in the WordPress root alongside wp-admin and wp-content. The leading dot makes it hidden, so your File Manager may need “show hidden files” turned on.

Rules go above the # BEGIN WordPress line, never between that and # END WordPress, because WordPress rewrites everything between its own markers when you save permalinks and anything there disappears.

Take that copy of the file now if you have not. A single URL:

# Simple prefix match: this also catches /old-page/anything/ below it.
Redirect 301 /old-page/ https://example.com/new-page/

# Exact match only, using a regex anchored at both ends.
RedirectMatch 301 ^/old-page/?$ https://example.com/new-page/

The difference between those two catches people out. Redirect matches a prefix, meaning any URL starting with what you typed, so /old-page/ also matches /old-page/child-item/ and sends it to the wrong place. RedirectMatch takes a pattern instead, written as a regular expression, where ^ means the start of the path and $ the end. Together they mean “this exactly, nothing longer”. When in doubt use RedirectMatch with both.

A whole folder, keeping everything below it:

# $1 carries the rest of the path through, so
# /old-folder/thing/ lands on /new-folder/thing/
RedirectMatch 301 ^/old-folder/(.*)$ https://example.com/new-folder/$1

A query string is the part of a URL after a question mark, as in /?p=123. Redirect passes it through to the destination untouched but gives you no way to match on it, so matching one needs Apache’s rewrite engine, mod_rewrite, which is a more capable and more fiddly set of instructions:

RewriteEngine On
# Matches /?p=123 and sends it to the new URL.
# The trailing ? on the target drops the original query string.
RewriteCond %{QUERY_STRING} (^|&)p=123($|&)
RewriteRule ^$ /new-page/? [R=301,L]

[R=301,L] means “redirect with a 301” and “stop processing rules here”. Leave off the R=301 and Apache rewrites internally without telling the browser, which is a different thing entirely.

Route 3: nginx

nginx does not read .htaccess. Rules you put there are ignored silently, with no error to say why nothing is happening, which is a fun afternoon for anyone who has not been told. nginx keeps its settings in its own configuration file, in a section per site called a server block, and changes there do nothing until nginx reloads. On managed hosting only your host can do either.

A single URL, and a folder:

# Exact match, no regex, fastest form nginx offers.
location = /old-page/ {
    return 301 https://example.com/new-page/;
}

# Folder move, carrying the remaining path through as $1.
rewrite ^/old-folder/(.*)$ /new-folder/$1 permanent;

permanent is nginx’s word for 301. Its counterpart, redirect, gives you a 302.

This is server config you cannot reach from inside WordPress, in the same way the maximum upload file size is set outside WordPress on nginx. On managed hosting that means raising a ticket, so for a handful of redirects the plugin route is usually less friction.

Route 4: PHP in a small site-specific plugin

This route is for developers, and skip it happily if you are not one. When the mapping is genuinely pattern-based and you want it in version control with the rest of the site, do it in PHP. It belongs in a small site-specific plugin, meaning one written for this site alone, not in functions.php, because a theme update wipes functions.php and takes your redirects with it. Bespoke logic like this is exactly the kind of thing I build as a custom plugin so it survives everything else.

Create /wp-content/plugins/site-redirects/site-redirects.php:

<?php
/**
 * Plugin Name: Site Redirects
 * Description: Pattern-based 301 redirects for legacy URLs.
 */

// Bail if loaded directly rather than through WordPress.
defined( 'ABSPATH' ) || exit;

add_action( 'template_redirect', function () {

    // Only act when WordPress found nothing. This keeps real pages fast
    // and makes it impossible to redirect a URL that still works.
    if ( ! is_404() ) {
        return;
    }

    $path = wp_parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH );
    $path = untrailingslashit( $path );

    // Old dated permalinks: /blog/2019/05/some-post -> /blog/some-post
    if ( preg_match( '#^/blog/\d{4}/\d{2}/(.+)$#', $path, $m ) ) {
        wp_safe_redirect( home_url( '/blog/' . $m[1] . '/' ), 301 );
        exit; // Always exit, or WordPress carries on rendering the 404.
    }

    // Retired section with no equivalent: tell crawlers it is gone.
    if ( str_starts_with( $path, '/old-section' ) ) {
        status_header( 410 );
        exit;
    }
} );

Three things worth understanding. A hook is a named moment during a page load that WordPress lets your code join in at, and template_redirect is the moment just before a page is rendered. Hooking there and checking is_404() first means the rules only run once WordPress has failed to find a matching page, so you can never accidentally redirect a page that still exists. wp_safe_redirect() refuses destinations on other hosts as a deliberate safety net, so use wp_redirect() if you really are sending traffic to another domain. And the exit is not optional, because without it WordPress carries on and sends page output after the header.

If it misbehaves, deactivate the plugin, or rename its folder over SFTP if a loop has locked you out of wp-admin.

Moving a domain, with www and https in one hop

A domain change is where chains breed. Done carelessly you get http://www.old to http://old to https://old to https://new, which is four requests to serve one page.

Handle host, protocol and domain in a single rule. On Apache:

RewriteEngine On

# Match the old domain with or without www, on any protocol,
# and send it straight to the canonical https new domain in one hop.
RewriteCond %{HTTP_HOST} ^(www\.)?olddomain\.co\.uk$ [NC]
RewriteRule ^(.*)$ https://newdomain.co.uk/$1 [R=301,L]

On nginx, give the old names their own server block that does nothing but redirect:

server {
    listen 80;
    listen 443 ssl;
    server_name olddomain.co.uk www.olddomain.co.uk www.newdomain.co.uk;

    # A block listening on 443 ssl still needs a certificate covering
    # every name above, or nginx refuses to start at all.
    ssl_certificate     /etc/letsencrypt/live/olddomain.co.uk/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/olddomain.co.uk/privkey.pem;

    # $request_uri keeps the full original path and query string.
    return 301 https://newdomain.co.uk$request_uri;
}

Then set the canonical form in WordPress under Settings > General, matching WordPress Address and Site Address to the destination exactly, protocol and www decision included. If the server sends traffic to the non-www https version while WordPress thinks the site lives at the www version, you get a loop.

One caution on http versus https tests. Behind a CDN or load balancer, meaning something like Cloudflare that answers visitors before your server sees them, the encryption often ends at that outer layer. Your server then gets a plain request and reports %{HTTPS} as off even though the visitor is on https, so a rule trusting it builds a loop that only appears in production. Check %{HTTP:X-Forwarded-Proto} instead, the note the outer layer leaves saying what the visitor really used.

A rebuild is when this bites, because the redirect map, the canonical host and the new permalink structure all land on the same day. It is a large part of what I plan up front on a website rebuild, rather than patching afterwards from a Search Console error report.

Avoiding chains and loops

A chain is old URL to middle URL to final URL. Every hop is a round trip the visitor waits for, and it happens because someone added a redirect without checking whether the source was already a destination. Before adding one, check whether the new target already redirects somewhere else, and if it does, point the old URL straight at the final destination.

A loop is A pointing to B and B pointing back to A. The browser gives up with “too many redirects” and the site is down, admin included. The usual causes are a mismatch between Settings > General and the server rules, an SSL plugin fighting a server-level https rule, or two redirect plugins with contradictory entries. If you cannot reach wp-admin you can still switch the culprit off from outside: rename the plugin’s folder in wp-content/plugins over SFTP, which deactivates it instantly, or put a # at the start of each line of the .htaccess block. That total outage is a common reason people call about an urgent site fix, and it is almost always one duplicated rule.

How to check it worked

Do not test in the browser you have been working in. Browsers cache 301s aggressively, so the first response you got, right or wrong, is the one you keep seeing whatever you change on the server. Use curl, a command that fetches a URL and shows the raw answer with nothing cached in between. It is built into macOS and Windows, so open Terminal or PowerShell.

# -I sends a HEAD request and prints only the response headers.
curl -I https://example.com/old-page/

# -L follows the full chain; this prints each status and each hop
# so you can count them. One hop is the target. Three is a problem.
curl -sIL https://example.com/old-page/ | grep -Ei '^(HTTP|location)'

You want HTTP/2 301, then a single location: line naming the final URL, then HTTP/2 200. That line is the server saying where to go next, so two or more of them is a chain, and now is the moment to flatten it.

To stay in the browser instead, open its developer tools (F12), go to the Network tab, tick Disable cache, and load the old URL with the tools open. The Status column shows each hop. A private window helps but is not a guarantee.

Then check the destination genuinely returns a 200 and is the right page. Redirecting neatly to another 404 is a surprisingly popular outcome.

When it does not work

The .htaccess rule does nothing at all

Either you are on nginx, or your rule sits inside the # BEGIN WordPress block and got overwritten, or something earlier in the file already matched and ended processing. Order matters, because Apache uses the first matching rule.

The redirect works but drops the rest of the path

You used Redirect where you needed RedirectMatch with a capture group: the (.*) in the folder example above, which grabs whatever followed the folder name so $1 can paste it onto the destination. Without it the rest of the path is discarded.

The browser keeps going to the old destination

Cached 301. Test with curl instead. If you published a wrong 301 publicly, change it to the right target and accept that some visitors will carry the old one for a while; there is no way to force a browser to forget it.

ERR_TOO_MANY_REDIRECTS after going live

Compare Settings > General with what your server rule produces, character for character, including www and https. If they disagree you have a loop. Behind a CDN, check the protocol detection point above.

Rankings dropped after the migration anyway

Check that you redirected to genuine equivalents and not to category pages or the homepage. Google treats a redirect to an irrelevant page as a soft 404, deciding the page is gone whatever the status code claims, so the old URL is dropped and its signals go nowhere.

Common questions

Do 301 redirects lose ranking?

Google has said for years that 301s do not lose PageRank, its internal measure of how much authority a page has picked up from links, and in my experience a well-mapped migration holds up. What actually loses rankings is redirecting to a page that is not a real replacement, so the signal has nothing meaningful to attach to. The redirect is rarely the problem; the mapping is.

Why should I not redirect everything to the homepage?

Because Google treats a redirect to an irrelevant page as a soft 404 and passes nothing. It is also a poor experience: someone clicked a link about a specific thing and landed on a generic page with no explanation. If there is no equivalent, a 404 or 410 is more honest and more useful.

How long should I keep redirects in place?

Indefinitely for anything with external links. The often-quoted “one year” figure is about how long Google needs, not about backlinks and bookmarks, which never expire. Redirect rules cost almost nothing to keep. Removing them costs traffic.

Should I use a plugin or edit .htaccess?

Plugin for most sites, because of the 404 logging and because a non-developer can maintain it. Server config for large volumes, for a whole domain move, or when you want the rules in version control with the rest of the site. The two coexist fine as long as you know which layer owns which rule.

Can I redirect one URL to another site?

Yes, and it is normal when content moves to a different domain. In PHP use wp_redirect() rather than wp_safe_redirect(), since the safe version deliberately blocks external hosts.

What to do next

Pick the code first, then the route. A genuine replacement gets a 301, a temporary move gets a 302, and content you deliberately removed gets a 410 rather than a redirect to somewhere convenient. That decision matters more than which of the four methods implements it.

For most sites a redirect plugin with 404 logging is the sensible default, and the log teaches you more about which URLs matter than any amount of planning. Move to server rules when the volume justifies it or when a whole domain is moving, and keep every rule down to a single hop.

Test with curl rather than your browser, and check again a fortnight later once real traffic has found the gaps. If you are planning a rebuild and the redirect map is the part making you nervous, get in touch and I will map it out with you before anything goes live.