Skip to content
  • FG-38
  • 13 min read
  • Last checked November 8, 2026

How to Set Up 301 Redirects After a Redesign or Domain Change (Without Losing Rankings)

Changed your URLs or domain? Build a complete old-to-new map, pick the right status code, put the rules in the right layer and test every line before and after launch.

Run the health check

You launched a redesign or moved to a new domain, and now old links land on 404 Not Found, Search Console fills up with Not found (404) entries, and organic traffic slides. Or you are about to launch and want to avoid exactly that. Either way the job is the same: every address that used to work must send visitors and search engines to its new home in a single, permanent step.

A redirect is a short reply from the server that says “this page now lives over there”, with a status code that says whether the move is permanent. Search engines use that code to decide whether to move the old page’s ranking signals to the new address. Get the map, the code and the placement right and most sites come through a move with little lasting loss. Skip pages, send everything to the homepage or use the wrong code, and those pages start over.

What is happening

When a browser or crawler requests an old address, the server answers with a 3xx status and a Location header naming the new address. The client then requests that address. The codes differ in two ways: whether the move is permanent, and whether the client may change a form submission (POST) into a plain page request (GET).

Code Meaning Method kept? How Google treats it Use it for
301 Moved Permanently Permanent Clients may switch POST to GET Strong signal that the new URL is canonical Almost every redesign and domain redirect
308 Permanent Redirect Permanent Yes Same as 301 Permanent moves of form or API endpoints
302 Found Temporary Clients may switch POST to GET Weak signal; the old URL tends to stay indexed Short-term moves, testing rules
307 Temporary Redirect Temporary Yes Same as 302 Temporary moves of form or API endpoints

Two more facts shape the plan. Browsers cache 301 and 308 responses, sometimes for a very long time, so a wrong permanent redirect keeps misfiring for returning visitors after you fix it. And Google’s crawlers follow up to 10 redirect hops by default, but every extra hop slows users down and delays the move being processed, so the target is always one hop.

Plain HTML meta refresh and JavaScript redirects exist, but they are a fallback for hosts where you cannot set server redirects. Use server redirects whenever you can.

Before you start

  • Access you need: the old site (or a full backup or crawl of it), Google Search Console for the old and new sites, your analytics, SFTP or SSH to the server, and your DNS or CDN dashboard for domain moves.
  • Back up .htaccess or your Nginx config before adding rules, and export any existing redirect plugin settings.
  • How to run commands. Commands starting with curl, grep or bash run in a terminal on your own computer: macOS and Linux have one built in, and on Windows use WSL (the Windows Subsystem for Linux), because the test script below is a bash script. Commands starting with wp run over SSH in the site’s folder, the one with wp-config.php; on some hosts the command is wp-cli. Blocks starting with Redirect, RewriteRule, location, server or map are config lines you add to a file, not commands.
  • Test with temporary codes first. While writing rules, use 302 so a mistake is not cached in your browser. Switch to 301 once each rule is proven.

Step 1: Build a complete list of old URLs

The most common reason a move loses traffic is not a bad rule. It is pages nobody remembered existed. No single source lists everything, so combine several.

The old sitemap

curl -s https://OLD-DOMAIN/wp-sitemap.xml

Replace OLD-DOMAIN with the old site’s domain. WordPress 5.5 and later publish /wp-sitemap.xml; SEO plugins usually use /sitemap_index.xml instead. -s hides progress output. Good output is XML listing child sitemaps. Then pull the page addresses out of each child sitemap:

curl -s https://OLD-DOMAIN/CHILD-SITEMAP.xml | grep -o "<loc>[^<]*" | sed "s/<loc>//" >> old-urls.txt

CHILD-SITEMAP.xml is one of the addresses listed in the index. grep -o prints only each <loc> value, sed strips the tag, and >> appends to old-urls.txt, so you can repeat it for every child sitemap. Good output is a text file with one full URL per line.

WordPress itself

wp post list --post_type=any --post_status=publish --fields=ID,post_type,url --format=csv > old-posts.csv

Run over SSH on the old site. --post_type=any includes pages, posts and custom post types, url adds each permalink, and --format=csv writes a spreadsheet-friendly file. Category, tag and author archives are not posts, so take those from the sitemap or a crawl.

A crawl, analytics and Search Console

  • Crawl the old site with a desktop crawler such as Screaming Frog SEO Spider. It finds pages linked internally but missing from the sitemap, plus PDFs and images people link to.
  • Analytics: in GA4, open Reports, Engagement, Landing page, set a long date range (a year or more) and export. These are the URLs real visitors arrive on.
  • Search Console: export Performance, Search results, Pages for the last 16 months, and Links, Top linked pages. These are the URLs that carry search traffic and backlinks, so they matter most.

Paste everything into one spreadsheet column, normalize it (same protocol, same trailing slash style) and remove duplicates.

Step 2: Map each old URL to its best new URL

