# .htaccess Cheatsheet
.htaccess is Apache's per-directory config override file — the standard way to control routing, headers, and caching on shared hosting where you can't touch the main server config. This sheet covers the directives used most, drawn from real, load-bearing rewrite rules.
| Directive | Purpose |
|---|---|
| `RewriteEngine On` | Turns on `mod_rewrite` for this scope — required before any `RewriteRule` |
| `RewriteCond` | A condition that must be true for the *next* `RewriteRule` to apply |
| `RewriteRule pattern target [flags]` | The actual rewrite/redirect, `pattern` is a regex against the URL path |
| `Header set/always set <Name> <Value>` | Sets a response header (`always` applies to error responses too) |
| `ErrorDocument <code> <path>` | Custom error page for a given status code |
| `Options -Indexes` | Disables Apache's automatic directory listing |
| `<IfModule mod_x.c> ... </IfModule>` | Only apply the block if that Apache module is loaded (safe on hosts without it) |
| Flag | Meaning |
|---|---|
| `[L]` | Last — stop processing further rules if this one matched |
| `[R]` / `[R=301]` / `[R=302]` | Issue an HTTP redirect with the given status (default 302) |
| `[QSA]` | Query String Append — preserve the original `?query=string` on the rewritten URL |
| `[NC]` | No Case — case-insensitive pattern match |
| `[F]` | Forbidden — return 403 instead of rewriting |
| `[E=VAR:value]` | Set an environment variable other directives/scripts can read |
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
The two conditions matter: without !-f/!-d, requests for real files/directories (images, CSS) would also get routed to index.php instead of served directly.
RewriteCond %{HTTP:Authorization} .+
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
On many shared-hosting PHP configurations, Apache strips the Authorization header from $_SERVER by default — this rule re-injects it as an environment variable PHP can read.
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Content-Security-Policy "default-src 'self'"
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
</IfModule>
Safe to set aggressively long max-age values only if your assets are cache-busted with a version/hash in the filename or query string — otherwise a deploy won't reach returning visitors until the cache expires.
RewriteRule ^(config|data|snapshots)(/|$) - [F,L]
RewriteEngine On is required per scope: a .htaccess in a subdirectory doesn't automatically inherit it from the parent — omit it and every RewriteRule below is silently ignored, not an error.[L] doesn't mean "stop everything": [L] stops the current pass through the ruleset, but Apache then restarts matching from the top against the new URL. This can cause unexpected loops if a later rule matches the rewritten URL again — add another RewriteCond to guard against it.Header set inside an <IfModule> block that never gets checked for a matching request path: if you scope Header directives inside a <FilesMatch> or <Directory> block, headers only apply to matching requests — a common cause of "why isn't my CSP header showing up" on some routes but not others.RewriteRule patterns as case-sensitive by default (add [NC] for case-insensitive matching); this can differ from local development on Windows/macOS where the filesystem itself is case-insensitive, masking the bug until deploy..htaccess is parsed on every request unless disabled: AllowOverride All (needed for .htaccess to work at all) has a real performance cost since Apache re-reads and re-parses the file per request — on high-traffic sites, moving these directives into the main vhost config (where supported) avoids that overhead entirely.location/proxy_pass syntax, if you're migrating off Apache.Content-Security-Policy string to drop straight into a Header set line.Header set directives can send.