Tips for configuring WordPress environments

Many WordPress hosts will give your site a “staging” environment. You can also use tools like Local by Flywheel, or MAMP Pro to run a local “dev” version of your site. These are great ways of testing code changes, playing with new plugins, or making theme tweaks, without risking breaking your live “production” site.

Here is my advice for working with different WordPress environments.

Handling Credentials

The live (“production”) version of your site should be opt-in. That is, your site’s Git repo should not store production credentials in wp-config.php. You don’t want something to happen like when this developer accidentally connected to the production database and destroyed all the company data on his first day.

Instead of keeping database credentials in wp-config.php, have wp-config.php look for a local-config.php file. Replace the section that defines the database credentials with something like this:

if ( file_exists( __DIR__ . '/local-config.php' ) ) {
    include( __DIR__ . '/local-config.php' );
} else {
    die( 'local-config.php not found' );
}

Make sure you add local-config.php to your .gitignore so that no one commits their local version to the repo.

On production, you’ll create a local-config.php with production credentials. On staging or development environments, you’ll create a local-config.php with the credentials for those environments.

Production is a Choice

Right after the section that calls out local-config.php, put something like this:

if ( ! defined( 'WP_ENVIRONMENT' ) ) {
    define( 'WP_ENVIRONMENT', 'development' );
}

The idea here is that there will always be a WP_ENVIRONMENT constant available to you that tells you what kind of environment your site is being run in. In production, you will put this in local-config.php along with the database credentials:

define( 'WP_ENVIRONMENT', 'production' );

Now, in your theme, or your custom plugins, or other code, you can do things like this:

if ( 'production' === WP_ENVIRONMENT ) {
    add_filter( 'option_gravityformsaddon_gravityformsstripe_settings', function( $stripe_settings ) {
        $stripe_settings['api_mode'] = 'live';
        return $stripe_settings;
    });
} else {
    add_filter( 'option_gravityformsaddon_gravityformsstripe_settings', function( $stripe_settings ) {
        $stripe_settings['api_mode'] = 'test';
        return $stripe_settings;
    });
}

This bit of code is for the Easy Digital Downloads Stripe gateway plugin. It makes sure that on the production environment, the payment gateway is always in live mode, and the anywhere else, it is always in test mode. This protects against two very bad situations: connecting to live services from a test environment (which could result in customers being charged for test transactions) and connecting to test services from a live environment (which could prevent customers from purchasing products on your site).

You can also use this pattern to do things like hide Google Analytics on your test sites, or make sure debug plugins are only active on development sites (more on that, in a future post!)

Don’t rely on complicated procedures (“step 34: make sure you go into the Stripe settings and switch the site to test mode on your local test site”) — make these things explicit in code. Make it impossible to screw it up, and working on your sites will become faster and less stressful.


Do you need WordPress services?

Mark runs Covered Web Services which specializes in custom WordPress solutions with focuses on security, speed optimization, plugin development and customization, and complex migrations.

Please reach out to start a conversation!

How I fixed Yoast SEO sitemaps on a large WordPress site

One of my Covered Web Services clients recently came to me with a problem: Yoast SEO sitemaps were broken on their largest, highest-traffic WordPress site. Yoast SEO breaks your sitemap up into chunks. On this site, the individual chunks were loading, but the sitemap index (its “table of contents”) would not load, and was giving a timeout error. This prevented search engines from finding the individual sitemap chunks.

Sitemaps are really helpful for providing information to search engines about the content on your site, so fixing this issue was a high priority to the client! They were frustrated, and confused, because this was working just fine on their other sites.

Given that this site has over a decade of content, I figured that Yoast SEO’s dynamic generation of the sitemap was simply taking too long, and the server was giving up.

So I increased the site’s various timeout settings to 120 seconds.

No good.

I increased the timeout settings to 300 seconds. Five whole minutes!

Still no good.

This illustrates one of the problems that WordPress sites can face when they accumulate a lot of content: dynamic processes start to take longer. A process that takes a reasonable 5 seconds with 5,000 posts might take 100 seconds with 500,000 posts. I could have eventually made the Yoast SEO sitemap index work if I increased the timeout high enough, but that wouldn’t have been a good solution.

  1. It would have meant increasing the timeout settings irresponsibly high, leaving the server potentially open to abuse.
  2. Even though it is search engines, not people, who are requesting the sitemap, it is unreasonable to expect them to wait over 5 minutes for it to load. They’re likely to give up. They might even penalize the site in their rankings for being slow.

