Spring Sale! Save 30% on all books w/ code: PLANET24
Web Dev + WordPress + Security

WordPress .htaccess file

[ WordPress .htaccess file ] The WordPress core uses .htaccess for two things: Permalinks and Multisite. This means that .htaccess is only required if you have enabled either of these features. Otherwise, .htaccess is entirely optional for default WordPress installations.

Beyond the WP core, many plugins also use the .htaccess file for custom directives involving rewrites, redirects, custom headers, file compression, and much more. In many cases, such plugins add their .htaccess rules to your .htaccess file automatically, behind the scenes.

So even if you haven’t enabled Permalinks or Multisite, your site may be using .htaccess rules added by WordPress plugins for various types of functionality. That’s one of the cool things about .htaccess: it can be configured and customized to improve your site’s performance, security, and usability. To help you get started, this tutorial provides a collection of .htaccess techniques that are useful for any WordPress-powered site. Combined into a blank .htaccess file, these techniques serve as a great starting point for creating your own custom .htaccess file for WordPress.

Before diving in..

Before making any changes to your site’s .htaccess file(s), remember to make a backup copy. That way you can quickly restore original functionality should anything unexpected happen. These rules are all well-tested and commonly used, but in this line of work anything can happen, so it’s best to be prepared with good working backups just in case.

Disable directory views

With this technique, we increase security by disabling directory views, which also are known as “directory indexes” or “directory listings”. Many web hosts disable directory views by default, but it’s important to know for sure. If your files are visible, here is an effective way to lock things down.

# Disable directory views
Options -Indexes
<IfModule dir_module>
	DirectoryIndex disabled
	DirectoryIndex index.php
</IfModule>

Here we disable Indexes via the Options directive. Then we specify the DirectoryIndex just in case. So technically it’s fine to just do this:

# Disable directory views
Options -Indexes

I include the additional dir_module rules because they are related, and some people may find them useful. To learn more about customizing directory views, check out my tutorial, Better Default Directory Views with HTAccess.

Set default encoding

Setting your site’s default encoding as UTF-8 helps browsers and devices correctly interpret your page content.

# Set default encoding
AddDefaultCharset UTF-8

This directive helps to improve the accuracy and consistency of your web pages. It’s also possible to set the character encoding for specific file types. For example, here we are setting UTF-8 encoding for all CSS and JavaScript files:

# Set encoding for CSS & JS
<IfModule mod_mime.c>
	AddCharset utf-8 .html .css .js
</IfModule>

For more possibilities, check out the “Add mod_mime suport” section of my tutorial, Useful .htaccess rules for all websites.

Add Vary header

According to Google and others, publicly cacheable, compressible resources should include the following header:

Vary: Accept-Encoding

This is to prevent certain public proxy services from delivering compressed versions of your files to clients that do not support compression. Sending a Vary header for compressed resources helps resolve this issue by instructing the proxy to cache both compressed and uncompressed versions. To implement, add this code to your site’s root .htaccess file:

# Add Vary header
<IfModule mod_headers.c>
	<FilesMatch "\.(css|js|gz|xml)$">
		Header append Vary: Accept-Encoding
	</FilesMatch>
</IfModule>

This code sends the necessary Vary header for CSS, JavaScript, gzip, and XML files. If your site serves other compressed resources, you can add their respective file types to the list, like so:

\.(css|js|gz|xml|zip)$

Here we have added the ZIP file format, zip.

Add security headers

There are lots of useful headers that can be added to any website. For example, here are three headers that provide some great security benefits:

  • Protect against XSS attacks
  • Protect against page-framing and click-jacking
  • Protect against content-sniffing

Amazingly, you can add these security benefits with a simple slice of .htaccess:

# Add Security Headers
<IfModule mod_headers.c>
	Header set X-XSS-Protection "1; mode=block"
	Header always append X-Frame-Options SAMEORIGIN
	Header set X-Content-Type-Options nosniff
</IfModule>

No edits need made for this code. Simply add to root .htaccess and enjoy the extra protection. You can learn more about this technique by visiting my tutorial, Increase Security with X-Security Headers.

Protect sensitive files

By default, all WordPress installs include the following files:

  • wp-config.php – WP configuration file
  • license.txt – WP license information
  • readme.html – WP install & version information

Of these, wp-config.php should by default NOT be publicly accessible. The other two files, license.txt and readme.html, are by default publicly accessible. All three of these files contain information about your WordPress-powered site. By blocking public access to these files, you prevent bad actors from obtaining and using the information when crafting their attacks. Here is the code to make it happen:

