Note about HTTP/2
Before we get started, some introduction. With the prevalence of HTTP/2 traffic in 2018, this technique becomes controversial. Take note of the following about separate domain for static file:
- It will reduce the data payload sent to server in HTTP/1 connections, thus network latency will be improved
- HTTP/2 clients represent the majority now
- A separate domain for static will result in an extra DNS lookup
- You have to use single SSL certificate (or a certificate that lists both domains) for both main and static in order to benefit from connection coalescing
- HTTP/2 features headers compressions and doesn’t need this technique that much, but you may want to use this technique for other reasons, see below.
I would say you should use cookie-less/separate domain for static if you plan to use a CDN and any of:
- The CDN you want to use introduces a too high TTFB increase for uncached pages. It means you should not put CDN against the main domain / PHP pages, and it’s better to have requests hit the uncached pages directly.
- The CDN you want to use simply doesn’t allow to route all requests through it. Cloudflare can serve everything through its servers, but KeyCDN requires a subdomain to be set up.
Either way, you have to be aware that this technique will require extra DNS lookup and reduced HTTP/2 request multiplexing, if coalescing is not available. Reduced multiplexing will happen:
- For Safari browsers. Safari simply doesn’t do request multiplexing between different hostnames
- If certificates are different between main and static domains (e.g. when using CDN or other cases when certificates are simply not the same, as in “DNS and certs do not agree”).
Cookieless domain for static WordPress files ensures better cacheability of your website. It’s quite simple to understand how this works on example:
- Your website will operate on www.example.com subdomain. WordPress will set cookie on that level and not for domain in whole.
- Your asset files (Javascript, images, CSS files) will be hosted at static.example.com subdomain. This way, WordPress cookies will not apply to those files.
Using cookieless domains makes the static files to be better cached by browsers and content delivery networks. The obvious requirement is that your WordPress site URL should be configured with www prefix, i.e.: https://www.example.com would be your Site URL.
Nginx changes
You will need to configure server block for static subdomain of your website. Then simply point its document root to wp-content
directory of your WordPress installation. The following boilerplate configuration is a good starting point. Adjust as necessary:
server {
listen 8080;
listen [::]:8080;
server_name static.example.com;
root /var/www/html/blog/wp-content;
# Disallow access to any PHP files. We only serve static files here
location ~ \.php$ {
return 403;
}
etag off;
add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
add_header Cache-Control "public, max-age=315360000";
# Allow WordPress to access fonts and other assets from the static subdomain
location ~* \.(eot|otf|ttf|woff|woff2|cur|gif|ico|jpg|jpeg||png|svgz|webp)$ {
add_header Access-Control-Allow-Origin "https://www.example.com";
add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT";
add_header Cache-Control "public, max-age=315360000";
}
}
Note that we have disabled ETag generation for static files. There’s really nothing wrong with ETags in nginx. Nginx doesn’t include inodes in ETag value, only file size and mod time, so it is safe in multi-server environments.
However, we chose to validate static files by their Last-Modified
header value instead.
We set the expiration time to maximum. There, we’ve also addressed best practices of far future Expires
header. And we simply reduced our HTTP header size by eliminating ETags from response.
Someone might wonder why we’re relying on add_header
directive when we could use expires max;
. We do it this way in order to ensure that public
keyword is included to Cache-Control
header. This marks the files to be cacheable better by proxy servers.
We also repeat our add_header
directives to deal with common configuration pitfall of its inheritance.
If you care for ancient browsers which are capable of HTTP/1.0 only, then you would also include add_header "Pragma" "public";
to the bunch.
But it doesn’t end there. Let’s review few more necessary steps.
Make changes to Google Analytics
Chances are, you use Google Analytics to track your website visitor statistics. Google Analytics will by default create a cookie that is bound to your domain and all of its subdomains. You need to adjust your tracking Javascript code to fix this. We will tell Google Analytics to use cookie which will apply to WordPress www domain only:
Change ga('create', 'UA-XXXXXXXX-X', 'auto');
to ga('create', 'UA-XXXXXXXX-X', 'www.example.com');
.
Apply SSL for your static domain
Static subdomains needs SSL, same to your parent domain. We recommend to use LetsEncrypt for the job. Once you have it, it’s easy to generate SSL certificate with a one line command. You can go further and generate single certificate which be valid for both www and static subdomains:
certbot-auto certonly --webroot \
-w /var/www/html/ -d example.com -d www.example.com \
-w /var/www/html/wp-content/ -d static.example.com
This creates single SSL certificate that you will use on both www and static.
We will now adjust both nginx server blocks to include SSL configuration:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
...
}
Adjust cookie domain
Open up wp-config.php
and put right below opening <?php
tag:
define("COOKIE_DOMAIN", "www.example.com");
This tells WordPress that we don’t want the cookies to be bound to all subdomains, only to www.
Change WordPress HTML to link files from static subdomain
Now it’s time to make WordPress actually use our static subdomain for good purpose. We want the static files, i.e. javascript files, images, so on to be linked as “`https://example.com/example.jpg“` instead of the main domain. How do we approach this? There are more than a couple of ways to achieve this. Choose the way that is most appropriate for you.
Option #1. Native WordPress way to change static file links
We can instruct WordPress to link all static files from wp-content
directory using our static subdomain. Additionally we can “shorten” our URLs a bit by providing plugins directory URL.
Open up wp-config.php
and put right below opening <?php
tag:
define("WP_CONTENT_URL", "https://static.example.com");
define("WP_PLUGIN_URL", "https://static.example.com/plugins");
Adjust existing wp-content URLs to link to static subdomain
I should be honest with you. I don’t like WordPress for storing absolute URLs across its database. It makes changing URLs a cumbersome task to deal with. But we can use the excellent WP-CLI command line tool to work around this design failure 🙂
wp search-replace 'https://www.example.com/wp-content/uploads/' 'https://static.example.com/uploads/'
Now your website will have all the images and plugins assets linked from static.example.com
.
Option #2. Plugin to the rescue.
You might have noticed that we changed URL to wp-content
directory only. But there are quite more files which are typically linked from wp-includes
directory. Those would still be linked from main domain should we stick to option #1.
We can fix re-link as many static files as possible (including wp-content
and wp-includes
) by using CDN Enabler plugin. It will rewrite every link to non-PHP file to specified domain name, and it’s not limited to wp-content
. If you use that, you’d have to adjust document root of static nginx server block to match with the main domain.
Using the plugin requires slightly more load to your server, because it has to buffer the whole page and replace content in the HTML.
Option #3. ngx_pagespeed
Personally, I hate to see long wp-content/uploads
links so I try to combine several approaches into an ultimate solution.
We maintain a CentOS repository for ngx_pagespeed plugin for nginx. It has the feature to rewrite assets links found in HTML to whatever we want. Here’s how we would approach this:
pagespeed MapRewriteDomain https://static.example.com/ https://www.example.com/;
Now create a symbolic link from ./wp-content/wp-includes
to ./wp-includes
(remember, static server block has the root pointed to wp-content).
The WordPress config from option #1 will already output all of the wp-content
links from static subdomain.
Now ngx_pagespeed will pick up the generated HTML and fix all the wp-includes
assets by putting them under our static subdomain as well.
I’m quite happy with this approach because it allows to shorten WordPress links and still have most of the static files linked properly for both directories in question.
Known issues with using static subdomain in general
- Autoptimize or W3TC plugins would not know how to handle optimizing assets from a static subdomain
- Query string removal (
functions.php
hack) might stop working for you. An update is needed to the code
Going back to a single domain
As we said, the cookie-less domain technique is generally controversial nowadays to due the prevalence of HTTP/2.
Using a single domain is now the de-facto standard practice. To back to a single domain, you can use WP CLI as well:
wp search-replace static.getpagespeed.com/plugins www.getpagespeed.com/wp-content/plugins