I needed the sitemap to be reliably generated without making the search engines wait.

When something intensive needs to happen reliably on a site, look to the command line.

The Solution

Yoast SEO doesn’t have WP-CLI (WordPress command line interface) commands, but that doesn’t matter — you can just use wp eval to run arbitrary WordPress PHP code.

After a little digging through the Yoast SEO code, I determined that this WP-CLI command would output the index sitemap:

wp eval '
$sm = new WPSEO_Sitemaps;
$sm->build_root_map();
$sm->output();
'

That took a good while to run on the command line, but that doesn’t matter, because I just set a cron job to run it once a day and save its output to a static file.

0 3 * * * cd /srv/www/example.com && /usr/local/bin/wp eval '$sm = new WPSEO_Sitemaps;$sm->build_root_map();$sm->output();' > /srv/www/example.com/wp-content/uploads/sitemap_index.xml

The final step that was needed was to modify a rewrite in the site’s Nginx config that would make the /sitemap_index.xml path point to the cron-created static file, instead of resolving to Yoast SEO’s dynamic generation URL.

location ~ ([^/]*)sitemap(.*).x(m|s)l$ {
    rewrite ^/sitemap.xml$ /sitemap_index.xml permanent;
    rewrite ^/([a-z]+)?-?sitemap.xsl$ /index.php?xsl=$1 last;
    rewrite ^/sitemap_index.xml$ /wp-content/uploads/sitemap_index.xml last;
    rewrite ^/([^/]+?)-sitemap([0-9]+)?.xml$ /index.php?sitemap=$1&sitemap_n=$2 last;
}

Now the sitemap index loads instantly (because it’s a static file), and is kept up-to-date with a reliable background process. The client is happy that they didn’t have to switch SEO plugins or install a separate sitemap plugin. Everything just works, thanks to a little bit of command line magic.

What other WordPress processes would benefit from this kind of approach?


Do you need WordPress services?

Mark runs Covered Web Services which specializes in custom WordPress solutions with focuses on security, speed optimization, plugin development and customization, and complex migrations.

Please reach out to start a conversation!

Tips for Hosting WordPress on Pantheon

Pantheon has long been hosting Drupal sites, and their entry into the WordPress hosting marketplace is quite welcome. For the most part, hosting WordPress sites on Pantheon is a dream for developers. Their command line tools and git-based development deployments, and automatic dev, test, live environments (with the ability to have multiple dev environments on some tiers) are powerful things. If you can justify the expense (and they’re not cheap), I would encourage you to check them out.

First, the good stuff:

Git-powered dev deployments

This is great. Just add their Git repo as a remote (you can still host your code on GitHub or Bitbucket or anywhere else you like), and deploying to dev is as simple as:

git push pantheon-dev master

Command-line deployment to test and live

Pantheon has a CLI tool called Terminus that can be used to issue commands to Pantheon (including giving you access to remote WP-CLI usage).

You can do stuff like deploy from dev to test:

terminus site deploy --site=YOURSITE --env=test --from=dev --cc

Or from test to live:

terminus site deploy --site=YOURSITE --env=live --from=test

Clear out Redis:

terminus site redis clear --site=YOURSITE --env=YOURENV

Clear out Varnish:

terminus site clear-caches --site=YOURSITE --env=YOURENV

Run WP-CLI commands:

terminus wp option get blogname --site=YOURSITE --env=YOURENV

Keep dev and test databases & uploads fresh

When you’re developing in dev or testing code in test before it goes to live, you’ll want to make sure things work with the latest live data. On Pantheon, you can just go to Workflow > Clone, and easily clone the database and uploads (called “files” on Pantheon) from live to test or dev, complete with rewriting of URLs as appropriate in the database.

No caching plugins

You can get rid of Batcache, W3 Total Cache, or WP Super Cache. You don’t need them. Pantheon caches pages outside of WordPress using Varnish. It just works (including invalidating URLs when you publish new content). But what if you want some control? Well, that’s easy. Just issue standard HTTP cache control headers, and Varnish will obey.

<?php

function my_pantheon_varnish_caching() {
	if ( is_user_logged_in() ) {
		return;
	}
	$age = false;

	// Home page: 30 minutes
	if ( is_home() && get_query_var( 'paged' ) < 2 ) {
		$age = 30;
	// Product pages: two hours
	} elseif ( function_exists( 'is_product' ) && is_product() ) {
		$age = 120;
	}

	if ( $age !== false ) {
		pantheon_varnish_max_age( $age );
	}
}

