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.

Enable JavaScript to customise; default output below.

Pick force-https-proxy if the site sits behind Cloudflare or a load balancer: the connection to Apache is plain HTTP there, so the ordinary rule redirects forever.

Without www. The www variants are handled by the rule itself.

For a page or folder redirect, and for the file to deny. It goes into a regular expression, so a dot matches any character unless you escape it.

A path starting with a slash, or a full URL. For the domain redirect, the new domain without a scheme.

Status code

302 while testing, because a 301 is cached by the browser and is very hard to take back. 308 is the permanent one that preserves the request method.

Live preview .htaccess
# 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.

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

  1. Pick the rule. Use the proxy variant of force-HTTPS if anything sits in front of Apache.
  2. Fill in the domain and paths. Spaces and angle brackets are refused: a directive has no way to quote them.
  3. Paste it above the # BEGIN WordPress block, 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?
In .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?
302 while you are testing, because browsers cache 301s. 301 once you are sure, for anything permanent. 307 and 308 are the versions that preserve the request method, which matters for POST.
Why did my HTTPS redirect cause a loop?
Almost always a proxy terminating TLS. Use the 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?
Either, consistently. Pick one, redirect the other to it, and make sure your site URL setting matches. Serving both without a redirect is the only wrong answer.
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.

From the people who built this tool

WP Adminify

The WordPress admin, rebuilt: a dashboard worth looking at, menu and column control, a real file manager and the login page your client sees.

See WP Adminify Free version on WordPress.org

Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.