How to set up a W3TC mirror CDN for WP multisite with domain mapping

WordPress Multisite with the domain mapping plugin is a good way to run multiple wordpress sites. It means you don’t have to worry about keeping WordPress and plugins up to date for each individual site.

The W3 Total Cache plugin is one of the most popular wordpress plugins for speeding up wordpess websites. One of the things it offers is serving static content from a CDN (Content Delivery Network). You don’t have to use a real CDN, but can instead a generic mirror CDN.

A generic mirror CDN doesn’t have all the benefits of a real CDN. However, because you use a different domain (or subdomain) for the CDN address, it does solve the problem of cookies being sent for static content requests. And unlike a real CDN, you don’t need to pay anything extra for it.


Setting up a static mirror

To start off, you’ll need to create a subdomain for your CDN. Using this site as an example, the main address is, so I would create a subdomain of for use as the CDN. If your webhost uses cpanel, look for ‘Subdomains’ in the ‘Domains’ section.

When adding your subdomain, ensure that the document root is the same as the document root for the main website address.

If you use a main website address that is not prefixed by www. (or anything else), then you will need to use a completely different domain for the CDN. e.g. if my main website address was (no www.), using as a mirror CDN would be pointless.

This is because cookies applied to the root domain are applied to all subdomains as well. So it is a good idea to always use the www. subdomain as your main website address to avoid this issue.

After setting up your static subdomain, you’ll need to edit the .htaccess file in your wordpress folder. Add the following:

#Set an environment variable if it is a static subdomain
#Set long expires headers for static subdomains
Header set Cache-Control "max-age=29030400, public" env=STATIC_DOMAIN

What this does is it first sets an apache environment variable called ‘STATIC_DOMAIN’ if the domain starts with ‘static’.

Then the Header line sends a long expires cache header, but only if the ‘STATIC_DOMAIN’ variable is true (i.e. for all requests to domains / subdomains starting with ‘static’).

On my wordpress .htaccess I have the following as well:

#Rewrite any non existant files on www. subdomain to index.php
RewriteRule ^index\.php$ - [L]
RewriteCond %{ENV:STATIC_DOMAIN} !=1
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

#Serve 404 to anyone requesting PHP files from a static subdomain
RewriteCond %{ENV:STATIC_DOMAIN} =1
RewriteRule \.php$ non-existant-file [L]

Now in the W3TC options in the wordpress admin area, you can select to use a CDN and choose Generic Mirror. W3TC will automatically rewrite all your urls pointing to static content to point at your static subdomain instead. For example, if I included an image in this post with an src of /wp-content/uploads/some-image.jpg, W3TC would change that so when the page is served, the src points to

Stop requests to /files/* being rewritten to ms-files.php

However, we’re not done quite yet as wordpress multisite usually serves static files via PHP. Yes, it is madness. Thankfully a ticket is on the wordpress trac for fixing this, with a milestone of version 3.4.

In the meantime, you’ll need to fix this yourself. First check your .htaccess and look for a line like

RewriteRule ^([_0-9a-zA-Z-]+/)?files/(.+) wp-includes/ms-files.php?file=$2 [L]

This is the line that causes static files (with the address containing /files/) to be served by PHP. If you know your site doesn’t use /files/ for anything, then you don’t need to do any of the following. Otherwise, comment that line out by adding a pound sign (#) in front of it:

#RewriteRule ^([_0-9a-zA-Z-]+/)?files/(.+) wp-includes/ms-files.php?file=$2 [L]

Now you need to find out the ids of each of your blogs. Look at the database for your wordpress site (e.g. using phpMyAdmin) and find the table called wp_blogs. Look at the blog_id column and make a note of the blog_ids and matching domains.

Map blog_ids using .htaccess

Unless you can setup a rewrite map, you’ll need to manually add a line to your .htaccess file for each domain:

SetEnvIfNoCase HOST$ BLOG_ID=1
SetEnvIfNoCase HOST$ BLOG_ID=2
SetEnvIfNoCase HOST$ BLOG_ID=3
RewriteRule ^files/ wp-content/blogs.dir/%{ENV:BLOG_ID}%{REQUEST_URI} [L]

The SetEnvIfNoCase lines map the domains to the wordpress blog_ids. Then the rewrite rule rewrites the request to correct directory.

Map blog_ids using a rewrite map

If your host allows rewrite maps, or you can have access to configure apache, then you can do the following instead:

Create a plain text file called domainToBlogIdMap.txt and add your domains. As far as I’m aware, apache only does exact matches for this, so you need to include both www. and static subdomains. 1 1 2 2 3 3

In your virtualhost config for the site, add the rewrite map

RewriteMap domainToBlogIdMap txt:/home/xoogu/path/to/domainToBlogIdMap.txt

RewriteRule ^files/ wp-content/blogs.dir/${domainToBlogIdMap:%{HTTP_HOST}}%{REQUEST_URI} [L]

Setting up a static mirror and mapping blog_ids in Nginx

If you’re using nginx you can do the following:

map  $http_host  $name  {
  *     1;
  *       2;
  *			3;

server {
	listen		80;
	error_log  logs/wpms-error.log warn;
	access_log  logs/wpms-access.log  main;
	root  /home/xoogu/path/to/wordpress;
	location ~* \.php$ {
		deny all;

	location / {
		try_files       $uri /wp-content/blogs.dir/$name$uri =404;
		expires         max;
		access_log      off;

In the first section we set up our rewrite map to map the domains to the wordpress blog_ids. Then in the second section we set up our static subdomains. Set the root to the same as the main site.

Then we have one location block to prevent people accessing php files via the static subdomain. The other location block serves the requested file if it exists, otherwise it tries to rewrite the request to the correct blog_id directory. If the file doesn’t exist there, it will serve a 404 error.

Posted on by xoogu, last updated

Leave a Reply