# Protect sensitive files
<FilesMatch "^(wp-config.php|license.txt|readme.html)">
	Order Allow,Deny
	Deny from all
</FilesMatch>

This simple technique uses FilesMatch to target each of the files that should be denied access. You can protect any other loose or sensitive files by adding them to the FilesMatch directive.

Leverage browser caching

Browser caching plays an important role with all websites, especially WordPress. This technique ensures that your site is taking advantage of browser caching by setting optimal ExpiresByType HTTP headers.

# Leverage browser caching
<IfModule mod_expires.c>
	ExpiresActive on
	ExpiresDefault "access plus 1 month"
	#
	ExpiresByType image/jpg  "access plus 1 year"
	ExpiresByType image/jpeg "access plus 1 year"
	ExpiresByType image/gif  "access plus 1 year"
	ExpiresByType image/png  "access plus 1 year"
	ExpiresByType text/css   "access plus 1 month"
	#
	ExpiresByType text/javascript        "access plus 1 month"
	ExpiresByType application/javascript "access plus 1 month"
	ExpiresByType text/x-javascript      "access plus 1 month"
	#
	ExpiresByType image/x-icon "access plus 1 year"
</IfModule>

This code helps to optimize performance by setting expires headers for the most common file types. You can adjust the cache times to suit your needs, and add more file types as required. For more information, check out Google’s article, Leverage Browser Caching.

Enable file compression

File compression reduces the amount of data that is sent to the client, so it is a great way to improve performance. Fortunately, Apache enables compression via its Deflate module, which can be implemented with the following slab of .htaccess:

# Enable file compression
<IfModule mod_deflate.c>
	AddOutputFilter       DEFLATE css js
	AddOutputFilterByType DEFLATE image/svg+xml image/x-icon
	AddOutputFilterByType DEFLATE application/vnd.ms-fontobject application/x-font-ttf font/opentype
	AddOutputFilterByType DEFLATE application/javascript application/x-javascript text/javascript text/x-js
	AddOutputFilterByType DEFLATE text/html text/plain text/richtext text/css application/json text/xsd text/xsl
	AddOutputFilterByType DEFLATE text/xml text/x-component application/xml application/xhtml+xml application/rss+xml application/atom+xml
</IfModule>

This code first enables compression for all CSS and JavaScript files. It then enables compression for many other types of files. Feel free to customize the file formats that should be compressed. For more information, check out .htaccess enable compression.

Block external POST requests

POST requests are required when submitting certain forms. For example, the comment form, login form, and most contact forms all submit their data via the POST request method. Normally, POST requests are initiated by users who are visiting your site. But it’s also possible for bad actors to submit malicious POST requests from another domain or source, in an attempt to hack your stuff. To prevent this, we can deny access to all POST requests that are not initiated from your domain. This can be accomplished with the following slice of .htaccess:

# Block external POST
<IfModule mod_rewrite.c>
	RewriteCond %{REQUEST_METHOD} POST
	RewriteCond %{REQUEST_URI} (wp-comments-post|wp-login)\.php [NC]
	RewriteCond %{HTTP_REFERER} !(.*)example.com [NC,OR]
	RewriteCond %{HTTP_USER_AGENT} ^$
	RewriteRule .* - [L]
</IfModule>

Remember to change example.com to match your own domain. Once implemented correctly, this code does the following:

  1. Checks if the request is POST
  2. Checks if the request is for the comments form or the login form
  3. Checks if the request is not from your domain, or if the user agent is empty
  4. If these conditions are true, the request is denied via 403 “Forbidden” response

In doing this, the above technique helps to keep your site secure by blocking any POST requests that do not originate from your domain. This helps to reduce spam, brute-force login attacks, and lots of other malicious mischief.

Note that you can protect other POST forms as well. For example, if you have a contact form located at http://example.com/contact/, you can add it by modifying the REQUEST_URI directive like so:

RewriteCond %{REQUEST_URI} (wp-comments-post\.php|wp-login\.php|/contact/) [NC]

Remember to test well before going live with any changes. More info: Block Spam by Denying Access to No-Referrer Requests.

Enable WordPress permalinks

Last but not least, if you have enabled Permalinks for your site, you must include the required .htaccess directives. There are two possibilities here: WordPress installed in the site’s root directory, or WordPress installed in a sub-directory. Depending on where WordPress is installed, you will want to include one of the following sets of directives.

WordPress in site root directory

Here is the code to use if WordPress is installed in the site’s root directory:

# BEGIN WordPress
<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteBase /
	RewriteRule ^index\.php$ - [L]
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule . /index.php [L]
</IfModule>
# END WordPress

No modifications are required for this code.

WordPress in a subdirectory

