Menu

WordPress Hacks: Nested Paths For WPMU Blogs

Situation: you’ve got WordPress Multi-User setup to host one or more domains in sub-directory mode (as in site.org/blogname), but you want a deeper directory structure than WPMU allows…something like the following examples, perhaps:

  • site.org/blogname1
  • site.org/departments/blogname2
  • site.org/departments/blogname3
  • site.org/services/blogname3

The association between blog IDs and sub-directory paths is determined in wpmu-settings.php, but the code there knows nothing about nested paths. So a person planning to use WordPress MU as a CMS must either flatten his/her information architecture, or do some hacking.

**Challenge: hacking WordPress MU to support arbitrary directory paths for each blog

**

As with my multi-domain hack, the following assumes that you’re using the vhost=no setting, that you have access to and know how to manipulate your MySQL, that you have control over your DNS and know how to use it, and that you know how to configure Apache or similar. You’d also be smart to turn off any object caching you may have running, at least until we’re done doing direct database manipulation. The following also assumes that your wp-config.php sets the DOMAIN_CURRENT_SITE and PATH_CURRENT_SITE constants — if you’ve done a fresh install recently, it probably does, or you can check my domain mapping hack.

Hack The Path Mapping

Right at the top of wpmu-settings.php you can see how it strips all but the base of the URL path, but rather than mod that file, we can take advantage of an obscure MU hack: sunrise.php, which gets executed after some important WordPress components like the database class get loaded and before wpmu-settings.php.

To use sunrise.php, create a PHP file at /wp-content/sunrise.php and set define('SUNRISE', TRUE); in your wp-config.php.

Here’s the sunrise.php code I’m using:

if( defined( 'DOMAIN_CURRENT_SITE' ) && defined( 'PATH_CURRENT_SITE' ) ) {
	$current_site->id = (defined( 'SITE_ID_CURRENT_SITE' ) ? constant('SITE_ID_CURRENT_SITE') : 1);
	$current_site->domain = $domain = DOMAIN_CURRENT_SITE;
	$current_site->path  = $path = PATH_CURRENT_SITE;
	if( defined( 'BLOGID_CURRENT_SITE' ) )
		$current_site->blog_id = BLOGID_CURRENT_SITE;

	$url = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );

	$patharray = (array) explode( '/', trim( $url, '/' ));
	$blogsearch = '';
	if( count( $patharray )){
		foreach( $patharray as $pathpart ){
			$pathsearch .= '/'. $pathpart;
			$blogsearch .= $wpdb->prepare(" OR (domain = %s AND path = %s) ", $domain, $pathsearch .'/' );
		}
	}

	$current_blog = $wpdb->get_row( $wpdb->prepare("SELECT *, LENGTH( path ) as pathlen FROM $wpdb->blogs WHERE domain = %s AND path = '/'", $domain, $path) . $blogsearch .'ORDER BY pathlen DESC LIMIT 1');

	$blog_id = $current_blog->blog_id;
	$public  = $current_blog->public;
	$site_id = $current_blog->site_id;
	$current_site = sl_get_current_site_name( $current_site );
}

function sl_get_current_site_name( $current_site ) {
	global $wpdb;
	$current_site->site_name = wp_cache_get( $current_site->id . ':current_site_name', "site-options" );
	if ( !$current_site->site_name ) {
		$current_site->site_name = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->sitemeta WHERE site_id = %d AND meta_key = 'site_name'", $current_site->id ) );
		if( $current_site->site_name == null )
			$current_site->site_name = ucfirst( $current_site->domain );
		wp_cache_set( $current_site->id . ':current_site_name', $current_site->site_name, 'site-options');
	}
	return $current_site;
}

The first few lines of the code do pretty much the same as the start of the wpmu_current_site() function in wpmu-settings.php, but starting with line 8 it takes a big departure.

That’s where it splits the requested URL path like /path/to/blog/and/stuff/ into pieces and constructs an SQL query against the wp_blogs table to identify the correct blog to serve the request. The following example shows how:

SELECT *, LENGTH( path ) as pathlen
	 FROM wp_blogs
	 WHERE domain = 'domain.org' AND path = '/'"
	  	 OR (domain = 'domain.org' AND path = '/path/')
	 	 OR (domain = 'domain.org' AND path = '/path/to/')
	 	 OR (domain = 'domain.org' AND path = '/path/to/blog/')
	 	 OR (domain = 'domain.org' AND path = '/path/to/blog/and/')
	 	 OR (domain = 'domain.org' AND path = '/path/to/blog/and/stuff/')
	 ORDER BY pathlen DESC
	 LIMIT 1

Optimization note

Setting a maximum depth (and array_slice( $patharray, 0, $maxdepth )) would allow the query to be cached up to that depth. Otherwise, the query must be executed for every page load. The $maxdepth could either be set arbitrarily, or could be determined automatically based on the maximum path length of registered blogs.

Setting Up New Blogs

Once you’ve hacked the path mapping (and tested that it didn’t break your current site), you can add a new blog at a nested path.

Create a new blog in the MU blog admin.

Create a new blog in the MU blog admin.

Unfortunately, MU strips the slashes from the URL path you just tried to set.

The new blog you just tried to create, but with a very different path.

The new blog you just tried to create, but with a very different path.

Fortunately, you can set the path correctly in the MU blog editor, and it won’t break the path when you save there.

Set the blog path in the MU blog editor, MU won't break it when you save it this time.

Set the blog path in the MU blog editor, MU won't break it when you save it this time.

Once you create the new blog, try to load it in your browser. You’ll quickly notice the stylesheet is missing, though the blog works and functions properly.

Hack The .htaccess

WPMU uses the following .htaccess rewrite rule to properly direct requests for files on the real filesystem:

RewriteRule  ^([_0-9a-zA-Z-]+/)?(wp-.*) $2 [L]

Obviously, that rule won’t work for deep paths, so I’ve replaced it with this rule:

RewriteRule  ^(.+)?/(wp-.*) /$2 [L]

And with that, you should be done.