Mod Rewrite Rule Generator
htaccess rewrite rules for redirects, HTTPS and www, with the proxy case that makes the usual HTTPS rule loop and a reminder to test with 302.
# Rewrite rules for Apache, generated on swiftplugins.pro.
# Goes above the "# BEGIN WordPress" block in .htaccess, because WordPress rewrites
# everything that reaches it and a rule below that block will never run.
<IfModule mod_rewrite.c>
RewriteEngine On
# Everything over HTTPS. Behind a load balancer or Cloudflare the connection to Apache is
# often plain HTTP, so %{HTTPS} is off and this loops: use the X-Forwarded-Proto rule instead.
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=302,L]
</IfModule>
# Test with 302 first. A 301 is cached by the browser, sometimes permanently, so a wrong one
# is very hard to take back: visitors who saw it keep following it after you delete the rule.
# Check with: curl -sI https://example.com/old-page | head -n 20
Output is valid and updates as you type.
Fix the highlighted fields to update the output.
Two things about rewrite rules cause most of the damage.
A 301 is cached by the browser, sometimes for as long as the browser feels like. Ship a wrong one and the visitors who saw it keep following it after you delete the rule, and there is nothing you can do about it from the server. Test with a 302, confirm it with curl, then change the number.
And the ordinary force-HTTPS rule loops forever behind Cloudflare or a load balancer. The connection from
the proxy to Apache is plain HTTP, so %{HTTPS} is off, so Apache redirects to HTTPS, so the proxy sends
another plain HTTP request. The fix is to test X-Forwarded-Proto instead, and it is a separate option
here for that reason.
How to use
- Pick the rule. Use the proxy variant of force-HTTPS if anything sits in front of Apache.
- Fill in the domain and paths. Spaces and angle brackets are refused: a directive has no way to quote them.
- Paste it above the
# BEGIN WordPressblock, keep the status at 302 until you have tested, then change it.
Example
Forcing HTTPS, with the test command:
<IfModule mod_rewrite.c>
RewriteEngine On
# Everything over HTTPS. Behind a load balancer or Cloudflare the connection to Apache is
# often plain HTTP, so %{HTTPS} is off and this loops: use the X-Forwarded-Proto rule instead.
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=302,L]
</IfModule>
# Check with: curl -sI https://example.com/old-page | head -n 20
A folder and everything under it, permanently:
RewriteRule ^blog/2019/(.*)$ /archive/2019/$1 [R=301,L]
Pitfalls
Position matters more than the rule. WordPress’s own block rewrites every request that reaches it to
index.php, so anything you add below # BEGIN WordPress never runs. Put custom rules above it, and
outside the markers, because WordPress rewrites what is between them.
A 301 is close to permanent. Browsers cache it aggressively and some do so until the profile is cleared. Use 302 while testing. When you are sure, 301 for a moved page and 308 if the request method must survive, which matters for a POST endpoint.
Anchor your patterns. ^old-page$ matches that path exactly; old-page matches anything containing
it, including /blog/old-page-2. The missing ^ and $ is the commonest reason a redirect catches more
than it should.
A dot in a regular expression matches any character. ^file.pdf$ also matches fileXpdf. Escape it
as ^file\.pdf$ when you mean a literal dot.
Do not mix mod_rewrite and mod_alias. Redirect and RedirectMatch come from mod_alias and are
evaluated separately from RewriteRule, and combining them on the same path produces behaviour that
depends on which module ran first. Pick one.
Hotlink protection allows an empty referer on purpose. A direct visit, a privacy-conscious browser and some corporate proxies all send none, so blocking empty referers blocks real people. It also means the protection is trivially bypassed, which is the honest limit of the technique.
None of this works on nginx. There is no .htaccess: the equivalent rules live in the server
configuration as return 301 and rewrite directives and need a reload. If your host is nginx, or LiteSpeed
without htaccess support, this file is ignored silently.
Redirect chains cost you. A → B → C works and wastes a round trip each time, and search engines follow a limited number. When you add a redirect, check whether the target is itself redirected.
Compatibility
Everything runs in the browser: nothing is uploaded and nothing is stored.
The rules are wrapped in <IfModule mod_rewrite.c> so that a server without the module ignores them rather
than returning a 500, which is what an unrecognised directive does in .htaccess.
Path fields refuse spaces, angle brackets and double quotes. That is not cosmetic: a config file has no
escaping mechanism, so a </IfModule> in a path would close the block early and a space would split a
directive into the wrong number of arguments. The tool refuses the input instead of emitting something
broken, and the test suite includes a path full of regular-expression metacharacters to show what does get
through.
Apache 2.4 syntax is used throughout, including Require all denied rather than the 2.2 Order/Deny
pair. On Apache 2.2, which is long out of support, the deny rule needs the old form.
.htaccess is read on every request, so rules here are slower than the same rules in the virtual host
configuration. For a handful of redirects the difference is not measurable; for hundreds it is, and they
belong in the server config or in a redirect plugin with a database index.
Frequently asked questions
Where exactly do I put this?
.htaccess in the web root, above the # BEGIN WordPress line. Never between the WordPress markers:
those are rewritten when permalinks are saved.301 or 302?
Why did my HTTPS redirect cause a loop?
X-Forwarded-Proto variant. If you are on Cloudflare, also
check the SSL mode is Full rather than Flexible, which causes the same loop from the other end.Should I redirect www or to www?
How do I test without breaking the site?
curl -sI shows the status and Location header without following it. Keep a copy of the working
.htaccess before editing: a syntax error there takes the whole site down with a 500, and the only fix is
replacing the file.