.htaccess Redirect Rules: A Practical Guide
Practical guide to .htaccess redirect rules: mod_rewrite basics, single page and pattern redirects, forcing HTTPS, www canonicalization, query string handling, and debugging tips.
The .htaccess file is the most common way to manage redirects on Apache web servers. It sits in your site's root directory (or any subdirectory) and controls how the server handles requests for that path. For the full overview of all redirect types, see our HTTP Redirect Guide.
This guide covers the practical side: syntax, common patterns, regex rules, and the mistakes that cause redirect loops and broken URLs.
What .htaccess Actually Is
.htaccess (hypertext access) is a per-directory configuration file for Apache. When Apache receives a request, it checks for .htaccess files in the directory tree leading to the requested file and applies any directives it finds.
For redirects, you will use one of two Apache modules:
- mod_alias: Simple redirects with the
RedirectandRedirectMatchdirectives. No regex required for basic use. - mod_rewrite: The full redirect engine. Uses
RewriteRuleandRewriteCondfor pattern matching, conditions, and complex logic.
Most redirect work uses mod_rewrite because it handles conditions (like checking HTTPS status) that mod_alias cannot.
mod_rewrite Basics
Every mod_rewrite block in .htaccess starts with enabling the rewrite engine:
RewriteEngine On
You only need this once per .htaccess file, not once per rule. After that, you write rules using two directives:
- RewriteCond: A condition that must be true for the next RewriteRule to fire. Optional, but used often.
- RewriteRule: The actual redirect or rewrite instruction.
The basic syntax:
RewriteRule pattern substitution [flags]
patternis a regex matched against the request path (without the leading slash in.htaccesscontext).substitutionis the target URL.flagscontrol behavior:[R=301,L]means "301 redirect, last rule (stop processing)."
Single Page Redirects
The simplest case: redirect one URL to another.
# Using mod_alias (simplest)
Redirect 301 /old-page https://example.com/new-page
# Using mod_rewrite
RewriteEngine On
RewriteRule ^old-page$ https://example.com/new-page [R=301,L]
Both produce a 301 permanent redirect. The mod_alias version is shorter, but mod_rewrite gives you more control when you need conditions.
For a temporary redirect, use 302 instead:
Redirect 302 /sale-page https://example.com/current-sale
# Or with mod_rewrite
RewriteRule ^sale-page$ https://example.com/current-sale [R=302,L]
Pattern Redirects with Regex
Regex is where mod_rewrite earns its keep. You can redirect entire directories, match URL patterns, and capture parts of the URL for reuse.
Redirect an entire directory
RewriteEngine On
RewriteRule ^old-blog/(.*)$ https://example.com/blog/$1 [R=301,L]
This redirects /old-blog/my-post to /blog/my-post. The (.*) captures everything after old-blog/, and $1 inserts it into the target.
Redirect all URLs with a specific extension
RewriteRule ^(.+)\.html$ https://example.com/$1 [R=301,L]
This strips the .html extension: /about.html becomes /about.
Redirect with multiple capture groups
RewriteRule ^products/([^/]+)/([^/]+)$ https://example.com/shop/$1/$2 [R=301,L]
$1 is the first capture group, $2 is the second. /products/shoes/red becomes /shop/shoes/red.
Forcing HTTPS
One of the most common .htaccess redirects is forcing all HTTP traffic to HTTPS. For the complete HTTPS migration guide, see HTTP to HTTPS Redirect Guide.
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
The RewriteCond checks if the request is not HTTPS. If true, the RewriteRule redirects to the same URL with https://.
If your site is behind a load balancer or CDN that terminates SSL, the server always sees HTTP. Check the X-Forwarded-Proto header instead:
RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} =http
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
WWW vs Non-WWW Canonicalization
Pick one and redirect the other. Mixing both creates duplicate content issues and can cause redirect loops.
Redirect www to non-www
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
Redirect non-www to www
RewriteEngine On
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]
Combine HTTPS + www canonicalization in one hop
Do not chain these as separate redirects. That creates a redirect chain (HTTP non-www to HTTPS non-www to HTTPS www). Handle it in a single rule:
RewriteEngine On
# Force HTTPS + www in one redirect
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]
Query String Handling
By default, mod_rewrite passes the original query string through to the target URL. If the original request is /page?ref=google, the redirect target gets ?ref=google appended automatically.
Strip the query string
Add a ? at the end of the substitution to discard the original query string:
RewriteRule ^old-page$ https://example.com/new-page? [R=301,L]
Redirect based on query string
Use RewriteCond to match query parameters:
RewriteCond %{QUERY_STRING} ^id=42$
RewriteRule ^product\.php$ https://example.com/products/widget? [R=301,L]
This redirects /product.php?id=42 to /products/widget (with the query string stripped).
Preserve a specific query parameter
RewriteCond %{QUERY_STRING} ^category=([^&]+)
RewriteRule ^products\.php$ https://example.com/shop/%1? [R=301,L]
%1 refers to the first capture group in a RewriteCond (not $1, which is for RewriteRule captures).
Common .htaccess Mistakes
Forgetting the L flag
Without [L] (last), Apache continues processing rules after a match. This can cause unexpected behavior or double redirects.
# BAD: Missing L flag
RewriteRule ^old$ /new [R=301]
RewriteRule ^new$ /final [R=301]
# /old redirects to /new, then /new redirects to /final = redirect chain
# GOOD: L flag stops processing
RewriteRule ^old$ /final [R=301,L]
Redirect loops from overlapping rules
If your target URL matches your source pattern, you get a loop:
# LOOP: Everything matches ^(.*)$, including the target
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
Add a condition to prevent this:
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://example.com/$1 [R=301,L]
Wrong file path vs URL confusion
In .htaccess, RewriteRule patterns match against the URL path relative to the directory containing the .htaccess file. They do not match the full URL with the domain. The leading slash is stripped.
# WRONG: Leading slash in pattern
RewriteRule ^/old-page$ /new-page [R=301,L]
# CORRECT: No leading slash in .htaccess
RewriteRule ^old-page$ /new-page [R=301,L]
This is different from server config context (httpd.conf or VirtualHost), where the leading slash is included.
Multiple RewriteEngine On directives
Only use RewriteEngine On once per .htaccess file. Multiple declarations can reset previously defined rules depending on the Apache version.
Debugging with RewriteLog
When rules do not behave as expected, Apache's rewrite logging shows exactly what is happening.
Apache 2.4+
# In your VirtualHost config (NOT in .htaccess)
LogLevel alert rewrite:trace3
Levels go from trace1 (minimal) to trace8 (extremely verbose). Start with trace3. Check the error log for output:
[rewrite:trace3] mod_rewrite.c(476): applying pattern '^old-page$' to uri 'old-page'
[rewrite:trace3] mod_rewrite.c(476): RewriteCond: input='off' pattern='off' => matched
Apache 2.2 (legacy)
RewriteLog "/var/log/apache2/rewrite.log"
RewriteLogLevel 3
Never leave rewrite logging on in production
Rewrite logging generates massive amounts of output under traffic. Enable it to diagnose a problem, then turn it off immediately. A forgotten RewriteLog can fill your disk.
Performance Considerations
.htaccess files have a real performance cost. Apache reads and parses them on every single request, for every directory in the path. On high-traffic sites, this adds up.
When to use .htaccess
- Shared hosting where you have no access to the main server config
- Per-directory overrides that need to be managed separately from the server config
- Quick changes that do not require an Apache restart
When to use server config instead
If you have access to httpd.conf or your VirtualHost configuration, put your redirect rules there instead. Rules in the server config are parsed once at startup, not on every request. This is significantly faster.
# In VirtualHost config (faster than .htaccess)
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
Minimize regex complexity
Simple string matches are faster than complex regex patterns. If you can use Redirect (mod_alias) instead of RewriteRule (mod_rewrite), do it. If you must use regex, keep patterns as specific as possible. Avoid (.*) when a more targeted pattern works.
Order rules by frequency
Put your most frequently matched rules first. Apache evaluates rules in order and stops at the first match (with the [L] flag). If 80% of your redirected traffic hits one rule, put it at the top.
Bulk Redirects
For site migrations with hundreds of redirects, listing them all as individual RewriteRule lines works but gets unwieldy. Consider alternatives:
# Individual rules (works but messy at scale)
RewriteRule ^old-page-1$ /new-page-1 [R=301,L]
RewriteRule ^old-page-2$ /new-page-2 [R=301,L]
RewriteRule ^old-page-3$ /new-page-3 [R=301,L]
# ... hundreds more
RewriteMap (server config only)
If you have access to server config, RewriteMap lets you store redirects in a text file or database:
# In httpd.conf or VirtualHost
RewriteMap redirects txt:/etc/apache2/redirect-map.txt
# In .htaccess or VirtualHost
RewriteCond ${redirects:$1} !=""
RewriteRule ^(.+)$ ${redirects:$1} [R=301,L]
The map file is a simple key-value list:
old-page-1 /new-page-1
old-page-2 /new-page-2
old-page-3 /new-page-3
This is parsed once at startup and looked up via hash table, so it scales to thousands of entries without the performance hit of hundreds of regex rules.
Testing Your Redirects
Always test after making changes. A single typo in .htaccess can return a 500 error for your entire site.
# Test a single redirect
curl -I https://example.com/old-page
# Look for: HTTP/1.1 301 Moved Permanently
# Look for: Location: https://example.com/new-page
# Follow the full redirect chain
curl -ILs https://example.com/old-page | grep -E "HTTP/|Location:"
Test these scenarios:
- The specific URLs you redirected
- URLs that should not be redirected (make sure your patterns are not too broad)
- Query strings (are they preserved or stripped as intended?)
- Both HTTP and HTTPS versions
- Both www and non-www versions
Trace your redirect chains
Paste any URL and see every hop, status code, and header instantly.
References
- Apache Software Foundation, "mod_rewrite documentation," https://httpd.apache.org/docs/current/mod/mod_rewrite.html
- Apache Software Foundation, "mod_alias documentation," https://httpd.apache.org/docs/current/mod/mod_alias.html
- Apache Software Foundation, ".htaccess files," https://httpd.apache.org/docs/current/howto/htaccess.html
Related Articles
Never miss a broken redirect
Trace redirect chains and detect issues before they affect your users and SEO. Free instant tracing.
Try Redirect Tracer