2026年11月12日
2026年11月12日
WordPress .htaccess: Default Rules, Redirects, Access Control, Caching
This article is a localized edition of the Japanese original. Read the Japanese original
How .htaccess and WordPress fit together
.htaccess is a per-directory configuration file that Apache reads. WordPress permalinks (URLs like /sample-post/) rely on a mod_rewrite rule that hands requests for non-existent files to index.php. If that rule is missing, or Apache is configured not to read .htaccess, every page except the front page returns a 404.
This article assumes Apache 2.4 (the series shipped by the major distributions as of October 2026). Nginx does not read .htaccess, so on Nginx the equivalent goes into the server configuration files.
The default WordPress .htaccess
For a single site installed at the web root, WordPress writes the following default rules (Basic WP in the official documentation).
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
What each line does:
| Line | Purpose |
|---|---|
RewriteRule .* - [E=HTTP_AUTHORIZATION:...] |
Passes the Authorization header to PHP (used, for example, for REST API authentication with application passwords) |
RewriteRule ^index\.php$ - [L] |
Serves index.php itself as is |
RewriteCond %{REQUEST_FILENAME} !-f / !-d |
Leaves existing files and directories (images, CSS and so on) untouched |
RewriteRule . /index.php [L] |
Sends everything else to index.php |
If WordPress lives in a subdirectory such as /blog/, the rules become RewriteBase /blog/ and RewriteRule . /blog/index.php [L].
Everything from # BEGIN WordPress to # END WordPress is managed by WordPress and gets overwritten, for example when you save the permalink settings. Put your own rules outside that block (usually above it).
Regenerating .htaccess
From the admin screen
Go to Settings → Permalinks and click Save Changes without changing anything; WordPress rewrites .htaccess. If the file is not writable, the screen shows the rules at the bottom so you can paste them into .htaccess by hand.
From WP-CLI
wp rewrite flush --hard
--hard also updates .htaccess and works only on single-site installs. To generate .htaccess from WP-CLI, you have to tell WP-CLI that mod_rewrite is available (official wp rewrite flush documentation). Put a wp-cli.yml in the WordPress root with the following:
apache_modules:
- mod_rewrite
Permissions
.htaccess is normally fine at 644. Giving the web server user write access lets WordPress update it automatically, but it also lets an intruder rewrite it. Keeping it read-only and applying the rules shown in the admin screen by hand works just as well.
ls -l /var/www/html/.htaccess
Making Apache read .htaccess
If a rule in .htaccess has no effect, check the server configuration first. Ubuntu's /etc/apache2/apache2.conf sets AllowOverride None for /var/www/, which makes Apache ignore .htaccess.
Add the following to the virtual host configuration (for example /etc/apache2/sites-available/000-default.conf):
<Directory /var/www/html>
AllowOverride All
</Directory>
Enable mod_rewrite, check the syntax, then reload the configuration.
sudo a2enmod rewrite
sudo apache2ctl configtest
sudo systemctl reload apache2
If, instead of AllowOverride All, you allow only the classes you need, these are the classes for the directives used in this article (from the Override field in the Apache manual):
| Directive | Required AllowOverride |
|---|---|
RewriteEngine / RewriteRule, Header |
FileInfo |
Require |
AuthConfig |
Options |
Options |
ExpiresActive / ExpiresByType |
Indexes |
On shared hosting you usually cannot change this setting, and .htaccess is normally enabled already.
Setting up redirects
Put redirects above # BEGIN WordPress. Mixing mod_rewrite with Redirect (mod_alias) makes the processing order hard to follow, so everything here uses RewriteRule. Replace example.com with your own domain.
<IfModule mod_rewrite.c>
RewriteEngine On
# www -> non-www (and to HTTPS in a single hop)
RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
# HTTP → HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# Moved page
RewriteRule ^old-page/?$ /new-page/ [R=301,L]
# Changed category slug (moves every URL under it)
RewriteRule ^category/old-cat/(.*)$ /category/new-cat/$1 [R=301,L]
</IfModule>
Things to watch:
- Set WordPress Address and Site Address under Settings → General to the same form as the redirect target (
https://, with or without www). If they disagree, the redirects loop. - When a load balancer or CDN terminates HTTPS, requests reach Apache over HTTP, so
%{HTTPS} offis always true and the redirect loops. In that case, do the HTTPS redirect at the CDN, or test a header that fits your setup (such asX-Forwarded-Proto). - Browsers cache 301s. Test with 302 first and switch to 301 once it works, so mistakes are easy to undo.
Restricting access
On Apache 2.4, use Require instead of Order / Deny from (the Order family is Apache 2.2 syntax and causes a 500 error without mod_access_compat).
# Deny direct access to wp-config.php
<Files wp-config.php>
Require all denied
</Files>
# Do not show directory listings
Options -Indexes
.htaccess itself is already protected from outside reads by the default configuration on Ubuntu and similar systems (<FilesMatch "^\.ht"> in apache2.conf).
Blocking xmlrpc.php
If you do not use XML-RPC, you can block it to remove one entry point for brute-force attacks. Do not block it if you use Jetpack features or external tools that publish through XML-RPC.
<Files xmlrpc.php>
Require all denied
</Files>
Stopping PHP execution in uploads
<Directory> cannot be used inside .htaccess (it is valid only in the server config and virtual hosts). To stop PHP in the uploads folder, create wp-content/uploads/.htaccess with the following:
<FilesMatch "\.(?i:php|phtml|phar)$">
Require all denied
</FilesMatch>
For details, see Blocking PHP execution in the uploads folder with .htaccess.
Preventing image hotlinking
This stops other sites from embedding your images. Requests with an empty referrer (opened directly, or from browsers configured not to send one) are still allowed.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^https?://(www\.)?example\.com/ [NC]
RewriteRule \.(jpe?g|png|gif|webp)$ - [F,NC]
</IfModule>
Images may also disappear from search engine image results and social media previews, so add allowed domains as needed.
Browser caching and compression
Setting cache lifetimes on static files reduces downloads on repeat visits. Check whether mod_expires and mod_deflate are enabled and enable them if not.
apache2ctl -M | grep -E 'expires|deflate|headers'
sudo a2enmod expires deflate
sudo systemctl reload apache2
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType image/jpeg "access plus 6 months"
ExpiresByType image/png "access plus 6 months"
ExpiresByType image/webp "access plus 6 months"
ExpiresByType image/svg+xml "access plus 6 months"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css
AddOutputFilterByType DEFLATE application/javascript application/json
AddOutputFilterByType DEFLATE application/xml image/svg+xml
</IfModule>
- HTML gets no lifetime, so browsers do not keep showing an old page after you update a post.
- WordPress loads theme and plugin CSS/JS with
?ver=, so a version bump fetches the new file. If you edit a theme directly without changing its version, use shorter lifetimes. - JPEG, PNG, WebP and WOFF2 are already compressed formats, so they are not passed through DEFLATE.
- Caching plugins sometimes write the same rules into
.htaccess. Check what is already there so you do not duplicate them.
Checking after a change
apache2ctl configtest checks the syntax of the server configuration files and does not inspect .htaccess. Mistakes in .htaccess show up as a 500 error when the page is requested.
Make a copy before you change anything.
cp /var/www/html/.htaccess /var/www/html/.htaccess.bak
After the change, check the result in the response headers.
# Redirect target and status
curl -sI http://www.example.com/ | grep -iE '^(HTTP|location)'
# Cache lifetime
curl -sI https://example.com/wp-includes/css/dist/block-library/style.min.css | grep -iE '^(cache-control|expires)'
# Should be denied (returns 403)
curl -sI https://example.com/wp-config.php | head -n 1
If you get a 500 error, look at the Apache error log.
sudo tail -n 50 /var/log/apache2/error.log
Invalid command '...', perhaps misspelled or defined by a module not included in the server configuration means the directive's module is not enabled or the directive is misspelled. not allowed here means the directive is not permitted by AllowOverride, or cannot be used in .htaccess at all (such as <Directory>). To roll back quickly, overwrite the file with the copy you made.
Summary
- The default WordPress rules sit between
# BEGIN WordPressand# END WordPress; put your own rules outside that block. - Regenerate it by saving Settings → Permalinks, or with
wp rewrite flush --hardafter addingapache_modulestowp-cli.yml. - If it has no effect, check
AllowOverrideand whether mod_rewrite is enabled. - Write access rules with Apache 2.4
Require, and do not use<Directory>in.htaccess. .htaccessmistakes are not caught byconfigtest, so make a copy before changing it and verify withcurl -Iand the error log.
For .htaccess on Apache in general, see also How to use .htaccess on Apache.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)