Custom Permalinks for WordPress Custom Template Page or Custom Tables

I've blogged before about custom routing in WordPress, but today I faced a more practical problem. Let's say you've create a page template and are displaying some information on it based on the query parameters. For example:

http://www.example.com/?page_id=47&recipe_id=1231

And you've created a custom table for your recipes (though in this case they would fit in as a post type, but for the sake of example bare with me). Say you've done something like this:

global $wpdb;
if ( ! empty($wpdb->charset) )
	$charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
if ( ! empty($wpdb->collate) )
	$charset_collate .= " COLLATE $wpdb->collate";

$schema = "CREATE TABLE {$wpdb->prefix}recipes(
		ID bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
		slug varchar(255) NOT NULL,
		description TEXT
		) $charset_collate;";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $schema );

And you've gone through all the effort to display the description and a bunch of other information linked from other tables, but now you're stuck with URLs that look like the above, and you've decided they're ugly and you'd rather have a nice structure like this:

http://www.example.com/recipes/real-cool-recipe

Where the real-cool-recipe is the slug of the recipe. Believe it or not, this is actually pretty simple to do with WordPress permalinks! Though there is a very dasterdly gotcha here:

If another post is set to the same thing as your permalink's structure, you'll get an endless redirect

More on that in a moment, here's how to do it:

functions.php

add_action( 'init', 'vanity_setup' );
function vanity_setup(){
	$pageId = 47; //pull this from the page with the recipes template
	add_rewrite_rule(
        'recipes/(([^/]+))?',
    	'index.php?page_id='.$pageId.'&recipe_id=$matches[1]',
    	'top'
	);
}

add_filter( 'query_vars', 'vanity_vars' );
function vanity_vars( $query_vars ){
    $query_vars[] = 'recipe_id'; //dont forget this!
    return $query_vars;
}

add_action('pre_get_posts', 'vanity_permalink', 10, 3);

function vanity_permalink($query) {

	if(if_on_recipe_index_page()){
		return;
	}
	if( isset($query->query_vars['recipe_id']) 
		&& !empty($query->query_vars['recipe_id']) 
		&& !is_numeric($query->query_vars['recipe_id']) 
		&& isset($query->query_vars['page_id']) ){

			switch ($query->query_vars['page_id']) {
				case 47:
					$query->query_vars['recipe_id'] = Recipe::getIdBySlug($query->query_vars['recipe_id']); 
					if(empty($query->query_vars['recipe_id'])){
						//404...
					}
				break;
			default:
    			break;
			}    	
	}
}

Alright, so let's examine the code! First off, we create our rewrite rule, this must be setup in or before the init action. Because we know our page id then you can go ahead and use your knowledge to set which page we'll actually load (since we need it's template). Next, we create the redirect rule 'index.php?page_id='.$pageId.'&recipe_id=$matches[1]' which tells wordpress to treat it like we're getting the page_id and the recipe_id as query parameters. This is why we filter the query_vars and add the variable. The variable will not be availabled from $_GET becuase WordPress will filter it out.

Next, we do 'the magic' as it were. Mapping our slug to the id field in the pre_get_posts action. Note the call to if_on_recipe_index_page() is to prevent redirects. If you had another page whose name was recipes, and then set the permalink structure to display single recipes in recipes/slug and didn't do this, WordPress would keep jumping between them until your browser got tired.

All the checks in the condition before the switch statement is because we only want to filter the calls we want to know about. Then the switch statement is useful for if you need multiple structures like this and want to handle them in one routing function.

Note that once you use something like this you will have to save your permalinks everytime you make a change to the rule in the add_rewrite_rule.

Hope this helps someone!