function pantheon_varnish_max_age( $minutes ) {
	$seconds = absint( $minutes ) * 60;
	header( 'Cache-Control: public, max-age=' . $seconds );
}

add_action( 'template_redirect', 'my_pantheon_varnish_caching' );

And now, some unclear stuff:

Special wp-config.php setup

Some things just aren’t very clear in Pantheon’s documentation, and using Redis for object caching is one of them. You’ll have to do a bit of work to set this up. First, you’ll want to download the wp-redis plugin and put its object-cache.php file into /wp-content/.

Update: apparently this next step is not needed!

Next, modify your wp-config.php with this:

// Redis
if ( isset( $_ENV['CACHE_HOST'] ) ) {
	$GLOBALS['redis_server'] = array(
		'host' => $_ENV['CACHE_HOST'],
		'port' => $_ENV['CACHE_PORT'],
		'auth' => $_ENV['CACHE_PASSWORD'],
	);
}

Boom. Now Redis is now automatically configured on all your environments!

Setting home and siteurl based on the HTTP Host header is also a nice trick for getting all your environments to play, but beware yes-www and no-www issues. So as to not break WordPress’ redirection between those variants, you should massage the Host to not be solidified as the one you don’t want:

// For non-www domains, remove leading www
$site_server = preg_replace( '#^www\.#', '', $_SERVER['HTTP_HOST'] );

// You're on your own for the yes-www version 🙂

// Set URLs
define( 'WP_HOME', 'http://'. $site_server );
define( 'WP_SITEURL', 'http://'. $site_server );

So, those environment variables are pretty cool, huh? There are more:

// Database
define( 'DB_NAME', $_ENV['DB_NAME'] );
define( 'DB_USER', $_ENV['DB_USER'] );
define( 'DB_PASSWORD', $_ENV['DB_PASSWORD'] );
define( 'DB_HOST', $_ENV['DB_HOST'] . ':' . $_ENV['DB_PORT'] );

// Keys
define( 'AUTH_KEY', $_ENV['AUTH_KEY'] );
define( 'SECURE_AUTH_KEY', $_ENV['SECURE_AUTH_KEY'] );
define( 'LOGGED_IN_KEY', $_ENV['LOGGED_IN_KEY'] );
define( 'NONCE_KEY', $_ENV['NONCE_KEY'] );

// Salts
define( 'AUTH_SALT', $_ENV['AUTH_SALT'] );
define( 'SECURE_AUTH_SALT', $_ENV['SECURE_AUTH_SALT'] );
define( 'LOGGED_IN_SALT', $_ENV['LOGGED_IN_SALT'] );
define( 'NONCE_SALT', $_ENV['NONCE_SALT'] );

That’s right — you don’t need to hardcode those values into your wp-config. Let Pantheon fill them in (appropriate for each environment) for you!

And now, some gotchas:

Lots of uploads = lots of problems

Pantheon has a distributed filesystem. This makes it trivial for them to scale your site up by adding more Linux containers. But their filesystem does not like directories with a lot of files. So, let’s consider the WordPress uploads folder. Usually this is partitioned by month. On Pantheon, if you start approaching 10,000 files in a directory, you’re going to have problems. Keep in mind that crops count towards this limit. So one upload with 9 crops is 10 files. 1000 uploads like that in a month and you’re in trouble. I would recommend splitting uploads by day instead, so the Pantheon filesystem isn’t strained. A plugin like this can help you do that.

Sometimes notices cause segfaults

I honestly don’t know what is going on here, but I’ve seen E_NOTICE errors cause PHP segfaults. Being segfaults, they produce no useful information in logs, and I’ve had to spend hours tracking down the code causing the issue. This happens reliably for given code paths, but I don’t have a reproducible example. It’s just weird. I have a ticket open with Pantheon about this. It’s something in their custom error handling. Until they get this fixed, I suggest doing something like this, in the first line of wp-config.php:

// Disable Pantheon's error handler, which causes segfaults
function disable_pantheon_error_handler() {
	// Does nothing
}

if ( isset( $_ENV['PANTHEON_ENVIRONMENT'] ) ) {
	set_error_handler( 'disable_pantheon_error_handler' );
}

This just sets a low level error handler that stops errors from bubbling up to PHP core, where the trouble likely lies. You can still use something like Debug Bar to show errors, or you could modify that blank error handler to write out to an error log file.

Have your own tips?

Do you have any tips for hosting WordPress on Pantheon? Let me know in the comments!