Site icon GetPageSpeed

WordPress Static Files Cookieless Domain with Nginx

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:

I would say you should use cookie-less/separate domain for static if you plan to use a CDN and any of:

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:

Cookieless domain for static WordPress files ensures better cacheability of your website. It’s quite simple to understand how this works on example:

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;
    ...
}

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.

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.

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");

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

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
Exit mobile version