Add a second column for the new address. Work through it with three rules:

  1. One to one where possible. Each old page goes to the new page that answers the same question. A service page goes to the new service page, not the services overview.
  2. Do not send everything to the homepage. Google tends to treat mass redirects to an unrelated page as a soft 404, which passes nothing. If there is genuinely no equivalent, the nearest relevant parent section is better.
  3. Let dead content die honestly. If a page is gone for good with no replacement, a 404 or 410 is the correct answer. Do not invent a redirect.

Then look for patterns. If every /news/YYYY/MM/POST-SLUG/ became /blog/POST-SLUG/, one pattern rule replaces hundreds of lines. Keep a third column noting whether each row is handled by a pattern or a single rule.

Step 3: Decide where the redirects live

Redirects can be issued at the CDN edge, by the web server, or by WordPress through a plugin. Earlier layers are faster because later ones never run, but later ones are easier to edit.

Layer Good for Watch out for
CDN (for example Cloudflare Single Redirects or Bulk Redirects) Whole-domain moves, very large static lists, sites where the old domain no longer has hosting Plan limits on rule counts; rules hidden from anyone without CDN access
Web server (.htaccess, Nginx config) Pattern rules, domain-wide rules, hundreds of fixed rules Syntax errors take the site down; needs file or root access
WordPress plugin (Redirection, Rank Math, Yoast SEO Premium) Editors adding redirects over time, 404 logging, CSV import Only runs once WordPress has loaded; useless for a domain that no longer points at this WordPress

A sensible split: the domain-level redirect at the CDN or server, pattern rules in the server config, and individual page redirects either in the server config (if they will rarely change) or in a plugin (if editors manage them). Whatever you choose, write it down. Redirects spread across four places are how chains and conflicts start, a problem covered in our guide to redirect chains (FG-41).

Step 4: Write the rules on Apache or LiteSpeed

These go in .htaccess in the site folder, above the # BEGIN WordPress block, so they run before WordPress’s catch-all rule.

Single pages with Redirect

Redirect 301 /OLD-PATH/ https://example.com/NEW-PATH/

OLD-PATH is the old address without the domain, example.com is your new canonical domain and NEW-PATH is the destination. The big pitfall: Redirect matches by prefix and carries the rest of the path across. Redirect 301 /blog https://example.com/news also sends /blog-tips/ to /news-tips/ and /blog/POST-SLUG/ to /news/POST-SLUG/. Include the trailing slash, or use an anchored pattern instead.

Patterns with RewriteRule

RewriteEngine On
RewriteRule ^news/[0-9]{4}/[0-9]{2}/([^/]+)/?$ /blog/$1/ [R=301,L]
RewriteRule ^old-page/?$ /new-page/ [R=301,L,NC]

The first rule matches news/, four digits, two digits, then captures the slug (([^/]+) means one or more characters that are not a slash) and sends it to /blog/ plus that slug ($1). ^ and $ anchor the start and end so nothing else matches by accident, and /? makes the trailing slash optional. R=301 sets the code, L stops processing further rules, and NC ignores capitals. Replace old-page and new-page with your own paths. Pitfalls:

  • In .htaccess, the path being matched has no leading slash. ^/news/ never matches there (it does in a server config file).
  • The pattern never sees the query string. To match /index.php?page_id=ID_NUMBER, add RewriteCond %{QUERY_STRING} ^page_id=ID_NUMBER$ above the rule, where ID_NUMBER is the old page ID. Add the QSD flag to drop the old query string from the destination.
  • Do not mix Redirect and RewriteRule for overlapping paths. They come from different modules that run separately, so the order in the file does not decide which wins. Pick RewriteRule for everything once you need patterns.
  • Put specific rules above general ones, because the first match with L wins.

A whole domain

RewriteEngine On
RewriteCond %{HTTP_HOST} ^(www\.)?OLD-DOMAIN\.com$ [NC]
RewriteRule ^(.*)$ https://NEW-DOMAIN.com/$1 [R=301,L]

Replace OLD-DOMAIN and NEW-DOMAIN with the domain names (escape any dots in the old one with a backslash, as shown before com). (www\.)? catches both www and non-www, and $1 keeps the path, so /about/ lands on the new /about/. The query string is carried over automatically. If the URL structure also changed, put the page-level rules for the new domain above this catch-all so each old page goes straight to its final address in one hop.

Step 5: Write the rules on Nginx

These go in the site’s server block. Test with nginx -t and apply with systemctl reload nginx, or send them to your host.

