I need to produce the following: Two post types (collection & collaboration) that are hierarchical to each other. Collections can be parents to other collections and collaborations.

  • /collection-a/collection-b/collection-c
  • /collection-a/collection-b/collaboration-a

My method has been to hijack the page rewrite rule and add a custom rewrite tag:

function custom_rewrite_rules() {
    add_rewrite_rule( '(.?.+?)(?:\/([0-9]+))?\/?$', 'index.php path=$matches[1]&page=$matches[2]', 'top' );

function custom_rewrite_tag() {
    add_rewrite_tag( '%path%', '([^&]+)' );
add_action( 'init',  __NAMESPACE__ . '\\custom_rewrite_rules', 10, 0 );
add_action( 'init',  __NAMESPACE__ . '\\custom_rewrite_tag', 10, 0 );

I've also taken care of the post links using a custom parent selection meta box and the following filter:

add_filter( 'post_type_link' , __NAMESPACE__ . '\\post_type_link_handler', 10, 2 );
    function post_type_link_handler( $url, $post ) {
        if ( ! in_array( $post->post_type, [ 'collection', 'collaboration' ], true ) ) {
        return $url;
    $parents = get_post_ancestors( $post );

    $intermediate_slug = '';

    foreach ( array_reverse( $parents ) as $parent ) {
        $intermediate_slug .= trailingslashit( get_post_field( 'post_name', $parent ) );

    $post_link = home_url( user_trailingslashit( "$intermediate_slug$post->post_name" ) );
    return $post_link;

So far so good. The slugs for these post types are constructed as I mentioned above.

My issue comes when I try to use pre_get_posts to determine which object to load.

function query_handler( $query ) {
    global $wp_query;
    if ( is_admin() || ! $query->is_main_query() || is_archive() ) {
    $p = get_page_by_path( $query->query['path'], '', [ 'collection', 'collaboration', 'page' ] );
    $query->set( 'p', $p->ID );
    $query->set( 'post_type', get_post_type( $p->ID ) );
    $query->is_single = true;
    $query->is_singular = true;
    $query->queried_object = $p;
    $query->queried_object_id = $p->ID;
    $query->is_home = false; // this makes sure the correct template loads, otherwise it will load index.php

    return $query;
add_filter( 'pre_get_posts', __NAMESPACE__ . '\\query_handler', 10, 1 );

Using this, I am able to access collections and collaborations using the permalink constructs mentioned above. But this seems hacky and several things to break as a result: Including but not limited to:

  • Home Page 404s
  • Pages with non-existent slugs (site.com/asdfasdf) load the index page (loop of posts)

Am I missing something? Is there a better way to do this?

  • 1
    +1 just for the use of modern php ;) For the question itself... I don't think it is wise to set the is_* etc properties there as they should be set by the query itself, and the code there should be minimal, the whole idea of using the pre_get_posts hook is do keep it down to one DB query, but the use of get_page_by_path will probably result in an additional one. – Mark Kaplun Jun 3 '17 at 16:48
  • @MarkKaplun You are correct that get_page_by_path does perform an additional query, but it is a cached function, so the performance hit isn't that bad. I'm just not sure of anyone other way to achieve this. – psorensen Jun 3 '17 at 17:02
  • why cached? anyway, I think you are trying to do too much there. Some things are just better left for the template to handle – Mark Kaplun Jun 3 '17 at 17:14
  • ... the important thing is to make the initial query to return something sensible for the loop, that can be used to determine the template to use, hopefully with some relevant post(s) aleady fetched from the DB – Mark Kaplun Jun 3 '17 at 17:18
  • 1
    it's easier if you sort out the query earlier, like via the request filter, before WordPress has made any decisions about what it's querying for. – Milo Jun 3 '17 at 18:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.