Domain Mapping with Page Mapping

I had one of those mornings and a little bit of the afternoon when I had to dig pretty deep into the code... wrote a lot of code, then trashed most of it.

My goal was to be able to map a subdomain to a page's permalink. This way, I can have map to

It wasn't clear where I should hook in: before the query was run? With a new rewrite rule?

I tried a little bit of both, but eventually settled on hacking $_SERVER['REQUEST_URI'] by hooking into 'init'.

It sounds ugly and perhaps dangerous, but it resulted in a pretty straightforward hack.

To use this hack, you need two domains. One to set up multisite on your server, and the other is mapped to one of the subsites. This is necessary because we hook into 'pre_get_site_by_path', which is fired from the Multisite code.

You also need to set up a wildcard domain on your DNS server.

If you're using Apache virtual hosts (meaning WordPress doesn't have the IP address all to itself), add a "ServerAlias *" line to your VirtualHost config block. The line works just as you expect: it matches all subdomains.

First, in wp-config.php, I altered the Multisite setup a little bit:

/* 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 = [ '' ];

I commented out the default current site (network) and path, and site and blog IDs. When these are commented out, the program tries to match the domain and path with configurations stored in the database.

I also created a global, $dmpm_domains, which is a list of mapped domains that will allow mapping subdomains to pages. This config isn't stored in the database because I'm the sysadmin, and it's my hack. Less overhead for me.

Defining SUNRISE causes WP to load sunrise.php before identifying the site and blog (network and site).

sunrise.php contains this:


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

We look at the domain to see if it supports mapping pages. If it does, we set the global $dmpm_subdomain with the subdomain part of the domain name. Then we get the site's ID (blog's ID), and then create an instance of WP_Site for it, and return it.

This short-circuits the network and site discovery algorithm, which is what we want to do.

If it doesn't match exactly, then it just proceeds to use the matching algorithm.

At this point, the $dmpm_subdomain global is defined only when we use a domain like

We then create a plugin:

wp scaffold plugin domain-mapping-page-mapping

And the plugin file domain-mapping-page-mapping.php is:

 * Plugin Name:     Domain Mapping Page Mapping
 * Plugin URI:
 * Description:     Maps pages to subdomains for one of the mapped domains.
 * Author:          John Kawakami
 * Author URI:
 * Text Domain:     domain-mapping-page-mapping
 * Domain Path:     /languages
 * Version:         0.1.0
 * @package         Domain_Mapping_Page_Mapping

namespace JK\DMPM;

function move_subdomain_to_uri() {
    global $dmpm_subdomain;
    if ($dmpm_subdomain) {
        $_SERVER['REQUEST_URI'] = '/'.$dmpm_subdomain.$_SERVER['REQUEST_URI']; 
add_action( 'init', 'JK\DMPM\move_subdomain_to_url' );

This code is a lot simpler. It hacks the REQUEST_URI. So if we used the domain, it hacks REQUEST_URI to look like /business/.

WP handles permalinks like /business/2/, for the 2nd page of the page. I don't know how that works, but it seems to be part of the page permalink URL, so I preserved it.

All this happens before WordPress tries to parse the REQUEST_URI.


One weird thing I do is use a namespace instead of wrapping my code in a class.

I do it because the code is shorter: you don't need to prefix your function names. There is no class { ... } syntax, so there's one less indentation. Calling the code is cleaner: you just call the function, no need for self::func() or Classname::func().

Also, using a class as a kind of namespace is a hack. It works, but, there's a namespacing feature in PHP. You usually see namespacing used along with classes, to organize libraries, but namespacing can be used without classes.