WordPress Best Practices
A list of best practices for developing WordPress sites on Pantheon.
This section provides suggestions for best practices to develop and manage WordPress sites on the Pantheon platform.
Development
Use an IDE, or a text editor designed for development like Atom, Sublime Text, Brackets, CodeLobster, or Visual Studio Code.
Do not modify core WordPress files. Core file modification frequently causes unintended issues, and can prevent you from updating your site regularly. Create a custom or Must Use plugin, which adheres to the WP.org Plugin best practices if you need to modify any WP functionality.
Use Object Cache Pro. Redis is an open-source, networked, in-memory, key-value data store that can be used as a drop-in caching backend for your WordPress site. Redis on Pantheon makes it easy to cache a large number database queries in WordPress.
Use wp-cfm. wp-cfm lets you store settings from the
wp_options
table in Git and pull it into the database. This helps with the option-heavy nature of WordPress site configurations. This is true for all WordPress sites, but especially helpful on Pantheon where you have at least three environments you will need to reconfigure every time.Use Grunt or Gulp to aggregate JS/CSS on your local development environment rather than relying on the server to do it for you. This helps speed up your workflow by minimizing redundant tasks.
Follow the WordPress Coding Standards when developing custom plugins or themes for efficiency and ease of collaboration.
Plugins
Add Composer and pull your WordPress plugins from wpackagist.org. WordPress Packagist mirrors the WordPress.org plugin repository and adds a
composer.json
file to your files. This makes future debugging simpler if you need to switch between multiple plugin or WordPress versions to see what caused something to break. Runningcomposer install
on the environments is not supported (just as Git submodules are not supported). You must commit the dependencies that Composer downloads on Pantheon to workaround this even though committing Composer dependencies is generally not recommended.If you have a custom plugin that retrieves a specific post (or posts), use the
get_post()
function instead of usingwp_query()
.wp_query
can be useful in some situations, however, the get_post function is built specifically to retrieve a WordPress Post object.Don't use plugins that create files vital to your site logic that you aren't willing to track in Git. Sometimes these files are dumped in uploads, sometimes not, and you'll likely have difficulty trying to figure it out later. Many plugins for uploads rely on
.htaccess
files which Pantheon does not support.
Themes
Use a simple PHP
include()
instead of WordPress's get_template_part() in your theme. The overhead is heavy if your use case is simply adding in another sub-template file. For example:<?php get_template_part('content', 'sidebar'); ?> <?php include('content-sidebar.php'); ?>
Manage License Keys for Themes or Plugins
There are many plugins and themes in WordPress that require license keys. It is best practice to associate the license key in a domain. You can easily update and deploy the updates to Test and Live environments because Dev and Multidev are the only writable environments in SFTP mode.
Testing
Run Launch Check to review errors and get recommendations on your site's configurations.
Automate testing with Behat. Adding automated testing into your development workflow will help you deliver higher quality WordPress sites.
Live
Use HTTPS. Refer to HTTPS on Pantheon's Global CDN for more information.
Verify that Global CDN caching works on your site.
Follow our Frontend Performance guide to tune your WordPress site.
Disable Anonymous Access to the WordPress REST API
Option 1: Block the entire WordPress REST API
The WordPress REST API is enabled for all users by default. You can disable the WordPress REST API for anonymous requests to improve security and avoid exposing admin users. This action improves site safety and reduces unexpected errors that can result in compromised WordPress core functionalities.
The following function ensures that anonymous access to your site's REST API is disabled and that only authenticated requests will work. You can add this code sample to a theme's functions.php
file or to a must-use plugin:
// Disable WP Users REST API for non-authenticated users (allows anyone to see username list at /wp-json/wp/v2/users)
add_filter( 'rest_authentication_errors', function( $result ) {
if ( true === $result || is_wp_error( $result ) ) {
return $result;
}
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_not_logged_in',
__( 'You are not currently logged in.' ),
array( 'status' => 401 )
);
}
return $result;
});
Option 2: Block only the /users
WordPress REST endpoint
If blocking the entire REST API is not feasible for your site, you can choose a more selective approach. The WordPress REST API exposes the complete users list at the /wp-json/wp/v2/users
endpoint. This is by design -- the /users
endpoint contains data that is public elsewhere on your site and availalbe in other public places in WordPress, notably the HTML output and RSS feeds including name, avatar, etc. These endpoints are public so that the data to view and render content from elsewhere in the REST API is available. For example, since a post links to the author user, making user information easily accessible makes it simpler to access from API tools and integrations.
However, in many cases, exposing the /user
endpoint is seen as a vulnerability in penetration testing. Additionally, if your site uses email addresses as usernames, it could be exposing every email address of a user that has a published post on the site. You can disable access to /wp-json/wp/v2/users
with the following filter:
function restrict_user_endpoints( $access ) {
if ( ! is_user_logged_in() || ! current_user_can( 'list_users' ) ) {
$requested_route = $_SERVER['REQUEST_URI'];
if ( strpos( $requested_route, '/wp/v2/users' ) !== false ) {
return new WP_Error( 'rest_forbidden', 'Sorry, you are not allowed to do that.', array( 'status' => 403 ) );
}
}
return $access;
}
add_filter( 'rest_authentication_errors', 'restrict_user_endpoints' );
This filter checks if a user is logged in and if they have access to the list_users
capability in the WordPress admin. If neither is true, it returns a REST error and blocks access to /users
and descendent endpoints (e.g. /wp-json/wp/v2/users/1
for the user with an ID of 1 in the database) while still allowing access to those endpoints for logged-in users.
For more information about WordPress user roles and capabilities, refer to the Roles and Capabilities documentation on WordPress.org.
Security Headers
Pantheon's Nginx configuration cannot be modified to add security headers, and many solutions (including plugins) written about security headers for WordPress involve modifying the .htaccess
file for Apache-based platforms.
There are plugins for WordPress that do not require .htaccess
to set security headers, but header specifications may change more rapidly than the plugins can keep up with. In those cases, you may want to define the headers yourself.
You can add code like the example below in a plugin (or mu-plugin) to help add security headers for WordPress sites on Pantheon, or any other Nginx-based platform. Do not add this to your theme's functions.php
file, as it will not be executed for calls to the REST API.
The code below is only an example to get you started. You must modify the code to match your needs, especially the Content Security Policy. Tools like SecurityHeaders.com can help to check your security headers, and link to additional information on how to improve your security header profile.
function additional_securityheaders( $headers ) {
if ( ! is_admin() ) {
$headers['Referrer-Policy'] = 'no-referrer-when-downgrade'; //This is the default value, the same as if it were not set.
$headers['X-Content-Type-Options'] = 'nosniff';
$headers['X-XSS-Protection'] = '1; mode=block';
$headers['Permissions-Policy'] = 'geolocation=(self "https://example.com") microphone=() camera=()';
$headers['Content-Security-Policy'] = "script-src 'self'";
$headers['X-Frame-Options'] = 'SAMEORIGIN';
}
return $headers;
}
add_filter( 'wp_headers', 'additional_securityheaders' );
Note: The headers are applied by PHP code when WordPress is invoked. This means that headers will not be added when directly accessing assets like https://example.com/wp-content/uploads/2020/01/sample.json
.