I have two custom post types: let's call them location and topic. The location custom post type is hierarchical. All articles are 'attached' to a single location using post meta. If it matters, the topic post type has a custom meta box that contains a dropdown of the locations that saves using:

update_post_meta( $post->ID, 'my-prefix-location', $the_location );

I'd like the rewrite the location so that their URLs would be https://my.domain.tld/location/sub-location/sub-sub-location/topic/.

For the location, I've tried, while registering the post type, to use the following rewrite argument:

'rewrite' => [ 'slug' => '/', 'with_front' => false, ],

That works fine for the locations, but I cannot access any of the topics, even after manually flushing rewrite rules. If I query them with /?id=100, they redirect to /topic/slug/ ( which isn't the URL structure I want ) but it results in a 404.

But even if I solve that problem, I have no idea how to rewrite the URLs for the topics so that they have the structure of the attached location in front.

I think I'm going to have to add_rewrite_rule() and/or add_rewrite_endpoint(). I've never actually used either of those functions before. Any pointers would be appreciated. Am I going about this the wrong way?


So I don't think I'm going to be able to do what I want easily (or even at all). What I've now been trying is to have my permalinks be:


It's easy enough to add a rewrite rule to ignore everything but the last parameter and match on that:

add_rewrite_rule( 'topic/(.*)/([^/]+)/?$', 'index.php?topic=$matches[2]', 'top' );

And this would work fine, except that I want the canonical URL to have that (.*) be the hierarchical URL structure of the category that the topic is attached to and have all other topic/*/leaf/ URLs redirect to this one.

I'm having a hard time figuring out how to do this, or even if it's possible.

  • Setting a cpt rewrite to just / will create a conflict and you will find that the page post type no longer works. Will the number of sub-locations differ for each topic?
    – Milo
    Apr 22, 2017 at 22:24
  • Hmm. Makes sense that the page post type wouldn't work either. I'd really not like to have the slug prepending the locations if at all possible. And yes, the number of sub-locations will be different for each topic. Apr 22, 2017 at 22:42

1 Answer 1


The way WordPress rewrite rules work (with a couple of special exceptions), different content types have some sort of unique element to their URLs that enables WP to identify what type of content it's looking for in the database before it goes and looks for it. For example, given the URL:


You can't say with any certainty if three is a location or a topic if both locations and topics share the same topic root.

So that said, the complicated part of this is that you need to resolve that ambiguity manually. This has a downside- every request for what might potentially be a topic will require an extra query to the database, which isn't a major issue, but something to be aware of.

In your original question you expressed the desire to remove the custom post type base. The answer I'll give here uses topic as the base. Removing that throws the page post type into the whole mix. You may be able to work it out using this solution, but for the sake of just showing you the simplest form of how this works, I'm not going to do that.

Step 1

Add a new %parent_location% rewrite tag that will be the placeholder for a topic's location. This happens on the init action along with your rules:

add_rewrite_tag( '%parent_location%', '(.+)' );

Step 2

Register your post types. I'll leave out the basic stuff and focus on the specific parts that make this work. This all happens on the init action as well. Don't forget to flush rewrite rules after adding these.

For locations, the root slug is topic, and the post type is hierarchical.

$args = array(
    'rewrite' => array( 'slug' => 'topic' ),
    'hierarchical' => true,
    // ... your other args
register_post_type( 'location', $args );

For topics, we put the new %parent_location% rewrite tag in the slug. We'll use this to substitute the location in the URL. The rewrite rules this generates will never actually be matched, but it makes the next steps easier.

$args = array(
    'rewrite' => array( 'slug' => 'topic/%parent_location%' ),
    // ... your other args
register_post_type( 'topic', $args );

Step 3

Add a filter to post_type_link to swap the location path in for our %parent_location% tag whenever a permalink for this topic is requested. Have a look at the comments to see what's happening.

function wpd_topic_link( $link, $post ) {
    // if this is a topic post
    if ( $post->post_type === 'topic' ) {
        // if there is location ID meta saved for this post under parent_location key
        if( $location_id = get_post_meta( $post->ID, 'parent_location', true ) ){
            // query for that post to make sure it exists
            $location_post = get_posts( array( 'post_type' => 'location', 'p' => $location_id ) );
            if( !empty( $location_post ) ){
                // get the location permalink
                // strip out everything except the location parts of the URL
                // substitute that value for our %parent_location% placeholder
                $location_link = get_permalink( $location_post[0] );
                $location_path = untrailingslashit( str_replace( home_url( '/topic/' ), '', $location_link ) );
                $link = str_replace( '%parent_location%', $location_path, $link );
    return $link;
add_filter( 'post_type_link', 'wpd_topic_link', 20, 2 );

Now when you add a topic post and save a valid location ID in post meta, you will see that path reflected in the URL when editing that topic post.

Step 4

Filter request to look for any requests for a location that might actually be a topic. Read through the comments to understand what's happening. Another side-effect to note here- you can never have a location slug that's also a topic.

function wpd_locations_may_be_topics( $request ){
    // if the location query var is set
    if( isset( $request['location'] ) ){
        // location will be a parent / child hierarchy
        // make it an array of URL segments
        $parts = explode( '/', $request['location'] );
        // it might be a topic only if there's more than a single segment
        if( count( $parts ) > 1 ){
            $topic_slug = end( $parts );
            $maybe_topic = get_posts( array( 'post_type' => 'topic', 'name' => $topic_slug ) );
            // if a topic was returned
            if( !empty( $maybe_topic ) ){
                // change request from location to topic    
                unset( $request['location'] );
                $request['post_type'] = 'topic';
                $request['topic'] = $topic_slug;
    return $request;
add_filter( 'request', 'wpd_locations_may_be_topics' );

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.