location = /OLD-PATH/ { return 301 https://example.com/NEW-PATH/; }
rewrite ^/news/[0-9]{4}/[0-9]{2}/([^/]+)/?$ /blog/$1/ permanent;

Replace the capitalized tokens as in Step 4. location = is an exact match, so it is fast and cannot catch other paths. return 301 sends the redirect immediately. rewrite ... permanent issues a 301 for a pattern (redirect instead of permanent issues a 302). In Nginx the path does start with a slash. For a whole domain, give the old name its own server block:

server {
    listen 443 ssl;
    server_name OLD-DOMAIN.com www.OLD-DOMAIN.com;
    # ssl_certificate lines for the old domain go here
    return 301 https://NEW-DOMAIN.com$request_uri;
}

$request_uri is the original path plus query string, filled in by Nginx. The old domain still needs a valid certificate, or HTTPS visitors see a certificate warning before the redirect ever happens. For long lists, an Nginx map in the http context keeps one lookup table instead of hundreds of location blocks: map $uri to a new address, then in the server block add if ($new_uri) { return 301 $new_uri; }. $uri excludes the query string.

Step 6: Or manage page redirects in a plugin

The free Redirection plugin stores rules in the database, logs 404s so you can catch missed URLs after launch, and imports a CSV. Save your map from Step 2 as a CSV with the old path in the first column and the new URL in the second, then import it under Tools, Redirection, Import/Export. Set the default redirect type to 301 before importing. Rank Math and Yoast SEO Premium include similar redirect managers; use only one redirect manager per site.

Plugin redirects cost a PHP request and a database lookup for each redirected hit, which is fine for a few hundred rules. For thousands, or for any rule that must work when WordPress is down, use the server or CDN.

Step 7: Extra steps for a domain change

  • Keep the old domain pointed somewhere that redirects. If you cancel the old hosting, the redirects disappear with it. A small hosting plan, the new server with the old domain added, or CDN redirect rules all work.
  • Keep renewing the old domain. Google’s site move guidance is to keep redirects as long as possible, generally at least a year. Backlinks keep arriving for much longer, and an expired domain can be registered by someone else who then receives all those links.
  • Use the Change of Address tool. Verify both domains in Search Console as domain-level properties, add the redirects, then open the old property and go to Settings, Change of address. The tool checks that the old homepage and some pages redirect with 301 before it accepts the move. It is only for moving to a different domain, not for http to https or www changes.
  • Update everything you control: internal links and image URLs in the database (see FG-40 for safe search and replace), canonical tags, the XML sitemap (new URLs only), Google Business Profile, ad campaigns, email signatures and social profiles. Each internal link left on the old domain adds a needless hop.

Step 8: Test every line with a curl script

Spot checks miss things. Test the whole map, on staging before launch and on the live site straight after. Save your map as redirects.csv with two columns, full old URL and full expected new URL, no header row. Then save this as check-redirects.sh on your computer:

#!/usr/bin/env bash
while IFS=, read -r old expected; do
  expected="${expected%$'\r'}"
  result=$(curl -s -o /dev/null -w "%{http_code} %{redirect_url}" "$old")
  final=$(curl -s -o /dev/null -L -w "%{url_effective} %{num_redirects}" "$old")
  code="${result%% *}"; first="${result#* }"
  hops="${final##* }"; landed="${final% *}"
  if [ "$code" = "301" ] && [ "$first" = "$expected" ] && [ "$hops" = "1" ]; then
    echo "OK    $old"
  else
    echo "CHECK $old -> $code $first (lands on $landed after $hops hops)"
  fi
done < redirects.csv

Run it with bash check-redirects.sh in the folder containing both files. There are no placeholders inside the script; your URLs come from the CSV. For each row it makes two requests: one without following redirects, recording the status code (%{http_code}) and the Location target (%{redirect_url}), and one that follows them (-L), recording where it ended (%{url_effective}) and how many hops it took (%{num_redirects}). -o /dev/null throws the page content away. The \r line strips Windows line endings from spreadsheets saved on Windows. Good output is a column of OK. Every CHECK line tells you what went wrong: 302 means a temporary code, 404 means a missing rule, a different target means a mapping error, and more than one hop means a chain.

Advanced: Avoiding chains from past redesigns

Sites that have been redesigned before often carry older redirects: /page.html to /page/ from the first move, then /page/ to /services/page/ from this one. Every visitor following an old backlink now takes two hops, and a third redesign makes it three. Before launch, list the existing rules from .htaccess, your redirect plugin and any CDN rules, add their source URLs to your test CSV, and update each old rule so it points straight at the final address. The script above flags anything with more than one hop.

After launch, watch Search Console’s Pages report for a rising Not found (404) count and inspect the examples it lists; most are URLs missing from the map. The Redirection plugin’s 404 log shows the same from real visitors. Expect the old URLs to appear under Page with redirect for a while, which is normal and means Google has seen the redirect.

When to contact your host

Open a ticket if you cannot edit the server config, need an Nginx map or a large rule set installed, or need the old domain added to your hosting with a certificate so its redirects work over HTTPS. Send:

  • The rules you want added, as a file, and where they should apply (which domain names).
  • The output of the test script, showing the failing lines.
  • Whether a CDN sits in front, and which layer currently issues each redirect.
  • Your planned launch time in UTC, so they can be available if something breaks.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close