301 Redirects in .htaccess: Copy-Ready Rules and the Traps

301 redirects htaccess

Writing 301 redirects htaccess rules is the fastest way to move URLs on an Apache server, and the fastest way to take a site offline if you get a character wrong. The file is read on every request, applied immediately without a restart, and a syntax error returns a 500 for the entire site — not just the path you were editing.

So before anything else: back up the working file, keep an FTP or SSH session open so you can revert, and test on a staging copy when the change is more than a line or two. With that out of the way, here are the rules that cover almost every real situation.

Prerequisites: is .htaccess even active?

Rules that silently do nothing are the most common complaint, and there are only three causes. The mod_rewrite module must be enabled. The server’s virtual host must set AllowOverride All (or at least FileInfo) for the directory. And the file must be named exactly .htaccess, with the leading dot, in the web root — Windows editors love to save it as htaccess.txt.

If you are on nginx, none of this applies. nginx does not read .htaccess at all; redirects go in the server block and require a config reload. Plenty of hosting support tickets start with someone pasting Apache rules into an nginx box and wondering why nothing happens.

Single-page redirects

For a one-to-one move, the Redirect directive from mod_alias is the simplest thing that works:

Redirect 301 /old-page/ https://example.com/new-page/

The source is a path, the destination is a full absolute URL. One warning: Redirect matches prefixes, so Redirect 301 /blog also catches /blogging-tips. Use RedirectMatch with an anchored pattern when you need an exact match:

RedirectMatch 301 ^/old-page/?$ https://example.com/new-page/

Site-wide rules: HTTPS and www

These two are on nearly every site, and the important thing is to combine them so a visitor never takes two hops:

RewriteEngine On

RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]

Read the flags, because they carry the meaning. R=301 issues a permanent redirect rather than an internal rewrite. L stops rule processing for this pass. NC makes the match case-insensitive. Leave off R=301 and Apache rewrites the URL internally with no redirect at all, which is a completely different behavior and a frequent source of confusion.

The missing L flag deserves its own warning. Without it, Apache continues evaluating subsequent rules against the already-rewritten URL, which is how a file full of individually sensible rules produces a destination nobody wrote. Combined with a rule whose pattern matches its own output, that is the standard recipe for taking a site down with a 500 error or an infinite loop — and because .htaccess applies instantly on save, you find out in production.

RewriteCond ordering is the other common landmine. Conditions apply only to the single RewriteRule immediately following them, and multiple conditions are ANDed together unless you add the [OR] flag. Insert a blank line or a comment between a condition and its rule and it still works; insert another rule and the conditions silently attach to the wrong one.

Behind Cloudflare or a load balancer, %{HTTPS} is often off even on a secure connection, because the proxy terminates TLS. Use the forwarded header instead:

RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]

Folders, patterns, and query strings

To move an entire directory while preserving the path beneath it:

RewriteRule ^old-folder/(.*)$ /new-folder/$1 [R=301,L]

To redirect a whole old domain to a new one, keeping paths intact:

RewriteCond %{HTTP_HOST} ^olddomain\.com [NC,OR]
RewriteCond %{HTTP_HOST} ^www\.olddomain\.com [NC]
RewriteRule ^(.*)$ https://www.newdomain.com/$1 [R=301,L]

Query strings need special handling, because RewriteRule only matches the path. Match the query separately and append a ? to the destination to discard the original string:

RewriteCond %{QUERY_STRING} ^id=42$
RewriteRule ^product\.php$ /products/blue-widget/? [R=301,L]

That trailing question mark matters. Without it, Apache carries the old query string through and you end up at /products/blue-widget/?id=42, which is a duplicate URL you then have to canonicalize.

Apache versus nginx: why the same rule behaves differently

The distinction is architectural, not cosmetic. Apache checks for an .htaccess file in the requested directory and every parent directory above it, on every single request — including requests for images, stylesheets, and fonts. That is what makes the file so convenient: edit it, save it, and the change is live with no restart and no root access. It is also what makes it slow at scale, because none of that work is cached.

