Getting hierarchical custom post type permalinks to work just like pages

I’ve dug through every question here on custom post type permalinks, but most seem to be either problems with custom taxonomy rewrites, or the obvious missing of flush_rewrite_rules(). But in my case, I’m only using a custom post type (no taxonomy), set to be hierarchical (so I can assign parent-child relationships), with the proper “support” for the attributes metabox, etc, etc. I’ve flushed rewrite rules a thousand different ways. I’ve tried different permalink structures. But child URLs always result in 404!

I originally had independent custom post types for the “parent” and “child” elements (using p2p), and I probably would have had no trouble using a taxonomy for the “parental” grouping – I know those would be semantically more accurate. But for the client, it is easiest for them to visualize the hierarchy when the “posts” are displayed in the admin just like pages are: a simple tree where children appear underneath the parent, prefixed with a “–“, and in the proper order. Also, various methods for assigning order via drag-n-drop can be used. Grouping via taxonomy (or p2p) results in a flat list of “posts” in the admin listings, which is simply not as visually obvious.

Read More

So what I’m after is literally the exact same behavior as core “pages”, but with my custom post type. I’ve registered the post type just as expected, and in the admin it works perfectly – I can assign a parent and a menu_order for each newsletter “post”, they appear correctly in the edit listings:

Spring 2012
— First Article
— Second Article

And their permalinks appear to be constructed properly. In fact, if I change anything about the structure, or even alter the rewrite slug when registering the post type, they automatically update correctly, so I know something’s working:

http://mysite.com/parent-page/child-page/                  /* works for pages! */
http://mysite.com/post-type/parent-post/child-post/        /* should work? */
http://mysite.com/newsletter/spring-2012/                  /* works! */
http://mysite.com/newsletter/spring-2012/first-article/    /* 404 */
http://mysite.com/newsletter/spring-2012/second-article/   /* 404 */

I also have standard core “pages” with hierarchical relationships created, and they look just the same in the admin, but they actually work on the front-end too (both parent and child URLs work fine).

My permalink structure is set to:

http://mysite.com/%postname%/

I’ve also attempted this (just because so many other answers seemed to indicate it was needed, though it didn’t make sense in my case):

http://mysite.com/%category%/%postname%/

My register CPT args include:

