How to Find Redirect Chains and Redirects You Did Not Create in WordPress
A page redirects somewhere you never set up, or takes three hops to load. Trace each hop with curl, read the headers to find the layer that sent it, then fix it at the source.
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.
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.
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.
.htaccess or your Nginx config before adding rules, and export any existing redirect plugin settings.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.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.
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.
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.
Paste everything into one spreadsheet column, normalize it (same protocol, same trailing slash style) and remove duplicates.
Add a second column for the new address. Work through it with three rules:
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.
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).
These go in .htaccess in the site folder, above the # BEGIN WordPress block, so they run before WordPress’s catch-all rule.
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.
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:
.htaccess, the path being matched has no leading slash. ^/news/ never matches there (it does in a server config file)./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.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.L wins.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.
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.
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.
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.
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.
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:
Did this fix it?
Still stuck? Start again from what you can see in the Emergency Room.
Still stuck?
Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.
Get the free checklistsKeep reading
Search the field manual
Or jump straight to a symptom
Press / to search from any page, Esc to close