mod_rewrite is Apache’s URL rewriting engine. It maps requested URLs to other URLs or files, using regular expressions and conditions — powering pretty URLs, redirects, canonical hostnames and access rules. Its syntax is compact, which makes it powerful and occasionally confusing.

Enable it:

sudo a2enmod rewrite
sudo systemctl restart apache2

RewriteRule

RewriteEngine On
RewriteRule Pattern Substitution [Flags]
  • Pattern — a regex matched against the URL path.
  • Substitution — the new URL or path (- means “don’t change”).
  • Flags — modify behaviour.
# /product/42 → /product.php?id=42 (internal rewrite; the browser URL stays)
RewriteRule ^product/([0-9]+)/?$ product.php?id=$1 [L,QSA]

# Redirect an old path permanently
RewriteRule ^old-blog/(.*)$ /blog/$1 [R=301,L]

$1, $2… refer to groups captured in the pattern.

Context matters: the leading slash

  • In .htaccess and <Directory> blocks, the pattern is matched without the leading slash and relative to the directory: ^product/….
  • In virtual host (server) context, the path includes the leading slash: ^/product/….

A rule copied from a vhost into .htaccess (or the reverse) silently stops matching — the most common mod_rewrite mistake.

RewriteCond

Conditions placed before a rule; the rule applies only if all conditions match (AND, unless [OR] is used):

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [L]

Useful variables:

Variable Contains
%{HTTP_HOST} Requested hostname
%{HTTPS} on / off
%{REQUEST_URI} Path (with leading slash)
%{QUERY_STRING} Query string
%{REQUEST_FILENAME} Full filesystem path
%{REMOTE_ADDR} Client IP
%{HTTP_USER_AGENT}, %{HTTP_REFERER} Headers
%{HTTP:X-Forwarded-Proto} Any request header

Tests: -f (is a file), -d (is a directory), ! (negate), regex match, = (exact string).

Groups captured in a RewriteCond are referenced as %1, %2:

RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]

Important flags

Flag Meaning
L Last — stop processing this round of rules
R=301 / R=302 External redirect with that status
NC Case-insensitive match
QSA Append the original query string to the substitution’s
QSD Discard the original query string
F Forbidden (403)
G Gone (410)
NE Don’t escape special characters (e.g. # anchors)
END Stop all rewriting, including later rounds (Apache 2.4)
OR (on RewriteCond) Combine conditions with OR

Why rules loop in .htaccess: after a rewrite in per-directory context, Apache re-runs the rules with the new URL. [L] only ends the current round. Guard against loops with a condition, or use [END].

Practical recipes

RewriteEngine On

# Force HTTPS (also works behind a proxy that sets X-Forwarded-Proto)
RewriteCond %{HTTPS} off
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

# Remove .php extension externally, serve internally
RewriteCond %{THE_REQUEST} \s/([^.]+)\.php[\s?] [NC]
RewriteRule ^ /%1 [R=301,L]
RewriteCond %{REQUEST_FILENAME}.php -f
RewriteRule ^(.+?)/?$ $1.php [L]

# Block bad bots by user agent
RewriteCond %{HTTP_USER_AGENT} (badbot|evilscraper) [NC]
RewriteRule ^ - [F]

# Maintenance mode except for your IP
RewriteCond %{REMOTE_ADDR} !^203\.0\.113\.10$
RewriteCond %{REQUEST_URI} !^/maintenance\.html$
RewriteRule ^ /maintenance.html [R=302,L]

Simpler alternatives

For plain redirects, Redirect and RedirectMatch (mod_alias) are clearer than mod_rewrite:

Redirect 301 /old /new

For “fall back to index.php”, Apache 2.4’s FallbackResource /index.php replaces the three-line rewrite block.

Debugging

Enable rewrite logging (Apache 2.4) in the virtual host, temporarily:

LogLevel alert rewrite:trace3

Then watch the error log:

sudo tail -f /var/log/apache2/error.log | grep rewrite

It shows each pattern tested and what happened. Turn it off afterwards — trace logging is verbose. Test redirects with curl -sIL URL.

Moving rules to Nginx

When migrating, translate rules into Nginx rewrite, return and try_files — see Nginx rewrite rules. Related: the .htaccess guide.

  • Apache
  • mod_rewrite
  • Redirects