turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

Tool

htaccess rule generator

Tick what you need — HTTPS, clean URLs, file protection, caching, hotlink blocking — and copy the block.

Most .htaccess files are copied from an answer to a different question, which is why so many sites force HTTPS twice, cache their HTML for a year, or protect nothing at all.

Tick what you actually need and this writes the block, with the conditions in an order that works and a comment on each part explaining what it is for.

Every section is wrapped in an IfModule guard, so a host without mod_expires or mod_headers gets a working file rather than a 500 error.

Include

APACHE
Options -Indexes
DirectoryIndex index.php

<IfModule mod_rewrite.c>
    RewriteEngine On

    # Force HTTPS. Required outright on a .dev domain — the TLD is HSTS preloaded.
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

    # Generated so they carry this install's own domain
    RewriteRule ^robots\.txt$ robots.php [L]
    RewriteRule ^sitemap\.xml$ sitemap.php [L]

    # Clean URLs. QSA keeps any existing query string (pagination).
    RewriteRule ^p/([A-Za-z0-9\-]+)/?$ page.php?slug=$1 [L,QSA]

    # Anything not a real file or directory goes to the front controller.
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php?path=$1 [L,QSA]
</IfModule>

# Never serve the config, the schema or anything with credentials in it
<FilesMatch "^(config\.php|config\.sample\.php|\.env|composer\.(json|lock))$">
    Require all denied
</FilesMatch>
<IfModule mod_authz_core.c>
    <FilesMatch "\.(sql|log|ini|bak|old|md)$">
        Require all denied
    </FilesMatch>
</IfModule>

<IfModule mod_headers.c>
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
    # Only once every subdomain is HTTPS: browsers honour this after you remove it.
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</IfModule>

# Keep admin and internal endpoints out of search results
<IfModule mod_headers.c>
    <FilesMatch "^(cron|migrate)\.php$">
        Header set X-Robots-Tag "noindex, nofollow"
    </FilesMatch>
</IfModule>

<IfModule mod_expires.c>
    ExpiresActive On
    # Safe to cache for a year only if you version assets (?v=filemtime).
    ExpiresByType text/css "access plus 1 year"
    ExpiresByType application/javascript "access plus 1 year"
    ExpiresByType image/svg+xml "access plus 1 year"
    ExpiresByType image/png "access plus 6 months"
    ExpiresByType image/jpeg "access plus 6 months"
    ExpiresByType text/html "access plus 0 seconds"
</IfModule>

<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml
    AddOutputFilterByType DEFLATE application/javascript application/json application/xml
    AddOutputFilterByType DEFLATE image/svg+xml
</IfModule>

ErrorDocument 404 /404.php

Notes

Long asset caching only makes sense with versioned URLs. Without a ?v= on the filename, a year-long cache means your users keep last year's stylesheet.

HSTS is difficult to reverse. Browsers remember it for the full max-age even after you remove the header, so enable it only when every subdomain is on HTTPS.

An empty referer must be allowed in a hotlink rule. Direct visits, some privacy settings and several browsers all send nothing, and blocking those blocks real people.

Order matters: HTTPS and host redirects go before the rewrite rules, or you redirect after rewriting and the visitor gets an ugly URL back.

Other tools