Renaming something takes one keystroke. The URL it breaks is permanent. You shorten a post slug because the old one was ugly, you drop /blog/ from the permalink structure, you merge two categories, you move a shop section from /produkte/ to /products/. Each is a five second decision that leaves a set of addresses behind: still in Google’s index, still in a newsletter somebody sent in 2021, still linked from a forum thread you will never find.
The rules you write to catch those addresses start clean. Twelve lines in .htaccess, every one obvious. Then a year passes. Somebody adds a broad prefix rule near the top. A plugin imports forty more from a spreadsheet. One rule points at a URL that another rule also redirects. Nothing throws an error. The site just gets slower, and a handful of pages quietly stop being reachable.
That is what makes redirects nasty: a broken one usually still returns something. A loop is the only failure a browser bothers to announce. A three hop chain, a rule shadowed by the one above it, a 302 where you meant a 301, a rule that swallows every image request under /wp-content/uploads/: all of them look fine when you click through, and all of them cost you.
The generator below takes a list of old and new URLs, one rule per line, and writes the same rules three ways: Apache, nginx, and a CSV for a redirect plugin import. Underneath it there is a tester. Type a URL, see which rule catches it first, what it turns into, and whether anything catches it again.
Redirect rule generator
Turn a list of old and new URLs into Apache, nginx and CSV redirect rules, then test a URL against them and find the loops, the duplicates and the rules that never run before any of it reaches a server. Everything is worked out in this browser tab, nothing is uploaded and nothing is fetched.
Separate the two with a space, a tab or a comma. A line starting with # is skipped. A source without a leading slash gets one. The sample above carries a deliberate loop on the last two lines, so there is something to find.
Nothing pasted yet. The clipboard is read in this tab and goes straight into the rule list, it is never sent anywhere.
The four codes and what each one promises
A redirect is a status line and a Location header. The number in that status line answers two separate questions, and most redirect advice mentions only the first.
Question one is permanence. 301 and 308 say the old address is gone for good, so cache this and update your links. 302 and 307 say the move is temporary, so keep asking the old address. Search engines act on that: a permanent redirect consolidates the old URL into the new one, a temporary one leaves the old URL as the canonical candidate.
Question two is what happens to the request method, and this is where the old rule of thumb breaks. The spec allows a client that receives a 301 or a 302 to turn a POST into a GET and drop the body, for historical reasons: browsers did it before anyone wrote it down. Every browser does exactly that. 307 and 308 forbid it: method and body survive the hop. So “always use 301” is right for a page and wrong for anything that receives a POST, which means form endpoints, webhook receivers, REST routes and payment callbacks. Send a 301 for those and the sender’s POST arrives as a bodyless GET, which looks less like an error than an empty request.
A third property matters as much: 301 and 308 are cacheable by default, and browsers take that seriously. A wrong 301 can sit in a visitor’s browser long after you deleted the rule, and nothing on your server can reach in to correct it. If the move might not be final, ship a 302, confirm the destination, then change the number. The generator asks for the status once, as four cards with a one line explanation each.
Order of evaluation, and the rule that kills everything below it
Apache processes Redirect and RedirectMatch directives that sit in the same context in the order they appear, and the first match wins. Matching for Redirect is a plain string prefix on the URL path. Not a path segment, a string. So this line:
Redirect 301 /team /company/team
catches /team, and /team/history, and also /team-photos-2024, and /teamviewer-setup. Any rule further down whose source begins with /team is dead code. It will never run, it will never log a hit, and the only symptom is one page going to the wrong place.
mod_rewrite behaves the same way, top to bottom, with [R=301,L] ending the round. nginx does not. Prefix location blocks are chosen by longest match rather than by position, so reordering an nginx config often changes nothing while the same reordering in Apache changes everything. rewrite directives inside a server block, on the other hand, run in order. That is why the generator writes both nginx forms: they are not interchangeable.
The tool’s prefix option is a plain string prefix for the same reason. Segment boundary matching would look tidier, but the generated lines do not work that way, and a preview that disagrees with the server is worse than none. The findings list flags any rule swallowed by an earlier prefix rule.
One placement detail for .htaccess: your rules go above the # BEGIN WordPress marker. WordPress rewrites the block between those markers whenever permalinks are saved, so anything you put inside it is temporary.
Two hops cost more than twice one hop
A redirect is a full round trip that produces no content. On the same host over an open connection that is one round trip, perhaps 60 to 150 milliseconds on mobile. Cross the host boundary and the browser also pays for a DNS lookup, a TCP handshake and a TLS handshake, which is why example.com to www.example.com is not the free no-op it looks like in a config file.
Chains are rarely written on purpose. They assemble themselves. A typical one runs four hops and nobody authored more than one: HTTP to HTTPS at the edge, bare host to www, your rule from the old path to the new path, then WordPress adding a trailing slash because your target did not have one. The sum is a page that takes half a second to start existing.
Crawlers cut chains off. Google follows up to ten redirect hops and treats anything longer as an error, and its guidance is to point every old URL at the final destination rather than at the next rule. Browsers stop at twenty, at which point Chrome shows ERR_TOO_MANY_REDIRECTS and Firefox says the page is not redirecting properly. Neither shows the chain, so the visitor reports that the page is broken and you get to guess.
Real loops are almost never the obvious A to B to A. The common one is a fight between layers: your permalink structure ends in a slash, someone adds a rule that strips trailing slashes for tidiness, and WordPress puts the slash back on every request, forever. Another version is a prefix rule sending /shop to /shop/products, where the target still starts with /shop. That one is invisible in a config file and obvious the moment you walk a URL through the rules. The tester follows the chain to eight hops, calls anything longer a loop, and names how the chain ended: settled, off to another host, relative, or round in circles.
What WordPress redirects before you write a single rule
Server rules run before PHP exists, which is why an .htaccess rule always beats a plugin rule for the same URL. If the request does reach PHP, WordPress has two redirect handlers of its own, both on template_redirect, both sending 301.
The first is wp_old_slug_redirect(). When you change a post’s slug, wp_check_for_changed_slugs() stores the previous one as a _wp_old_slug post meta row, and a later request for the old address looks it up and forwards to the current permalink. It saves you after most renames, with two limits worth memorising: it only fires for published posts, and it skips hierarchical post types entirely. Rename a Page and nothing catches it. That is the case you have to write a rule for.
The second is redirect_canonical(), the source of most accidental extra hops. It adds or removes the trailing slash to match your permalink structure, converts ?p=123 into the pretty permalink, strips a stray index.php, and fixes malformed paged URLs. It also handles attachment requests: when the wp_attachment_pages_enabled option is off, an attachment URL goes to the file itself instead of rendering a thin page, which is the mechanism behind the attachment pages nobody knew they had published. Every one of those is a 301 you did not write, and each stacks on top of your rule when your target is not already in canonical form. Write targets with the trailing slash the structure expects, or you have doubled the hop count of the whole list.
A plugin rule and an htaccess rule for the same URL
This is the fight nobody wins. The server rule fires first, PHP never runs, and the plugin’s hit counter for that URL stays at zero forever, so you conclude the plugin is broken and add another rule. If the two point at different targets you now have a two hop chain spanning two systems, and no single interface shows you both halves.
You can tell them apart in one command. WordPress has sent an X-Redirect-By header on every wp_redirect() call since 5.1, defaulting to the string WordPress. If the header is there, PHP produced the redirect. If it is missing, the web server did.
curl -sIL https://example.com/old-page
| grep -i '^HTTP/|^location:|^x-redirect-by:'
Every hop prints its status line, its destination and its author. Pick one layer per URL and stay there: server rules for whole directory moves and for anything that must survive PHP being unavailable, plugin rules for the long tail that editors maintain, because editors are not going to open .htaccess.
Broad rules deserve one more warning: a prefix rule with a short source swallows files, not just pages. A rule on /wp will happily redirect /wp-content/uploads/2024/03/photo.jpg, and the symptom is not a redirect error, it is images that stop showing with no obvious cause. If you have just moved the uploads directory, those rules are their own careful job, covered in the guide to moving the uploads folder safely.
What the generator is doing underneath
Each line is split into a source and a target on the first space, tab or comma, and lines starting with # are skipped. The paste target takes two columns straight out of a spreadsheet, appends them as rules and reports the rows it ignored. The list is capped at 500 lines.
Because a comma is one of the separators, a comma inside a URL is not supported; a line that splits into three columns is flagged with the fix, which is to write it as %2C. The findings list also catches loops, a rule whose source and target are identical, two rules with the same source, a rule shadowed by an earlier prefix rule, a relative target where an absolute one is required, and a line with no target. It also reports that a host in the source is ignored: a rule only ever sees the path, in every flavour the tool writes, so https://old.example.com/page and /page behave identically.
The options change the generated lines, not just the preview. Exact matching writes a rule that fires on one path, prefix matching carries the rest of the path through to the target, query matching changes both what counts as a match and whether the old query is carried over, and the trailing slash option decides whether /team and /team/ go through the same rule.
Two combinations produce a refusal rather than a line, and the refusal is the useful part. nginx rewrite can only emit permanent and redirect, so it cannot express 307 or 308, and it cannot see the query string, so it cannot honour query matching. In both cases the rewrite block carries a comment saying why, instead of a directive that would quietly do something other than what the field above it claims. The return lines cover those cases properly, with $request_uri when the query takes part in the match and $is_args$args when the old query should be carried over. mod_alias cannot test a query string at all, so with query matching on, only the mod_rewrite half of the Apache block is query aware.
All of it happens in your browser. The rules are parsed, matched and chained in the page, with the same semantics the generated lines describe, so the tester and the server agree. Nothing is uploaded and nothing is fetched.
When you need something this does not do
There is no regex source, no wildcard syntax and no per line status override. If you need to catch a shape rather than a list, every /2019/ date archive or every ?p= query, that is a RewriteRule with a real regular expression and you should write it by hand. There is no import of an existing .htaccess either, so auditing what is already on the server stays a manual read.
- Protocol and host canonicalisation is left out on purpose. One rule at the top of the server config handles HTTP to HTTPS and bare host to
wwwfor the whole site; duplicating that per URL is how four hop chains get built. - Thousands of rules do not belong in a flat list. Apache walks that list on every request; at real scale use an nginx
mapblock or a plugin that keeps rules in an indexed table. - Language or country routing needs a 302 or 307 plus a
Varyheader, never a permanent redirect, or you cache one visitor’s country into everyone else’s browser. - Renaming media files is a redirect problem too, and a different one, because the old URL is usually referenced inside post content as well as from outside. File naming is the boring detail that breaks things, and it is cheaper to get right before upload.
One last thing that presents as a redirect problem and is not. If pages feel slow, count the hops on the entry URL before you blame the host: four redirects is half a second of nothing on every visit, and it shows up in no plugin’s performance report. The diagnostic order for a slow WordPress site puts network round trips ahead of most of what people tune first, for exactly that reason.
Rules you can still read a year from now
A redirect list decays the way any config file decays: not by breaking, but by accumulating. The rules that hurt are never the ones you thought hard about. They are the broad prefix rule added in a hurry, the target missing a trailing slash, the 301 that should have been a 308 because that path receives form posts. None of them announce themselves, and all of them are visible in seconds if you walk one URL through the list and watch where it goes.
So the working method is short. Write every old URL against its final destination, not against the next rule. Match the canonical form of the target exactly, slash and all, so WordPress does not add a hop behind your back. Keep specific rules above broad ones, and keep the broad ones as few as you can stand. Decide once whether a given URL is the server’s business or the plugin’s, and never both. Then test the awkward ones: the path that is a prefix of another path, the one with a query string, the one you have moved twice.
The generator will not make those decisions for you. What it removes is the gap between writing a rule and finding out what it does, normally measured in weeks and paid for in traffic. Paste the list, pick the status, read the findings, walk a URL through, and only then copy a block to your server.