WordPress Routing Hacks for Single Page Applications


If you’ve ever attempted to bundle a Javascript single page application (SPA) inside of WordPress, you’ll likely run into the issue of routing. The problem isn’t that JS frameworks can’t route properly, the question is how do you get WordPress to send all web requests pertaining to the SPA to the SPA.

Let’s say we have a WordPress site located at anchor.host, which I do. Now let’s say I want all requests going to /account to be directed to a custom page template which will act as my bootloader for my JS application. Well that’s easy, just create a page and assign it a custom page template. Now how do we also direct all children requests to that same template? Requests like /account/sites or /account/sites/<site_id> or even /account/sites/<site_id>/<action>?

WordPress doesn’t have a built in router for developers.

Custom routes like this are going to require clever manipulation of the WordPress template hierarchy, or custom coding. If interested in doing a proper WordPress router I’d highly recommend checking out these resources:

If you want a quick and dirty solution then keep reading.

There are two parts to this routing hack. The first is to intercept all requests going to a certain path and load our custom page template. Think of this as our catch all. This can be accomplished using the template_include filter.

// Load custom template for web requests going to "/account" or "/account/<..>/..."
add_filter( 'template_include', 'load_captaincore_template' );
function load_captaincore_template( $original_template ) {
  global $wp;
  $request = explode( '/', $wp->request );
  if ( is_page( 'account' ) || current( $request ) == "account" ) {
    return plugin_dir_path( __FILE__ ) . 'templates/core.php';
  }
  return $original_template;
}

The second part is to disable WordPress’ built in 404 redirection. If a request is not found, WordPress falls back to its permalink structure. That means a request going to something like account/unknown_page might get changed to post/unknown_page which would not be helpful. The redirect_canonical filter can be used to disable this redirection, thus allowing the original request to fallback to the custom template as defined above.

// Disable 404 redirects when unknown request goes to "/account/<..>/..." which allows a custom template to load. See https://wordpress.stackexchange.com/questions/3326/301-redirect-instead-of-404-when-url-is-a-prefix-of-a-post-or-page-name
add_filter('redirect_canonical', 'disable_404_redirection_for_captaincore);
function disable_404_redirection_for_captaincore( $redirect_url ) {
    global $wp;
    if ( strpos( $wp->request, "account/" ) !== false ) {
        return false;
    }
    return $redirect_url;
}

With that, any request going to account/ or account/<..>/... will be passed to a custom page template. With that page template we can do a bit of PHP to load in our single page JS application. Here is a basic Vue.js template with pushState to handle JS routing. This would be placed within a WordPress plugin templates/core.php as defined above.

<!DOCTYPE html>
<html>
<head>
    <title>Account</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
    <meta charset="utf-8">
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js"></script>
<script>
new Vue({
	el: '#app',
	data: {
		route: "",
		routes: {
			'/account': '',
			'/account/sites': 'sites',
			'/account/dns': 'dns',
		},
	},
	watch: {
		route() {
			this.triggerRoute()
		}
    },
	mounted() {
		// Detect browser back/forward page updates
		window.addEventListener('popstate', () => {
			this.updateRoute( window.location.pathname )
		})
		
		this.updateRoute( window.location.pathname )

		if ( this.route == "" ) {
			this.triggerRoute()
		}
	},
	methods: {
		triggerRoute() {
			// Do JS here to update page
		},
		updateRoute( href ) {
			// Remove trailing slash
			if ( href.slice(-1) == "/" ) {
				href = href.slice(0, -1)
			}
			this.route = this.routes[ href ]
		},
		goto ( href ) {
			this.updateRoute( href )
			window.history.pushState( {}, this.routes[href], href )
		},
    }
});
</script>
</body>
</html>