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.
Unfortunately, MU strips the slashes from the URL path you just tried to set.
Fortunately, you can set the path correctly in the MU blog editor, and it won’t break the path when you save there.
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.