$args = array(
    'public'                => true,
    'publicly_queryable'    => true,
    'show_ui'               => true,
    'has_archive'           => 'newsletter',
    'hierarchical'          => true,
    'query_var'             => true,
    'supports'              => array( 'title', 'editor', 'thumbnail', 'page-attributes' ),
    'rewrite'               => array( 'slug' => 'newsletter', 'with_front' => false ),

The only visible difference between my custom post type children and normal page children, is that my CPT has the slug at the beginning of the permalink structure, then followed by the parent/child slugs (where pages just begin with the parent/child slugs, no “prefix”). Why this would foul things up, I don’t know. Plenty of articles seem to indicate that this is exactly how such hierarchical CPT permalinks should behave – but mine, though nicely formed, don’t work.

What also baffles me is when I examine the query_vars for that 404 page – they seem to contain the correct values for WP to “find” my child pages, but something’s not working.

$wp_query object WP_Query {46}
public query_vars -> array (58)
'page' => integer 0
'newsletter' => string(25) "spring-2012/first-article"
'post_type' => string(10) "newsletter"
'name' => string(13) "first-article"
'error' => string(0) ""
'm' => integer 0
'p' => integer 0
'post_parent' => string(0) ""
'subpost' => string(0) ""
'subpost_id' => string(0) ""
'attachment' => string(0) ""
'attachment_id' => integer 0
'static' => string(0) ""
'pagename' => string(13) "first-article"
'page_id' => integer 0
[...]

I’ve tried this with various themes, including twentytwelve, just to be sure it’s not some missing template on my part.

Using Rewrite Rules Inspector, this is what shows up for the url:
http://mysite.com/newsletter/spring-2012/first-article/

newsletter/(.+?)(/[0-9]+)?/?$   
       newsletter: spring-2012/first-article
           page: 
(.?.+?)(/[0-9]+)?/?$    
       pagename: newsletter/spring-2012/first-article
           page: 

how its displayed on another inspector page:

RULE:
newsletter/(.+?)(/[0-9]+)?/?$
REWRITE:
index.php?newsletter=$matches[1]&page=$matches[2]
SOURCE:
newsletter

This rewrite output would lead me to believe that the following “non-pretty” permalink would work:

http://mysite.com/?newsletter=spring-2012&page=first-article

It doesn’t 404, but it shows the parent CPT item “newsletter”, not the child. The request looks like this:

Array
(
    [page] => first-article
    [newsletter] => spring-2012
    [post_type] => newsletter
    [name] => spring-2012
)

Related posts

Leave a Reply

5 comments

  1. This is my first time participating here on Stack Exchange, but I’ll give this a go and see if I can help point you in the right direction.

    By default, hierarchical CPTs behave exactly the way you’ve described. The unique slug prefix, “newsletter” in this case, is to let the rewrite engine know how to tell requests for different post types apart.

    Those CPT registration args look fine, but when a CPT is requested, the pagename query var should not have a value and the name query var should be the same as newsletter here, so it appears that there’s a conflict somewhere in your setup.

    To help debug, install and activate the Rewrite Rules Inspector Plugin, then visit the screen at “Tools -> Rewrite Rules.”

    1. While looking at the list, all of your rules for the newsletter CPT should be listed before any page rewrite rules. Verify this is the case by scanning the “Source” column.
    2. If that checks out, input the URL for your “first-article” CPT in the “Match URL” field and click the “Filter” button to see which rule is matched. It should match a newsletter rule and a page rule, but the newsletter rule should be first.

    If that doesn’t reveal any issues, search the post_name column in wp_posts to find other posts with the “first-article” slug to see if there might be a collision. Do a search for “newsletter” as well, just to be sure.

    Add the following snippet to inspect your query vars early in the request to check and see which ones are being set by the rewrite rule match (visit the child CPT on the front end). If the pagename var doesn’t appear here, then it’s being set by something later in the request:

    add_filter( 'request', 'se77513_display_query_vars', 1 );
    
    function se77513_display_query_vars( $query_vars ) {
        echo '<pre>' . print_r( $query_vars, true ) . '</pre>';
    
        return $query_vars;
    }
    

    Any plugins/functions that modify the query vars could be causing a conflict, so disable them if you still see issues. Also flush your rewrite rules after each step, especially if the request was matching a wrong rule in the second step above (keep the “Permalinks” screen open in a separate tab and just refresh it).

  2. The parent/Child permalink Works out of the box as long as you set

    'hierarchical'=> true,
    'supports' => array('page-attributes' ....
    

    Update:

    I just tested it again and it works as expected, with this test case:

    add_action('init','test_post_type_wpa77513');
    function test_post_type_wpa77513(){
        $args = array(
            'public' => true,
            'publicly_queryable' => true,
            'show_ui' => true, 
            'show_in_menu' => true, 
            'query_var' => true,
            'rewrite' => true,
            'capability_type' => 'post',
            'has_archive' => true, 
            'hierarchical' => true,
            'supports' => array( 'title', 'editor', 'thumbnail', 'page-attributes' )
        ); 
    
        register_post_type( 'newsletter', $args );
    }
    

    and permalink set to /%postname%/ I get newsletter/parent/child working just fine.

  3. Aside from asking »Have you tried turning it off and on again?«, I also have to ask “Do you have any plugins active and is your Theme doing anything to the permalinks (for e.g.: registering taxonomy, post types, adding rewrite rules, etc.)?

    If so: Does this still happen, after you disabled all plugins and switched to TwentyEleven?
    enter image description here

    To debug further, head over to GitHub and grab Toschos “Rewrite” Plugin. Then switch over to the official plugin repo on wp.org and grap the MonkeyManRewriteAnalyzer Plugin. I even wrote a little extension to glue both a little bit together. This will give you lots of details about your setup.

  4. So after confirming that I could get the expected hierarchical page behavior with a completely clean install and just a single CPT declaration, I knew the fault lay somewhere within my own plugin that I use to handle CPT creation (very complex, handles custom metaboxes, taxonomies, etc). Problem was, despite all the advice to check for rewrite or query problems, I couldn’t see anything obviously wrong. I checked every filter and hook, viewing the query at each point and seeing nothing that would cause the 404.

    So I was left with the task of manually disabling/enabling each of 9 large classes, and then, finding at least 2 of them to be causing the 404, going through each function one by one and disabling/enabling them, and then tracing line by line within those functions – a brute force attempt to see exactly what was causing the 404, even if I didn’t know why.

    That’s when I found there’s a consequence to using $query->get_queried_object(). It would seem using this wrapper function actually modifies $query itself, which I thought I was returning unchanged at the end of my function. Three filters across 2 classes were involved that modified the query: parse_query, posts_orderby, posts_join – and all of the callback functions were calling $query->get_queried_object() on the passed $query arg to then run some conditional tests and sometimes modify the query vars (like for special sort order cases). Strangely enough, these functions actually worked fine at what they were designed to do, despite my use of the function. I never noticed anything wrong with the returned $query before!

    Somehow, after more than a year of development and usage on dozens of live production sites, I had never experienced any adverse effects from this mistake. Only when I ventured into hierarchical CPTs did this one little difference suddenly break things. That’s part of what threw me so hard with this – as best I could tell, my query filters were trustworthy!

    I confess I still don’t know exactly why calling this function breaks just this one small aspect of CPT child pages – and yet never manifested any other problems! But it was clear that using it within a filter callback mangled the returned $query somehow. By removing this call, my 404 errors disappeared.

    Thanks for all the tips – wish I could split the bounty, as I gained insight from each answer, even if the ultimate solution was somewhat unrelated. This has been an educational lesson in not blindly trusting your code, even if it’s been working for you reliably for a long time, or it isn’t producing any obvious errors.

    Sometimes, as kaiser’s chart so neatly embodies, you just have to start throwing switches until the lights come back on. In my case, I had to take the same troubleshooting strategy all the way down to the individual lines in a function before I could see the problem.

  5. Are you running the most current version of WP? I guess that’s the first question (like when you call tech support and they ask you if your computer is plugged in!), since I’ve read that this issue was addressed in the most current version update.

    There’s a post on digwp.com that addresses this issue (http://digwp.com/2011/06/dont-use-postname/). Apparently WP can’t easily tell the difference between posts and pages, and if you start your urls with a text-based option, WP triggers a flag that creates a rule for every page/post that you have. If you have a lot of content on your site, that can result in a huge number of requests and your server will probably time out, which will result in a bunch of 404’s even if the pages are there.

    So. If you use a number based structure first, then your text based structure, I’d wager that your 404 issue would be resolved. Now, considering SEO issues, I wouldn’t use a date structure, but making that decision will be logical enough for you.