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.
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.phpNotes
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.