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
.htaccessand<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.