Creating rules in .htaccess
Check your server first
The same .htaccess line can work on one host and answer 500 on every page of another. Before writing a rule, find out which server reads the file, how PHP runs and where TLS ends. A few short commands answer that. Tools used here: curl, SSH, grep.
The response headers name the server, but not the PHP mode:
curl -sSI https://example.com/
HTTP/2 200 content-type: text/html; charset=UTF-8 server: Apache
A probe file, opened in the document root, prints the PHP mode and the TLS variables. Give it a random name and delete it afterwards:
vi ~/example.com/probe-7f3a.php
<?php
echo "HTTPS=", var_export($_SERVER['HTTPS'] ?? null, true),
" XFP=", var_export($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null, true),
" SERVER_SOFTWARE=", $_SERVER['SERVER_SOFTWARE'] ?? '',
" sapi=", PHP_SAPI, "\n";
curl -sS https://example.com/probe-7f3a.php
On a FastCGI host (DreamHost) the probe answers:
HTTPS='on' XFP=NULL SERVER_SOFTWARE=Apache sapi=cgi-fcgi
On a server you control, list the Apache modules. apache2ctl -M needs root or sudo and a server you control; a shared host usually gives no such command. This is a local Docker php:8.5-apache server with mod_php:
apache2ctl -M | grep -i -E 'php|rewrite|alias|headers|version'
version_module (static) alias_module (shared) headers_module (shared) php_module (shared) rewrite_module (shared)
On a FastCGI host (DreamHost), PHP runs as a separate process:
pgrep -l -u user php
2745746 php85.cgi
The Apache binary is there too:
ls /usr/sbin/apache2*
/usr/sbin/apache2 /usr/sbin/apache2ctl
When a rule did break the site, the error log names the file and the directive:
tail -n 1 ~/logs/example.com/https/error.log
[Fri Nov 06 23:29:54 2026] [example.com] [alert] [client 203.0.113.7:37118] [pid 3064457] config.c(2045): /home/user/example.com/.htaccess: Invalid command 'php_flag', perhaps misspelled or defined by a module not included in the server configuration
sapi=cgi-fcgi with php85.cgi means PHP runs as a FastCGI process, not as mod_php: php_flag is unknown there and stops Apache with a 500. sapi=apache2handler means mod_php, where the same lines work. The full walk-through, the meaning of every answer and the safe test that .htaccess is read at all: Which server do you have.
Which rules are safe where
Read a row as: the directive group on the left, then what happens on each server. Safe means the rule works as written. Guard it means wrap it in an IfModule or If block, so a server without the module skips it instead of failing. Breaks the site means one unknown directive answers 500 for the folder and every folder below it. Ignored means the server never reads .htaccess.
| Directive group | Apache + mod_php | Apache + FastCGI | Apache behind a TLS proxy | nginx alone | LiteSpeed |
|---|---|---|---|---|---|
| Redirect / RedirectMatch | safe | safe | absolute https:// target only: a bare path adds an http hop | ignored | safe |
| RewriteRule | safe | safe | safe: build the scheme with an env variable | ignored | safe |
| php_flag / php_value | safe | breaks the site | safe: depends on the PHP mode | ignored | check the host documentation |
| Header | guard it: <IfModule mod_headers.c> | guard it: <IfModule mod_headers.c> | guard it: <IfModule mod_headers.c> | ignored: use add_header in the server block | safe |
| Options | safe | safe | safe | ignored | guard it: check the host documentation |
| AddHandler / SetHandler for PHP | guard it | guard it: do not point PHP at mod_php | guard it | ignored | guard it |
A directive that AllowOverride does not allow in that folder (Options, for example) also answers 500.
The FastCGI and PHP-FPM entries follow the PHP manual on .user.ini files: php_value in .htaccess belongs to the Apache module mode, while CGI and FastCGI read .user.ini. The LiteSpeed rewrite rules page says LiteSpeed reads Apache-style rewrite rules. The per-user PHP settings page covers PHPRC and PHP_INI_SCAN_DIR and says .user.ini is not recommended with LSWS; it does not say that LiteSpeed takes php_value from .htaccess, so check the host documentation for PHP settings. The guards and a rollback plan are in Safe rules, guards and rollback.
Redirect: simple moves
One page moved to a new address. mod_alias handles it in one line - source first, target second:
Redirect 301 /old/page.php /new/page.php
Use 301 for permanent moves (link equity passes, browsers cache it) and 302 while experimenting. A cached wrong 301 is painful: browsers remember it long after you fix the file, so test with 302 and flip to 301 when sure.
On a host where HTTPS ends at a front proxy, a bare path target sends visitors to http:// first. The safe forms are in Redirect 301 or the pair and Redirects behind an HTTPS proxy.
RedirectMatch: patterns
Plain Redirect matches a URL prefix, which glues the remainder onto the target. To retire a whole tree into one file, anchor a regular expression instead:
RedirectMatch 301 ^/edu/suomen/.*$ /edu/index.php
Without the regex, /edu/suomen/x.php would land on /edu/index.phpx.php. Exact single-address matches work the same way:
RedirectMatch 301 ^/robot/?$ /qa/robot/
The /?$ anchor means subpaths like /robot/basics/ are untouched - only the bare directory redirects.
RewriteRule basics
When the target depends on part of the source, mod_rewrite captures it with parentheses and replays it as $1, $2:
RewriteEngine On RewriteRule ^blog/([0-9]{4})/([a-z-]+)\.php$ /archive/$1/$2/ [R=301,L]
RewriteRule sees the path without the leading slash and without the query string. To keep query parameters, add the QSA flag. A condition above the rule sees the full %{REQUEST_URI}, leading slash included, and captures groups as %1.
Flags that matter
- R=301 / R=302 - external redirect and its status. Without R the rewrite is internal: the URL in the browser never changes.
- L (last) - stop processing further rules on a match. Almost every redirect rule wants it.
- NC (nocase) - case-insensitive match, handy for legacy mixed-case URLs.
- QSA (qsappend) - append the original query string to the target instead of dropping it.
- NE (noescape) - do not percent-encode the target, needed when it contains # anchors.
Order of rules
Apache tries rules top to bottom, and a prefix-hungry rule shadows everything below it. Specific rules go first, catch-alls last:
Redirect 301 /pc/ /latex/ Redirect 301 /pc/LaTeX.php /latex/man/
Apache takes the first matching line, and the prefix match wins over the more specific line below it. /pc/LaTeX.php matches the /pc/ prefix, so the visitor lands on /latex/LaTeX.php, not /latex/man/: put the specific line first.
Comment the ordering constraint in the file, or the next editor will reorder it back. mod_rewrite rules run before mod_alias rules whatever the line order; do not mix the two in one file (why).
Testing rules
Never test in a browser first - the 301 cache lies. Ask for headers:
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" https://example.com/robot/
301 https://example.com/qa/robot/
Check the chain, not just the first hop: two redirects in a row double the latency and leak ranking. One hop from old URL to final page is the goal.
Common pitfalls
- Prefix gluing: Redirect 301 /edu/suomen/ to a file appends the remainder. Use RedirectMatch with .*$ when retiring a tree.
- Redirect targets must exist. Audit them against the document root - a redirect to a missing page trades a 404 for a slower 404.
- Redirect fires even when a real file sits at the source path, so a stale rule can hijack a live page. Retire rules whose source came back to life.
- Trailing slashes: ^/robot/?$ covers both spellings; without ? one of them 404s.
- Chained redirects (/linux/virtualbox.php to /virtualbox/ to /dvps/virtualbox/) work but cost a round trip each - collapse to one hop when editing anyway.
- Behind a proxy that ends TLS, Redirect and RedirectMatch answer with an http:// Location. See Redirects behind an HTTPS proxy.
- An unknown directive, php_flag on a FastCGI host for example, answers 500 for every page below the file. Guard it or move the setting out of .htaccess: Safe rules.
- A host move changes the server under the same file. Read Moving a website: domain and hosting before the switch, and compare the old and the new host in Beget shared hosting, DreamHost shared hosting and the hosting-like Docker images. Which hosting to choose is the overview.
Keep the same discipline as with code: one rule per move, a comment above non-obvious ones, and a periodic audit of every target.
The old chapter about HTTPS proxies now has its own article: Redirects behind an HTTPS proxy.
The topic articles
- Which server do you have - response headers, a PHP probe, Apache modules, the PHP process, the error log and the safe test that .htaccess is read at all.
- Safe rules, guards and rollback - backup, test in a subfolder, IfModule and If guards, and where PHP settings go when php_flag cannot be used.
- Redirect 301 or the pair - when a one-line Redirect is right and when the RewriteCond plus RewriteRule pair is the safe choice.
- Redirects behind an HTTPS proxy - why a proxied Apache writes http:// and how this site converted every redirect to a mod_rewrite pair.
Article author: Andrei Olegovich