Difficulty with rewrite rules

What you’ve done is add a query variable, that’s great and necessary for what you’re trying to do, but there are 2 more steps to take:

  • Making it pretty: Add a rewrite rule to map /books/edition/xxx to index.php?ed=xxx
  • Making it functional: Make it load your custom template so it knows what to do when the user requests index.php?ed=xxx

Making it Functional

Lets assume the user has arrived at example.com/?ed=123. Your filters allow you to use get_query_var('ed') but how to load your template?

First we need to make it do things when the ed query variable is set, WP won’t know what to do on its own:

// call render_page() when needed
add_action( 'template_redirect', 'custom_page_template_redirect' );

/**
 * Filter that maps the query variable to a template
 */
function custom_page_template_redirect() {
    global $wp_query;
    if ( ! empty( $wp_query->query_vars['ed'] ) ) {
        // we've found our page, call render_page and exit
        get_template_part( 'ed page template' );
        exit;
    }
}

If we’re relying on a main query to pull in content, we’ll need to do some extra steps to adjust things:

// don't pull in a full listing of posts in the main query, there's no need
// ( you can comment this out if you're not using a theme template to render content )
add_filter( 'pre_get_posts', 'custom_page_paging_issue' );
/**
 * fix page loops if pulling in a theme template
 */
function custom_page_paging_issue( $query ) {
    if ( !empty( $query->query_vars['custom_page'] ) ) {
        $query->set( 'posts_per_page', 1 );
    }
}

Making it Pretty

So now example.com/?ed=123 should work, but as your question described, we want pretty URLs!

This is where rewrite rules come in. Rewrite rules use add_rewrite_rule, which needs 2 things:

  • A regular expression to match the URL
  • A query parameter based url to map what got matched on to

For example, this rewrite rule allows you to use URLs such as /leaf/22:

add_filter( 'generate_rewrite_rules', 'custom_page_generate_rewrite_rules' );
/**
 * Add our rewrite rules
 */
function custom_page_generate_rewrite_rules( $wp_rewrite ) {
    $custom_page_rules = array(
        '^leaf/([0-9]+)/?' => 'index.php?page_id=$matches[1]'
    );
    $wp_rewrite->rules = $custom_page_rules + $wp_rewrite->rules;
}

Note that whenever you change rewrite rules, you have to flush the permalinks. WP won’t regenerate the rules automatically. Some people might try to get around this by calling flush_rewrite_rules on the init hook, but this can cause problems related to the ordering of things, and is very expensive/slow. Simply visiting the permalinks settings page should be enough.

Additionally, it might be tempting to rewrite on to a different URL than index.php, or to rewrite on to a pretty permalink. This will not work. Rewrite rules map on to index.php?key=value type URLs, anything else will not work. For that you need HTAccess or Nginx rules. Rewrite rules aren’t redirects either and can’t be used to redirect to different URLs

Further Reading

I wrote about this in greater length here, including a helper class to simplify things.

I also wrote a post here describing the full process of getting from a URL to a fully loaded page