I had been using the old WordPress MU Domain Mapping plugin for multisite WordPress, and was pretty happy with it, but unknown to me (because I only started reading WP blogs and groups this past year) WordPress has rolled this feature into the core.

The old plugin still works, but now you don’t need it. To uninstall it, delete wp-content/sunrise.php, the tables the plugin created, and the plugin directory. Manually re-map the domains by setting the value of the URL to your custom domain.

I was working on a hack to create a mapping of subdomains to pages, and learned a bit about the domain system in WordPress.

To implement this, I needed to learn about the way multisite blogs are loaded. The sequence is:


Libraries being loaded are not listed here. It’s just the important files that are run during the execution.

Mapping domains and paths to WordPress blogs and networks happens in code called from ms-settings.php.

Mapping Domains Happens Before Plugins are Loaded

By the time we reach ms-settings.php, the plugins have not been loaded. So, we cannot implement the feature with a plugin. It must be implemented in sunrise.php, a file that, if enabled, is loaded by ms-settings specifically to allow us to alter how the system will discover the current blog and current network of sites.

(Just to note, WP supports multiple networks of sites. I’m not sure of how this works, because it’s not written about much, but it’s in there. You can have only one one network with many blogs – you can also have many networks, each with many blogs. It appears that to have many networks, the blogs need to be path-based, not subdomain-based.)

So, create sunrise.php, then enable it by adding this line to wp-config.php

define(‘SUNRISE’, 1);

In the sunrise.php script, we could do our domain mangling, and set the globals $current_site and $current_blog.

(Note again, in some parts of the WP code, the terminology is scrambled: “site” means “network” in the admin pages, and “blog” means “site” in the admin pages. So $current_site is the current network, and $current_blog is the current site. This is a totally annoying naming clash, so I’m going to stick with “network” and “blog”, avoiding the use of “site” entirely, unless we’re talking about WP_Site and WP_Network.)

Hooking into pre_get_site_by_path

We could set those globals, but there’s already existing code that will do that for us, taking care of a lot of extra stuff. Instead of duplicating all that, can hook into a filter inside ms-load.php, in the get_site_by_path() function.

get_site_by_path($domain, $path, $segments) takes the domain and path, and spits out a WP_Site object.

Internally, it calls a filter named ‘pre_get_site_by_path’ that can short-circuit the process, allowing us to override the default search method.

Even better, if we only want to affect specific domains, not all domains, we can just return FALSE, and the default behavior applies.

Likewise, the function WP_Network::get_network_by_path() has a filter, ‘pre_get_network_by_path’, that performs a similar role with matching networks. This is called only if there is more than one network configured.

The following function doesn’t do anything except dump its arguments, but that’s where I am.


namespace JK\DMPM;

function mangle($ignore, $domain, $path, $segments, $paths ) {
    echo var_dump($ignore, $domain, $path, $segments, $paths);

    $site = \WP_Site::get_instance(3);
    return $site;
add_filter('pre_get_site_by_path', 'JK\DMPM\mangle', 10, 5); 
add_filter('pre_get_network_by_path', 'JK\DMPM\mangle', 10, 5); 

I was just so jazzed to find the right way to do this, that I had to share.


Well, I did end up writing this plugin, but not having really studied SEO, I didn’t realize that this plugin would have dissipated the pagerank of the pages: search engines consider subdomains to be separate domains. What does this mean for domains mapping to single pages? It means each page may have a very, very low rank starting out.

It’s a cool hack, but it doesn’t fulfill any business goals, unless that goal was to hide from search engines. That wasn’t my goal.

(That said, I do have another project for which this is the goal, so, it’s probably a good trick to avoid being crawled.)

Postscript: the code from part of the plugin

/* Multisite */
define( 'WP_ALLOW_MULTISITE', true );

define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', true);
// define('DOMAIN_CURRENT_SITE', 'localhost.lo');
// define('PATH_CURRENT_SITE', '/');
// define('SITE_ID_CURRENT_SITE', 1);
// define('BLOG_ID_CURRENT_SITE', 1);
define('COOKIE_DOMAIN', ''); // fixes cookie error message

/* Domain Mapping with Page Mapping */
define('SUNRISE', 1);
$dmpm_domains = [ 'example.com' ];

namespace JK\DMPM;

 * This function hooks into the domain loading part of 
 * get_site_by_path(), which is called by ms-settings.php.
 * Sets the $dmpm_subdomain global so it can be used by the
 * plugin later.
 * @return null|WP_Site
function mangle_subdomain($ignore, $domain, $path, $segments, $paths ) {
    global $dmpm_domains, $dmpm_subdomain, $wpdb;

    foreach($dmpm_domains as $dom) {
        $re = '/^.*\.'.$dom.'$/';
        if (preg_match($re, $domain)) {
            $subdomain = rtrim(str_replace($dom, '', $domain), '.');
            $id = $wpdb->get_var( $wpdb->prepare("
                SELECT blog_id FROM {$wpdb->blogs}
                WHERE domain = %s
            ", $dom));
            if ($id) {
                $dmpm_subdomain = $subdomain;
                return \WP_Site::get_instance($id);
            // if there's no matching domain configured, we continue the search
    // if we haven't matched anything, bail
    return null;
add_filter('pre_get_site_by_path', 'JK\DMPM\mangle_subdomain', 10, 5); 

So, the code is pretty self-explanatory. It extracts the domain, matches it against a list of domains where we want the plugin to work, and then return a matching site.

Don’t use this code. It’s obsolete. I’m keeping it here so you can see how the pre_get_site_by_path hook works.

Leave a comment

Your email address will not be published. Required fields are marked *