Alternately, here is the code to use if WordPress is installed in it’s own (sub) directory (e.g., a subdirectory named /wordpress/):

# BEGIN WordPress
<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteBase /wordpress/
	RewriteRule ^index\.php$ - [L]
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule . /wordpress/index.php [L]
</IfModule>
# END WordPress

For this code, you will need to edit both instances of wordpress with the path/directory in which WordPress is installed. For example, here at Perishable Press, WordPress is installed in a directory named /wp/, so I replaced wordpress with wp.

For more information about either of these techniques, check out my tutorial, The htaccess Rules for all WordPress Permalinks.

Bonus tip!

As a bonus, you can add the following directive (just after the first RewriteRule) to automatically redirect all login requests to the actual WP Login Page:

RewriteRule ^login/?$ /wp-login.php [QSA,L]

For more information on this trick, check out Chris Coyier’s DigWP post, Simpler Login URL. This is a useful technique for client sites :)

Download ’em all

The techniques presented in this article are not dependent on each other. So you can use them separately, as desired; or you can combine them all into a single .htaccess file to create a solid .htaccess file for WordPress. To make things easier, here is a text file that contains all of the techniques from this tutorial:

Download WordPress .htaccess fileVersion 1.0 ( 2.54 KB TXT )

Going further

Intentionally, the techniques provided in this article are kept as general and as widely applicable as possible. There are thousands more useful techniques that could be added, but keeping it simple with only the essentials is optimal when first getting started. I encourage you to go further with your .htaccess file. Here are some recommended resources that are worth exploring.

Questions and comments always welcome :)

About the Author
Jeff Starr = Web Developer. Security Specialist. WordPress Buff.
.htaccess made easy: Improve site performance and security.

7 responses to “WordPress .htaccess file”

  1. Hi Jeff

    If i want to add your following code to more pages that contains form to more than just one page (for example contact + advertise + jobs what would the line should look like ?

    RewriteCond %{REQUEST_URI} (wp-comments-post\.php|wp-login\.php|/contact/) [NC]

    Thanks

    • Jeff Starr 2017/06/08 8:32 am

      If the patterns match the actual URLs, then yes that looks correct. Always make sure to test thoroughly before going live with any changes.

  2. That’s correct because that your line i took from this post as an example, what i’d like to know is, if i want to add more pages (i have more than one page with forms), do i need to write it this way (one line per page) :

    RewriteCond %{REQUEST_URI} (wp-comments-post\.php|wp-login\.php|/contact/) [NC]
    RewriteCond %{REQUEST_URI} (wp-comments-post\.php|wp-login\.php|/advertise/) [NC]
    RewriteCond %{REQUEST_URI} (wp-comments-post\.php|wp-login\.php|/jobs/) [NC]

    or do i need to write it on one line, if yes, what would be the structure ? Something like that ?

    RewriteCond %{REQUEST_URI} (wp-comments-post\.php|wp-login\.php|/contact/|/advertise/|/jobs/) [NC]

    Thank you

    • Jeff Starr 2017/06/08 9:20 am

      Either each on its own line, or combined into one line is fine, totally up to you. Please note that your first example is redundant, with each resource effectively listed three times. Your second example is correct. Also, if you want to post more code, please follow the rules and wrap each line with code tags. Thanks!

  3. Thank you Jeff

  4. My latest message was delete, that’s weird.

    I was asking you if – as we force comrpession in .htaccess with deflate – we must delete de gzip compression lines we might have added in wp-config previously and also in any plugin that would offer it or even force it by default ?

    Thanks

    • Jeff Starr 2017/06/12 7:56 am

      Hi Mike, sorry about that — I’ve been working with the database and had to restore from backup. Some comments were lost. Not sure about your question, but in general I would use either deflate OR gzip module, not both at the same time. I hope this helps.

Comments are closed for this post. Something to add? Let me know.
Welcome
Perishable Press is operated by Jeff Starr, a professional web developer and book author with two decades of experience. Here you will find posts about web development, WordPress, security, and more »
Blackhole Pro: Trap bad bots in a virtual black hole.
Thoughts
I live right next door to the absolute loudest car in town. And the owner loves to drive it.
8G Firewall now out of beta testing, ready for use on production sites.
It's all about that ad revenue baby.
Note to self: encrypting 500 GB of data on my iMac takes around 8 hours.
Getting back into things after a bit of a break. Currently 7° F outside. Chillz.
2024 is going to make 2020 look like a vacation. Prepare accordingly.
First snow of the year :)
Newsletter
Get news, updates, deals & tips via email.
Email kept private. Easy unsubscribe anytime.