.htaccess redirects: copy-paste examples
·4 min read
An htaccess redirect is a line in Apache's .htaccess file that answers a request with a 3xx status and a new URL. For a single page, Redirect 301 /old-page https://example.com/new-page is all you need. Use RewriteRule with [R=301,L] only when the match depends on the host, the protocol or a pattern. Write every target as the full final URL, and no visitor will pass through more than one hop.
The examples below assume the canonical site is https://www.example.com. Swap in your own host. If you're unsure between 301 and 302, read 301 vs 302 redirects first. Every example here is permanent.
Redirect or RewriteRule: which htaccess redirect to use
Use Redirect and RedirectMatch from mod_alias for plain URL moves, and mod_rewrite for anything conditional. Apache's own guide on when not to use mod_rewrite says the same thing.
Two details catch people out. Redirect without a status sends a 302, per the mod_alias docs, so always write 301. And in .htaccess, a RewriteRule pattern never starts with a slash. Apache strips the directory prefix before matching, so ^/old-page$ silently never matches. Write ^old-page$.
The mod_rewrite flags you will use:
| Flag | What it does |
|---|---|
R=301 |
Sends an external redirect with that status. Plain R sends a 302. |
L |
Stops this pass through the rules. Always pair it with R. |
NC |
Matches without regard to case. Use it on host names. |
QSA |
Merges the old query string into a target that has its own. |
QSD |
Drops the query string. Without it, Apache copies the old query string to a target that has none. |
NE |
Stops Apache percent-encoding the target, so a #fragment survives. |
END |
Like L, but also stops Apache rerunning the rules on the rewritten URL. Use it for internal rewrites in .htaccess. |
Redirect a page, a folder or a whole domain
These four cover most moves.
# One page, exact match only
RedirectMatch 301 ^/old-page/?$ https://www.example.com/new-page
# A folder: /blog/post-1 goes to /articles/post-1
Redirect 301 /blog https://www.example.com/articles
# A whole domain, in the old domain's own .htaccess
Redirect 301 / https://www.example.com/Redirect matches by prefix and appends the rest of the path, so the folder rule carries every child URL across. It only matches whole path segments, so /blog won't catch /blogroll. That prefix matching is also why a single page belongs in RedirectMatch. Redirect 301 /old-page would move /old-page/faq too.
The Redirect 301 / line only works when the old domain has its own document root. If both domains point at the same folder, it redirects the new domain to itself forever. Test the host instead:
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(www\.)?old-example\.com$ [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]Force HTTPS and pick www or non-www in one hop
Fix the protocol and the host in a single rule. Two separate rules send http://example.com through two redirects.
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^ https://www.example.com%{REQUEST_URI} [R=301,L]For a non-www site, change the second condition to ^www\. and the target to https://example.com%{REQUEST_URI}. %{REQUEST_URI} holds the path without the query string, and Apache copies the query string across on its own. Choosing a host is its own question, covered in www vs non-www.
One warning. If a CDN or load balancer ends TLS and talks plain HTTP to Apache, %{HTTPS} is always off and this rule loops. The fix is in ERR_TOO_MANY_REDIRECTS.
Add or remove the trailing slash
Pick one form and redirect the other. Both rules skip real files and folders, which Apache handles itself.
# Remove it: /pricing/ goes to /pricing
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)/$ https://www.example.com/$1 [R=301,L]
# Or add it: /pricing goes to /pricing/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*[^/])$ https://www.example.com/$1/ [R=301,L]Use one block, never both. Together they bounce a URL between the two forms until the browser gives up.
How to stop .htaccess redirects from chaining
A chain happens when one rule's target matches another rule. Each rule looks fine on its own, and the visitor still pays for every hop. Google says its crawlers follow up to 10 redirect hops, and every hop slows the crawl. Three habits keep it to one.
Use absolute, final targets. Every example above writes https://www.example.com/... in full. A relative target like /new-page makes Apache reuse the scheme and host of the current request, so http://example.com/old-page lands on the http, bare-host version and needs a second redirect.
Put specific rules first. Order the file as page and folder moves, then the trailing slash rule, then the HTTPS and www rule last. A request for http://example.com/old-page matches the page rule first and goes straight to the final URL. The host rule only catches what nothing else fixed.
Don't mix modules blindly. In .htaccess, Apache runs mod_alias before mod_rewrite, per the mod_rewrite guide. A Redirect therefore fires before any RewriteRule, wherever it sits in the file. With absolute targets that's harmless. With relative ones it builds chains you can't see by reading the file.
Put all of it above any front-controller block, such as the one WordPress adds. That block rewrites the request to index.php, and rules below it then see the wrong path. When you replace an old redirect, update the rules that pointed at its old target, and the internal links too. A chain of redirects that once made sense is the most common one I find, and redirect chains covers cleaning them up.
Test with curl before you trust a browser, which caches 301s:
curl -sIL http://example.com/old-page | grep -iE "^(HTTP|location)"You want one 301 and one 200.
Check your htaccess redirects hop by hop
The Redirect Chain & HTTP Header Checker follows a URL hop by hop and lists each status code and Location target. It flags loops, chains of more than one redirect, chains still redirecting after 10 hops, temporary redirects that change host, HTTPS sent back to HTTP and slow hops. It also checks that the http, https, www and bare versions of your domain end at one URL, and reports response headers such as X-Robots-Tag: noindex that block indexing.
It reads HTTP redirects only. It doesn't run JavaScript or follow meta refresh redirects past the first document, and it never sees your .htaccess file, only what the server sends back. A run costs 10 credits.