— Article — № 001

001 —WordPress

Legacy WordPress .htaccess: the rules that fix 80%

The .htaccess rules that quietly resolve most of the redirect loops, mixed content and broken pretty-permalinks on inherited WordPress sites.

A brass key on an ivory paper card beside a clay-red wax seal, a leather rule book and a caliper on a bone workbench.
Hero · staged still№ 001

You've inherited a WordPress install from 2017. The previous developer is unreachable, the staging URL still leaks into canonical tags, and half the uploads return 403 behind Cloudflare. Before touching PHP, open .htaccess. Nine times out of ten the fix lives there.

What follows is the short list we reach for first on a legacy site audit. Each rule is copy-paste ready. Drop them inside the <IfModule mod_rewrite.c> block, above the stock WordPress rewrite stanza — Apache reads top to bottom and stops at the first match, so ordering matters.

Force HTTPS without looping behind a proxy

The classic RewriteCond %{HTTPS} off works on a bare server. Put the site behind Cloudflare, a load balancer or any reverse proxy that terminates TLS, and the server sees plain HTTP forever. You get a redirect loop and a support ticket by lunch.

Check the forwarded header instead:

RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

Both conditions must fail before we redirect, so a proxy that sets X-Forwarded-Proto: https is trusted and the loop dies. Keep the HTTPS off check for the case where the site is accessed directly on port 80.

Canonicalise the host

Legacy sites accumulate aliases: www and non-www, the old staging hostname, sometimes the server's raw IP. Google indexes whichever it finds first. Pick one and 301 the rest.

RewriteCond %{HTTP_HOST} !^example\.com$ [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

Replace example.com with your canonical host. This single rule also quietly kills the "our staging site is in Google" problem, because the staging hostname now redirects to production — assuming staging shares the same .htaccess, which it usually does when someone rsync'd the files two years ago and forgot.

Close xmlrpc.php

Not a rewrite strictly, but it belongs in the same file. xmlrpc.php is the single most-abused endpoint on legacy WordPress — pingback amplification, brute-force login via system.multicall. If you don't use the Jetpack app or the WordPress mobile app, close it.

<Files xmlrpc.php>
    Require all denied
</Files>

Watch your access logs for a day first. If something legitimate is hitting it, whitelist that IP with Require ip 203.0.113.0/24 instead of leaving the door open.

Fix pretty permalinks after a path change

Move WordPress into a subdirectory, or out of one, and pretty permalinks break in a way that's hard to diagnose because the homepage still works. The symptom: / loads, /about/ returns 404 from Apache, not WordPress. The fix is the RewriteBase directive and the fallback to index.php.

RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

If WordPress lives in /blog/, set RewriteBase /blog/ and rewrite to /blog/index.php. The two RewriteCond lines are the whole trick: send the request to index.php only if it isn't a real file or directory. Skip them and you'll start serving PHP for every image request, which is fun for nobody.

Redirect old permalink structures without a plugin

Inherited sites frequently carry two generations of permalink structure. The 2014 version used /?p=123, the 2018 redesign moved to /%postname%/, and the redirect plugin that bridged them was deactivated during a security audit in 2021. Now half your backlinks 404.

For the common date-based to postname migration:

RewriteRule ^([0-9]{4})/([0-9]{2})/([0-9]{2})/([^/]+)/?$ /$4/ [R=301,L]

For category-prefixed URLs where the category segment is now gone:

RewriteRule ^category/(news|blog|press)/(.+)$ /$2 [R=301,L]

Test with curl -I before you commit. Apache's regex engine is greedy, and the wrong anchor character will redirect your admin URLs into the void.

curl -I https://example.com/2019/03/14/some-post/
# HTTP/2 301
# location: https://example.com/some-post/

Protect wp-config.php and the uploads directory

Two last rules that earn their keep. The first denies direct access to wp-config.php, which Apache serves fine if PHP processing is ever misconfigured — and on shared hosting, it gets misconfigured. The second blocks PHP execution inside wp-content/uploads, which is the standard exfiltration route for plugin vulnerabilities.

<Files wp-config.php>
    Require all denied
</Files>

# In wp-content/uploads/.htaccess
<FilesMatch "\.(php|phtml|phar|php7|php8)$">
    Require all denied
</FilesMatch>

The uploads rule goes in a separate .htaccess inside wp-content/uploads/, not the root. Apache merges directory-level configs at request time; keep the scope tight so you don't accidentally break a legitimate endpoint somewhere else.

Deploying without fear

Rewrites work perfectly in your editor and break production in a way that takes the admin panel with them. Keep a timestamped backup before every edit (cp .htaccess .htaccess.$(date +%s)). Validate syntax with apachectl configtest if you have shell access, or httpd -t on some distributions. And test one rule at a time — if three go in together and the site 500s, you're bisecting by hand at 11pm.

When we built Pier we kept running into the same pattern — one edit to .htaccess, site down, no easy rollback over SFTP. The way we ended up handling it was a full version history of every file touched through the app, so reverting a bad rewrite is one keystroke rather than a scramble through backups.

If you do one thing today: open your production .htaccess, check whether the HTTPS redirect uses X-Forwarded-Proto, and if it doesn't, fix it before the next TLS-terminating proxy goes in front of your site.

— Questions —

Where exactly do these rules go in .htaccess?

Inside the <IfModule mod_rewrite.c> block, above the WordPress-generated stanza marked by the # BEGIN WordPress comment. Apache processes top-down and stops at the first match.

Will these rules work on Nginx?

No. Nginx ignores .htaccess entirely. The equivalents belong in your server block as return 301 and location directives. The logic translates, the syntax does not.

Why use X-Forwarded-Proto instead of just HTTPS off?

Behind a reverse proxy that terminates TLS, the origin server sees HTTP on every request. Checking only HTTPS off creates an infinite redirect loop. The forwarded header reflects the real client protocol.

Is disabling xmlrpc.php safe?

Safe for most sites. It breaks the WordPress mobile app, Jetpack's remote features, and some trackback functionality. Check access logs for a day before denying it outright.

How do I test a rewrite rule without breaking production?

Copy the rule to a staging site first, then use curl -I to inspect the response headers. Never paste untested rewrites into a live .htaccess — a bad anchor can take the admin panel offline.