Enforcing canonical URLs with multiple custom post types

I’m working on a project with several custom post types, where one of the post types is naturally (but not actually in the WordPress sense) the ‘parent’ of the others. For example, say I have a custom post type called ‘book’ and another one called ‘character’, and say I want to have the following custom URL structure:

books/[book-name]/characters/[character-name]

Thus, the canonical URL for the post about the character Thing One in the book The Cat In the Hat would be:

Read More
 books/cat-in-the-hat/characters/thing-one

A character is associated with a book with a post meta row.

I’ve created custom rewrites to make it work…

add_rewrite_rule (
    '^books/([^/]+)/characters/([^/]+)/?$',
    'index.php?book=$matches[1]&character=$matches[2]&post_type=character'
);

…but I’d also need to make sure that the ‘character post’ in the URL belongs to the ‘book’…

books/cat-in-the-hat/characters/thing-one     //good
books/anna-karenina/characters/count-vronsky  //good
books/anna-karenina/characters/thing-one      //bad

This can’t happen automatically: I have to hook into a filter or an action to check whether the character matches the book, and if not, either redirect to the correct, canonical URL or force a 404.

Where’s the best place to accomplish this check? My choices (so far) are:

  • The request filter — i.e. checking before the main query is instantiated.
  • The various WP_Query filters — i.e. make WP_Query do the checking for me by adding a post meta constraint.

I realize this is rather an open-ended question and probably a matter of taste, but any insight into the most code and database efficient way of doing things would be appreciated. Thanks.

Related posts

Leave a Reply

1 comment

  1. I tackled subordinate post types with a similar vein but was not as strict on the canonical url aspect; however, I ran into a similar situation with deeplinking custom WooCommerce product type links.

    I leveraged the template_redirect hook to push a 404 if it didn’t match the route and the post_type_link to ensure the meta tag for canonical link and all the_permalink() and get_permalink() references matched the intended link.

    A snippit of the way I would recommend handling this:

    add_action( 'template_redirect', 'wp20140320_template_redirect' );
    public function wp20140320_template_redirect(){
        global $wp_query, $post;
    
        // the character post type has the book id set as meta?
        // or could set as post_parent if you don't have characters heirarchical
        $book_id = get_post_meta( $post->ID, '_book_id', true );
    
        // compare_parent_slug_to_id to check required $book_id against set parent book slug
        if( $post->post_type == 'character' && ! compare_parent_slug_to_id( $book_id ) ){
    
            // set is_404 since post type is character and parent slug does not match set
            $wp_query->is_404 = true;
            status_header(404);
            include get_404_template();
            exit; // maybe a better way to gracefully exit?
    
        }
    }
    

    Modified from source