.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 Redirect and RedirectMatch directives. No regex required for basic use.
  • mod_rewrite: The full redirect engine. Uses RewriteRule and RewriteCond for 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]
  • pattern is a regex matched against the request path (without the leading slash in .htaccess context).
  • substitution is the target URL.
  • flags control 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.

Try Redirect Tracer

References

  1. Apache Software Foundation, "mod_rewrite documentation," https://httpd.apache.org/docs/current/mod/mod_rewrite.html
  2. Apache Software Foundation, "mod_alias documentation," https://httpd.apache.org/docs/current/mod/mod_alias.html
  3. Apache Software Foundation, ".htaccess files," https://httpd.apache.org/docs/current/howto/htaccess.html

Never miss a broken redirect

Trace redirect chains and detect issues before they affect your users and SEO. Free instant tracing.

Try Redirect Tracer