It turns out that a WordPress multisite network served entirely through a CDN is tricky to get right. We tried it with Amazon CloudFront and a little bit of PHP.
Overview
We have a couple of sites that we use to support our family’s blogging needs. As part of our hosting infrastructure, we selected WordPress Multisite on private hosting as our CMS platform because WordPress has all the features that we need, private hosting lets us host multiple sites without paying for each one, and the Network / Multisite arrangement helps with the administration like updating plugins across the network. We needed help with the responsiveness of the site.
The first thing we wanted to do was publish all the content as static content, so the server isn’t involved in generating it. We tried using a plugin like WP Super Cache but this still involved serving pages to destinations around the world from our single cheap server. CDNs make sense for distributing content, so we turned to AWS CloudFront as a cheap CDN that will support several sites for us without incremental charges for SSL Certificates and DNS hosting.
We wanted to avoid a TTL approach, since it might be days or weeks between visits, and we want the first response to be speedy.
Design
We configured WordPress with a subdomain URL structure, like https://origin.bigbrainsr.us, and started building the content at that origin. Everything works like normal WordPress.
We configured a CloudFront distribution to deliver content on our primary site, https://bigbrainsr.us, sourced from the origin site, https://origin.bigbrainsr.us. One of the configurations that CloudFront passes two special headers to the origin: the public domain name and an authorization key.
We created a custom WordPress plugin that we can activate on the sites where we want to replace the traffic. This plugin takes care of business;
- If the visitor is coming from the “public” url – they will have the domain name and authorization key headers.
- Rewrite all output to replace references to the origin domain name with the public domain name.
- Add cache control headers for really long TTL.
- Block wp-admin.
- If the visitor is coming from the “internal” url – they will not have the domain name or authorization key headers.
- Redirect unauthenticated traffic to the login page.
- Allow authenticated traffic to the web site and wp-admin without involving the CDN.
- Add a button to the admin toolbar that invalidates the whole site on the CDN.
Challenges and Solutions
Most of the problems come from the network implementation of WordPress that relies on the WordPress home_url to not only deliver content but also identify which network site to deliver. We can’t use the native WordPress home_url to write the content since we have to use the native WordPress home_url to generate the content.
Login
Challenge: Prevent private values from being cached.
Solution: Use CloudFront behaviors to prevent caching on wp-login and wp-admin paths.
WordPress Headers
Challenge: WordPress sets two Link
HTTP headers based on the site URL that we can’t rewrite with either the send_headers
action or the wp_headers
filter.
Solution:
We correct one of the headers by rewriting the rest_url:
add_filter('rest_url', function($rest_url) { $fromHost = self::getHeader("Host"); $toHost = self::getHeader("RealHostName"); // this is a custom header set in our CloudFront "origin" configuration $rest_url = str_ireplace($fromHost, $toHost, $rest_url); return $rest_url; }, PHP_INT_MAX);
We remove the other header after WordPress init by unscheduling its action.
add_action('init', function() { remove_action( 'template_redirect', 'wp_shortlink_header', 11); });
Admin bar
The toolbar or admin bar is a floating bar that contains useful administration screen links such as add a new post, see pending comments, edit your profile etc.
Challenge: Prevent the admin bar from being cached.
Solution: Prevent access to admin through the CDN.
Comments
Challenge: Present comments & discussion without refreshing the static published content.
Solution: Deliver comments & discussion through Ajax with the wpDiscuz plugin so they are separate from the page content.
Forms
Challenge: Present forms without accidentally caching the content in a way that lets other users see it.
Solution: Still in progress. We will either use Ajax forms or put cache-control on the pages that should not be cached.
Sitemaps
Challenge: Our Yoast SEO Plugin redirects from the WordPress sitemap to their custom sitemap page. The way this was implemented broke the URL cloaking.
wp_safe_redirect( home_url( '/sitemap_index.xml' ), 301, 'Yoast SEO' );
Solution: Do our own redirection in the redirection plugin before it gets to the Yoast code.