Ever wished you could extend single posts, pages or your custom post types with a custom template that keeps it own URL structure? With WordPress Rewrite API this is fully possible, and not difficult at all.

In this tutorial we’ll look at how to append single custom post type view with another slug that loads a different template. In other words, if a single custom post type post has the URL “example.com/destination/venice/”, you can add URL rules for separate pages with related information to each destination, for example”example.com/destination/venice/activities/” and “example.com/destination/venice/attractions/”.

If you’re interested in how to add endpoints to WooCommerce’s “My Account” page, I have another post about just that!

WordPress Rewrite API has plenty of useful functions for writing your own custom URL rules. You might have seen the most known and broad function; add_rewrite_rule(). This tutorial however uses the similar add_rewrite_endpoint(), which is very useful for adding URL “endpoints” (basically adding custom URL slugs after something that already exists, e.g. after the end of a single post or single catgory). You can achieve the same result by using add_rewrite_rule() but the process of adding endpoints are simpler.

What we will make

This guide will assume we have created two custom post types; ‘movie’ and ‘actor’. The permalink rule for a single movie is “example.com/movie/fight-club/” and for a single actor “example.com/actor/brad-pitt/”. We want a separate page for each movie that shows all actors in that movie, located at “example.com/movie/fight-club/actors/” and similarly a separate page for each actor that shows all movies that actor has been in, located at “example.com/actor/brad-pitt/movies/”.

I won’t go into detail about how to add these two custom post types; if you need help doing this part I recommend reading my post about how to add custom post types.

Writing the code

First step is calling add_rewrite_endpoint() in a function hooked to init (generally all functions in Rewrite API is hooked at init). to register our two desired endpoints; ‘movies’ and ‘actors’. The function takes two arguments; first the endpoint you want (e.g. ‘movies’), and secondly a constant for where the endpoint should “live” (e.g. pages, author, archives, etc). Look at the documentation for which constants you can use; as for this example the general EP_PERMALINK is fine:

add_action('init', function() {
	add_rewrite_endpoint('movies', EP_PERMALINK);
	add_rewrite_endpoint('actors', EP_PERMALINK);
});

NB: If your custom post type is hierarchical, meaning it has 'hierarchical' => true in its register_post_type(), you need to switch out the constant EP_PERMALINK with EP_PAGES.

After saving this code, you need to go to Settings > Permalinks and simply click the Save button to refresh permalinks. Whenever you add or modify a rewrite rule you need to refresh your permalinks for it to work!

Handling the query vars

The way you as a theme or plugin developer can figure out whether or not to show the templates for these endpoints, is by checking “query vars”; basically WordPress’ global query object. We do this by calling get_query_var() with the query as argument (‘movies’ or ‘actors’).

If you have worked with add_rewrite_rule() or get_query_var() before, you might already be aware that WordPress don’t automatically add custom query vars. Usually you’d have to filter query_vars and add your custom variables in order to get WordPress to populate them. However add_rewrite_endpoint() automatically does this for us.

However, if we try to call get_query_var('movies') it will never appear to be set. That’s because it assumes it has to have a value in order to be set. Endpoint rules assumes that whatever comes after the endpoint is the value. For example “example.com/actor/brad-pitt/movies/some-value/” would work, because on this page get_query_var('movies') would return the value ‘some-value’. But this is not what we want, we want it to work with the single endpoint alone. To solve this we need to hook into WordPress’ request filter and inform WordPress that if we are at our endpoints, the query vars should add the endpoint with some value (we just set it to true).

add_filter('request', function($vars) {
	if (isset($vars['movies'])) {
		$vars['movies'] = true;
	}
	if (isset($vars['actors'])) {
		$vars['actors'] = true;
	}
	return $vars;
});

If you try now usingget_query_vars('movies') when at “example.com/actor/brad-pitt/movies/” you would get the value true (the important thing is that it has actually been set).

Loading template

The next step is deciding what should happen on these two endpoints; or in other words, which templates you want to load. This part is up to you, you could hook onto template_redirect but that’s really only recommended if you want to perform an actual redirect. I recommend hooking onto the template_include filter and simply tell WordPress which templates to use when we’re at our custom endpoints. Let’s assume the theme has the PHP templates single-actor-movies.php for “example.com/actor/<actor>/movies/ and single-movie-actors.php for “example.com/movie/<movie>/actors/”.

add_filter('template_include', function($template) {
	if (is_singular() && get_query_var('movies')) {
		$post = get_queried_object();
		return locate_template(['single-actor-movies.php']);
	}
	if (is_singular() && get_query_var('actors')) {
		$post = get_queried_object();
		return locate_template(['single-movie-actors.php']);
	}
	return $template;
});

With this filter in place WordPress should load the provided templates for our custom endpoints, and within those templates the global $post object would be the related single post object before the endpoint – e.g. the single actor post object when we’re at the template for showing all movies the actor has appeared in. This makes it easy for us to query the information we want to show.

Getting the URL to your endpoint

Having our custom endpoints and pages is all fine and dandy, but somewhere you would need to link to those. For example in the single movie template you would like a link “See all actors” that goes to your endpoint.

Unfortunately there is no such simple WordPress function for this. You’ll need to build the URL yourself. I recommend using get_site_url() for sitewide endpoints, or in the above case you’d refer to a specific post with get_the_permalink() (either inside the loop or by providing a post ID) and appending the endpoint after it, like so:

echo get_the_permalink() . '/actors';