nginx made the opposite trade. Configuration is read once at startup and held in memory, so per-request overhead is near zero, but any change requires editing the server block and reloading the process. On managed hosting that usually means a support ticket and a wait. There is no .htaccess equivalent and there never will be — it is a deliberate design decision, not a missing feature.

If you are unsure which you are on, check the Server response header before spending an afternoon debugging rules that were never read.

Chains, loops, and the crawl cost of sloppy rules

After a couple of years of accumulated edits, most .htaccess files contain redirects that fire into other redirects. Page A moved to B in 2023, B moved to C last spring, and nobody went back to update the first rule. The result is a chain: three round trips before a visitor sees content, and three fetches before a crawler reaches the page it wanted.

That cost is not theoretical. Every hop is a separate request against your crawl capacity, and on a large site with thousands of legacy URLs, chains consume a meaningful share of what Googlebot fetches each day. Google follows a reasonable number of hops but gives up well short of ten.

Loops are the acute version of the same problem. Two rules each undo the other, the request bounces until the browser gives up, and users see the “too many redirects” error. The classic pairing is an HTTPS rule and a www rule written separately and ordered so that satisfying one violates the other. The fix is always the same: one combined rule, with conditions that prevent it firing against its own destination.

Fixing chains is mechanical: resolve every rule’s destination through to the final 200-status URL, then rewrite the rule to point straight there.

Order of operations and rule count

Apache evaluates rules top to bottom, so put specific one-to-one redirects above broad pattern rules. Otherwise a catch-all fires first and your carefully mapped exception never runs.

In WordPress, keep your custom rules above the # BEGIN WordPress block. Anything inside that block gets overwritten whenever permalinks are saved, and you will lose the lot without warning.

Rule count matters at scale. A few hundred is invisible; ten thousand hand-written lines is measurable latency on every request, which surfaces in your Largest Contentful Paint and your crawl rate. Past a few thousand, move them to the virtual host config, use a RewriteMap against a flat file, or push the map to your CDN.

Testing and the mistakes that cost rankings

Never trust a browser to tell you a redirect works — browsers cache 301s indefinitely and will replay stale behavior long after you fixed the server. Ask the server directly:

curl -sIL https://example.com/old-page/ | grep -E "HTTP/|location"

You want exactly one 301 followed by one 200. Anything else is a defect.

Test in three passes, not one. Before deploy, run the rules on a staging copy of the site with the same Apache version and the same module set — a rule that works locally and fails on the host is nearly always a module or AllowOverride difference. Immediately after deploy, re-check your top 100 URLs by organic clicks and your top URLs by referring domains, because those two lists carry most of the value at risk. Then a week later, crawl the whole site again to catch the chains that only appear when several rules interact.

The four failures worth hunting for:

  • Chains. Old URL → intermediate → final. Point every legacy rule straight at the current destination.
  • Loops. A rule that matches its own destination fires forever and produces the “too many redirects” error. Always add a condition excluding the target.
  • Redirects to irrelevant pages. Dumping hundreds of retired URLs on the homepage is treated as a soft 404 and passes no signal. Map to the closest genuine equivalent, or return 410.
  • Internal links still pointing at redirected URLs. Your own navigation should never rely on a redirect it can avoid.

Verifying thousands of rules by hand is not realistic, and chains are invisible when you inspect one URL at a time. SEO Rocket’s technical audit crawls entire sites — verified past 900 pages — and returns evidence per issue rather than a count: the actual redirecting URLs, their targets, and the titles and tags at both ends, which is exactly the list you hand a developer. It does not write or deploy .htaccess rules for you; that stays on your server. It also reports PageSpeed and Core Web Vitals with real-user field data beside lab metrics, so you can see whether an oversized rewrite file is costing you response time.

After deploying, re-test your top 100 URLs by traffic and by backlinks first, then leave it alone. Rankings on redirected URLs settle over weeks, and the two- or three-position daily wobble along the way means nothing.