Apache’s .htaccess file sits at the intersection of web server configuration and everyday development work. For developers who lack root access to the main server configuration — shared hosting environments, managed WordPress hosts, agency handoffs — it is often the only lever available for controlling how URLs behave. Used carefully, it is a capable tool. Used carelessly, it can silently degrade performance, create redirect loops, or break entire directories.
This reference covers the mechanics of .htaccess and mod_rewrite with working code patterns you can adapt directly.
What .htaccess Is (and What It Is Not)
An .htaccess file is a directory-level configuration override for Apache HTTP Server. Place one in any directory, and Apache applies its directives to requests for files within that directory and all subdirectories beneath it — unless a lower-level .htaccess file overrides specific settings.
The critical distinction is between .htaccess and the main server configuration file (httpd.conf or its equivalent on your distribution). Directives in httpd.conf are read once at server startup and cached. Directives in .htaccess are read on every single request that touches a file in that directory tree.
That per-request cost matters at scale. The Apache documentation on .htaccess is explicit: if you have access to httpd.conf, the preferred approach is always to place configuration there inside a <Directory> block and set AllowOverride None to disable .htaccess lookup entirely. For a high-traffic site on hardware you control, the difference is measurable.
For shared hosting or situations where httpd.conf is locked, .htaccess is a practical necessity. The patterns below will work in either context — the syntax is the same whether you place rules in .htaccess or a <Directory> block in the main config.
Enabling mod_rewrite
Before any rewrite rules can function, Apache’s mod_rewrite module must be loaded. On most shared hosts and modern Apache installations, it is already enabled. If you control the server, verify with:
apache2ctl -M | grep rewrite
If rewrite_module does not appear in the output, enable it on Debian/Ubuntu with:
sudo a2enmod rewrite
sudo systemctl restart apache2
Every .htaccess file that uses rewrite rules must begin with:
RewriteEngine On
Without that directive, all RewriteCond and RewriteRule lines are ignored.
The Three Core Directives
RewriteEngine
As above — this switch must be On. Place it once, at the top of your rewrite block.
RewriteCond
RewriteCond tests a condition before applying a rule. Multiple RewriteCond lines before a single RewriteRule are evaluated as a logical AND by default (all conditions must be true). Use [OR] at the end of a condition line to switch to logical OR for that pair.
The general syntax:
RewriteCond TestString CondPattern [Flags]
TestString is typically a server variable like %{HTTP_HOST}, %{REQUEST_URI}, or %{HTTPS}. CondPattern is a regular expression or a comparison expression. Common flags include [NC] (no case, case-insensitive matching) and [OR] (logical OR with the next condition).
RewriteRule
RewriteRule defines the actual URL transformation:
RewriteRule Pattern Substitution [Flags]
Pattern is a Perl-compatible regular expression matched against the requested URI path (without the leading slash in .htaccess context). Substitution is the target — either a new path, a full URL for redirects, or - to pass through without rewriting. Flags control behavior.
Essential Flags
| Flag | Meaning |
|---|---|
R=301 | Issue an HTTP 301 (permanent) redirect to the substitution URL. Use R=302 for temporary. |
L | Last rule — stop processing further rules if this one matches. |
NC | No case — pattern matching is case-insensitive. |
QSA | Query string append — preserve and append the original query string to the substitution. |
NE | No escape — do not encode special characters in the output. |
END | Stop processing all rewrite rules, including in parent directories (Apache 2.3.9+). |
[R=301,L] is the combination you will use most often for permanent redirects. [L] alone rewrites the URL internally without sending a redirect response to the browser.
Practical Patterns
Enforce HTTPS
This is the most common single-purpose .htaccess task. The pattern checks whether the connection is not already HTTPS and redirects:
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
The ^ pattern matches everything. %{HTTP_HOST} preserves the hostname (including any subdomain), and %{REQUEST_URI} preserves the full path and query string. Combined, this sends the client to the exact same URL on HTTPS.
If your server sits behind a load balancer or CDN that terminates SSL before it reaches Apache, %{HTTPS} may always read as off even on HTTPS requests. In that case, check the X-Forwarded-Proto header instead:
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Canonicalize www vs. Non-www
Choose one canonical form and redirect the other. Non-www is the more common modern choice for new projects, though either is correct as long as you pick one and are consistent with what you declare in HTML <link rel="canonical"> and your sitemap.
Remove www (redirect to non-www):
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]
The %1 backreference captures whatever matched the first group (.+) in the preceding RewriteCond — the hostname without the www. prefix. This approach is clean because it does not hardcode your domain name; the rule works on any hostname.
Add www (redirect to www):
RewriteEngine On
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteRule ^ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
Enforce Trailing Slash on Directories
URL consistency matters for duplicate content. If /about and /about/ serve the same page without a canonical signal, crawlers see two distinct URLs. A common pattern is to enforce a trailing slash on all paths that do not have a file extension:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !/$
RewriteRule ^ %{REQUEST_URI}/ [R=301,L]
The first condition (!-f) skips the rule if the request maps to an actual file — you do not want to redirect style.css to style.css/. The second condition (!/$) skips the rule if a trailing slash is already present. Everything else gets a 301 to the slash-suffixed form.
To do the reverse — strip trailing slashes — flip the conditions:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} /$
RewriteRule ^(.+)/$ /$1 [R=301,L]
Here, !-d skips the rule if the path is an actual directory (directories legitimately end in slashes). The (.+)/$ pattern captures the path without the trailing slash into $1.
Old URL to New URL: 301 Redirects
When a URL structure changes — a CMS migration, a content reorganization, removing date-based paths — you need explicit redirects for affected URLs. For a small number of redirects, Redirect (not mod_rewrite) is actually simpler:
Redirect 301 /old-page /new-page
Redirect 301 /blog/2019/my-post /articles/my-post
For pattern-based redirects across many URLs, mod_rewrite is the right tool. Say you are moving all URLs from /blog/YYYY/MM/slug to /articles/slug:
RewriteEngine On
RewriteRule ^blog/[0-9]{4}/[0-9]{2}/(.+)$ /articles/$1 [R=301,L]
The pattern [0-9]{4} matches a four-digit year, [0-9]{2} matches a two-digit month, and (.+) captures the slug into $1 for use in the substitution. The caret ^ anchors the match to the start of the path, and no trailing $ anchor on (.+) is needed because .+ is greedy and will consume to the end of the URI.
Preserve Query Strings Across Redirects
By default, when mod_rewrite issues a redirect to a full URL with R=301, the query string from the original request is not automatically appended. Add QSA to preserve it:
RewriteRule ^old-search$ /search [R=301,L,QSA]
A request to /old-search?q=apache now redirects to /search?q=apache instead of /search.
When you write an explicit URL in the substitution and the original request has query parameters you do not want passed through, omit QSA and include a ? at the end of the substitution to discard the query string:
RewriteRule ^old-page$ /new-page? [R=301,L]
The trailing ? signals Apache to use an empty query string, dropping whatever was in the original request.
Block Bad Bots by User-Agent
For high-volume scrapers or known bad actors, a user-agent block in .htaccess can reduce junk traffic from reaching your application. This is a blunt instrument — determined bots rotate user-agents — but it handles the lazy ones:
RewriteEngine On
RewriteCond %{HTTP_USER_AGENT} (SemrushBot|AhrefsBot|MJ12bot|DotBot) [NC]
RewriteRule .* - [F,L]
The [F] flag returns a 403 Forbidden response rather than a redirect. The - in the substitution position means “do nothing with the URL” — the flag handles the response. Note that blocking legitimate SEO crawlers like Ahrefs or Semrush will affect your own rank-tracking data if you use those tools; adjust the list to your actual needs.
For more surgical control over crawlers, the robots.txt file and server-level rate limiting are better-suited tools. .htaccess bot blocking is best reserved for crawlers actively causing problems.
Ordering and Redirect Chains
Rules in .htaccess are processed top to bottom. A single request can match multiple rules in sequence unless you use [L] to stop processing. Without [L], Apache continues evaluating subsequent rules against the rewritten URL — which can produce chains of rewrites or unintended behavior.
A common mistake is placing the HTTPS redirect after the www redirect. If the HTTPS rule fires first, the www rule needs to handle an already-HTTPS URL. The safer pattern is to combine both redirects into a single rule:
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
Or, more robustly, handle them separately but in the correct order — HTTPS first, then www — so that after the HTTPS redirect, the browser makes a new request that the www rule can then handle cleanly. Two sequential 301s is a minor cost on first visit; subsequent visits are cached.
Testing and Debugging
Before deploying rewrite rules in production, test them against htaccess tester tools or Apache’s own mod_rewrite logging. To enable verbose rewrite logging in httpd.conf (not .htaccess, as LogLevel cannot be set there):
LogLevel alert rewrite:trace3
Trace levels run from 1 (minimal) to 8 (everything). Level 3 shows each rule test and match result. Disable logging in production — the output volume is substantial.
For the performance concerns covered in web fonts loading decisions and other latency-sensitive configurations, the same principle applies here: fewer .htaccess files in fewer directories reduce the number of filesystem reads Apache must perform per request. Consolidate rules into a single .htaccess at the web root where possible, and wherever server access exists, migrate production rules into httpd.conf.
A Note on RewriteBase
When .htaccess is used in a subdirectory — say, a WordPress installation at example.com/blog/ — relative substitution paths in RewriteRule can behave unexpectedly. RewriteBase sets the base URL path that mod_rewrite prepends to relative substitutions:
RewriteEngine On
RewriteBase /blog/
RewriteRule ^old-post$ new-post [L]
Without RewriteBase /blog/, the substitution new-post might resolve relative to the server root rather than the subdirectory. If your rules work in the web root but break in a subdirectory, RewriteBase is usually the fix.
WordPress generates a RewriteBase directive automatically in its default .htaccess template; inspect it before adding custom rules above or below the WordPress block to avoid conflicts.

