Registering custom URLs with custom templates in WordPress (without using page templates)

It’s fairly common to find yourself on a situation where you want to use a specific URL to show a custom content (perhaps something an archive page with two different custom post types), and think: “well, that’s easy. I’ll just create a page to register the URL and a custom page template where I’ll query the contents I need”.

Well, it turns out that there’s a better way of doing this using rewrites and hooking into the right WordPress’ filters — which, by the way, it’s the recommended way to do it by the WordPress VIP team.

Let’s check this technique with an example.

Using a custom URL as an alias for a WordPress query

On this example, we’ll register a custom URL for showing a set of posts from three different custom post types on a single, unified archive.

Registering the custom URL

The first step it’s registering the custom URL we’ll use as the archive page. For this, we need to add our own rules to the WordPress rewrite engine, hooking into the init action:

add_action('init', function(){
    add_rewrite_rule(
        'writing-ideas/?$',
        'index.php?post_type[]=essay&post_type[]=novel&post_type[]=short_story',
        'top'
    );
});

Since we’re adding new rewrite rules, we must call flush_rewrite_rules(), which it’s something you shouldn’t be doing on every page load, so we need to hook into an event that’s not triggered with every request. To achieve this, we will wrap the code as a plugin and call flush_rewrite_rules() on plugin activation:

class Writing_Ideas{
    public function init(){
        $this->add_rewrite_rules();
    }
    private function add_rewrite_rules(){
        add_rewrite_rule(
            'writing-ideas/?$',
            'index.php?post_type[]=essay&post_type[]=novel&post_type[]=short_story',
            'top'
        );
    }
}

register_activation_hook( __FILE__, function(){
    $writing_ideas = new Writing_Ideas;
    $writing_ideas->init();
    flush_rewrite_rules();
});

Now, since in this example we’re just adding a friendly URL for a query that WordPress can already understand, we just need a way to display the custom template that we want to use for this URL.

There are several ways to get this done, but the cleanest one might be hooking into the template_include filter hook:

class Writing_Ideas{
    public function init(){
        $this->add_rewrite_rules();
        add_filter('template_include', array($this, 'filter_template'), 99);
    }
    public function filter_template( $template ){
        global $wp;
        if ( stripos( $wp->request, 'writing-ideas' ) !== false ) {
            $custom_template = locate_template('archive-writing-ideas.php');
            if ( $custom_template ) {
                return $custom_template;
            } 
        }
        return $template;
    }
[...]

Using this filter hook, we can be sure that WordPress already parsed the request and queried the database for the posts matching the posts types we defined on the rewrite rule.

You could also add other query parameters, such as posts_per_page to the query string, and they will be automagically handled by WordPress.

Since we’re piggybacking on the WordPress query, our custom template can use a normal loop to show the queried posts:

<?php if ( have_posts() ) : ?>
    <div class="row">
        <?php while ( have_posts() ) : the_post(); ?>
            <?php get_template_part('partials/list-entry', get_post_type()); ?>
        <?php endwhile; ?>
    </div>
<?php endif; ?>

Taking care of pagination

If you have lots of entries it’s quite likely you’ll want to use some sort of pagination on your custom template, but the rewrite rule we previously added won’t work if you just add /page/2/ to the URL, which means that we need to add a new rule for these cases.

private function add_rewrite_rules(){
    add_rewrite_rule(
        'writing-ideas/?$',
        'index.php?post_type[]=essay&post_type[]=novel&post_type[]=short_story',
        'top'
    );
    // add support for pagination
    add_rewrite_rule(
        'writing-ideas/page/([0-9]{1,})/?$',
        'index.php?paged=$matches[1]&post_type[]=essay&post_type[]=novel&post_type[]=short_story',
        'top'
   );
}

Once again, since we’re just adding new parameters WordPress can understand, there’s no need for further filtering, so we can keep using the same old loop.