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.

15 thoughts on “WordPress Hacks: Nested Paths For WPMU Blogs

  1. A multi-site plugin will do the same thing and give you an admin menu to control it.. :) The sunrise.php is an awesome file, which multi-site and domain mapping plugins make great use of.

    It won’t get overwritten on an upgrade.

    • @Andrea_R:
      Please link to a plugin you’d recommend. David Dean’s Multi Site Manager looks good, but doesn’t appear to support nesting blogs at different paths within the same site (it’s also frustrating that I can’t find SVN or Trac access to the plugins at wpmudev.org). It’s critical to my use that I be able to put a blog at any depth in the URL path.

    • @Steve: you could do that with the .org version of WordPress, but you’d end up doing a lot of work just to avoid installing WPMU. And, if you did go the WPMU route, your proposed page structure is fully supported by the default install.

  2. Pingback: » Hacking WordPress Login and Password Reset Processes For My University Environment MaisonBisson.com

  3. I got stuck trying to get sunrise.php to work correctly until I realized that the sample sunrise.php code is missing the opening and closing php tags.

  4. Hi, I’ve read both tutorials (multiple domains & nested paths).

    How would you achieve something like this?

    department.domain.com/blog

    where “department” in the only variable?

  5. Hi Casey,

    This is great advice for nested paths – something I would imagine alot of users need.

    I have gone through the steps successfully..until the very last one with the .htaccess file. The nested blog is present but unfortunately doesn’t have the stylesheet path working. I have put the line in there, but is there anything else that I have missed that could be the problem?
    Very grateful for any assitance you might be able to offer.

    RewriteEngine On
    RewriteBase /means/

    #uploaded files
    RewriteRule ^(.*/)?files/$ index.php [L]
    RewriteCond %{REQUEST_URI} !.*wp-content/plugins.*
    RewriteRule ^(.*/)?files/(.*) wp-content/blogs.php?file=$2 [L]

    # add a trailing slash to /wp-admin
    RewriteCond %{REQUEST_URI} ^.*/wp-admin$
    RewriteRule ^(.+)$ $1/ [R=301,L]

    RewriteCond %{REQUEST_FILENAME} -f [OR]
    RewriteCond %{REQUEST_FILENAME} -d
    RewriteRule . – [L]
    RewriteRule ^(.+)?/(wp-.*) /$2 [L]
    RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
    RewriteRule . index.php [L]

    SecFilterEngine Off
    SecFilterScanPOST Off

  6. I am also having my new blogs not use the the theme that I have assigned them. I am unsure if anyone has found a solution to this problem but I would appreciate it if someone would post it here.

  7. Hi Casey and everyone,
    Awesome work – anyone got this to work with WordPress 3.0 multisite? Can anyone post instructions, as we cannot get it working. If there is a plugin that applies this that would be great! @Andrea_R mentioned A multi-site plugin will do the same thing? Can anyone link to one that supports nested paths?

    I too need this same thing, the site is a non wordpress CMS
    mysite.com/travel
    mysite.com/insurance

    But every category folder needs its own blog:
    mysite.com/travel/blog
    mysite.com/insurance/blog

    Other idea – I saw someone managed to map a domain subfolder to each wordpress multisite, like domain.com/blog to one site, maybe this can be used to achieve my desired result = map mysite.com/travel/blog to one site and mysite.com/insurance/blog to another site?
    http://martyspellerberg.com/2010/07/migrating-directory-structures-of-existing-blogs-into-a-wordpress-3-0-network/

    If anyone can do a plugin or even solve this for a small fee please let me know. Hopefully there are not downsides to this hack? Like the rest of wordpress and all the plugins shd work fine right?

    The other solution for me is, do my site in wordpress, then for categories which need a more complex CMS use another CMS in that folder, that should work right?
    i.e. wordpress runs mysite.com so mysite.com/travel and mysite.com/insurance are just wordpress categories.
    But mysite.com/shopping I want a different CMS in there, is there anything in the root mysite.com wordpress install I have to do so mysite.com/shopping uses that other custom PHP CMS without issues?
    Although again I would like the mysite.com/shopping section to have its own blog at mysite.com/shopping/blog so would really want the Nested Paths for Multisite!

    Very keen to solve this, have seen lots of people on forums wanting the same thing too! So anyone pls reply or msg me, thanks
    Cheers,
    Dave
    P.S. I also posted about this on the WP forums: http://wordpress.org/support/topic/408371?replies=1

  8. @Dave

    I’m in the process of testing WP3.0 as a CMS for our site. I need the nested function. I just tested Casey’s solution with WordPress 3.0 multisite and after some minor tweaks I got it to work. Here is where I found problems:

    1. WordPress wouldn’t accept the nested blog name in the first place. It kept saying “missing or invalid site address” whenever I try to create a nested blog site. So I had to change the validation WP does of that field when you click on “Add site”. That happens on wp-includes/ms-edit.php, line 149. I changed that line from:
    if ( ! preg_match( ‘/(–)/’, $blog[‘domain’] ) && preg_match( ‘|^([a-zA-Z0-9-])+$|’, $blog[‘domain’] ) )
    To:
    if ( ! preg_match( ‘/(–)/’, $blog[‘domain’] ) && preg_match( ‘|^([a-zA-Z0-9-/])+$|’, $blog[‘domain’] ) )

    Note that I added “/” as a valid character.

    2. Silly me, I wasn’t putting the code in sunrise.php in php tags: “”. That was easy to spot and solve (looks like others have the same problem in the past.)

    3. The rewrite rule suggested by cassey didn’t work in WP3.0. Instead a replaced this rule:
    RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
    with
    RewriteRule ^([_0-9a-zA-Z-]+/)+(wp-(content|admin|includes).*) $2 [L]

    After that it seems to be working.

    @Cassey – Thank you very much for this post.

    • Hello!, I need to revive this topic ’cause I have different my .htaccess:

      # BEGIN WordPress
      RewriteEngine On
      RewriteBase /kb/
      RewriteRule ^index\.php$ – [L]

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

      # add a trailing slash to /wp-admin
      RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]

      RewriteCond %{REQUEST_FILENAME} -f [OR]
      RewriteCond %{REQUEST_FILENAME} -d
      RewriteRule ^ – [L]
      RewriteRule ^[_0-9a-zA-Z-]+/(wp-(content|admin|includes).*) $1 [L]
      RewriteRule ^[_0-9a-zA-Z-]+/(.*\.php)$ $1 [L]
      RewriteRule . index.php [L]

      # END WordPress

      What I have to replace here?

      Thanks in advance for your help :D

Comments are closed.