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.

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:


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):


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: spring-2012/first-article
       pagename: newsletter/spring-2012/first-article

how its displayed on another inspector page:


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


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

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

5 Answers 5


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).

  • I've edited the question to show the output from rewrite rules inspector, as these comments don't allow enough formatting. The rules for the CPT do show at the top of the list, before anything else - but not sure which of the rules below are "page" rules (not clear). Also, no collisions in post_name column.
    – somatic
    Jan 7, 2013 at 11:15
  • do you have any thoughts on the proper permalink structure? I've gone back to /%postname%/ since including /%category%/ only seems to confuse things further in the rewrite inspector. I've even seen people claim that /%category%/ is magically mapped for CPTs to mean "parent", though I don't find this to be true.
    – somatic
    Jan 7, 2013 at 11:17
  • That output appears to come for a different plugin, which is why the you don't see the "source" of the rule. Either way, it looks like your rule is matching fine. I've edited my answer to include a snippet to inspect the query vars earlier in the request to make sure they're being set correctly. Jan 7, 2013 at 15:35
  • As for the permalink structure, I'm not a fan of including the category. It's just another moving part that doesn't help anything, and posts can belong to multiple categories. If there's any chance at all that you might change the permalink structure in the future or would want another CPT to load without a slug prefix, I'd recommend "namespacing" your structure with something fixed and unique like /blog/%postname%/. Jan 7, 2013 at 15:46
  • it's the right plugin, but there are two pages: "rewrite analyzer" and "rewrite rules", both which offer a test URL. I tried the other page and it did show the source as "program" for the first indented group, and "page" for the second.
    – somatic
    Jan 7, 2013 at 18:39

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

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


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

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.

  • my code above didn't show it, but I do have page-attributes in the supports (which is really only about showing the metabox for assigning parentage, shouldn't affect permalinks) - but getting 404's. What permalink structure needs to be set? or does it matter?
    – somatic
    Jan 7, 2013 at 18:34
  • @somatic i just tested it again and its working fine, as for the permalink i'm not sure if it should matter, but any way i updated my answer.
    – Bainternet
    Jan 7, 2013 at 23:41
  • I can confirm that, with the addition of an argument array for 'labels' (without which CPT won't appear in admin), this basic example does work on a vanilla install of WP3.5 with twentlytwelve. But I'm noticing a few key differences in your register_post_type $args: I used 'rewrite' => array( 'slug' => 'newsletter', 'with_front' => false ), where you used just true. You also simply passed true for has_archive, where I passed the slug. However, when I matched your args, I still got 404... Still trying to track down where I'm going wrong.
    – somatic
    Jan 8, 2013 at 0:11

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.

  • well, if I disable everything, I won't have the custom post type that is the point of this question... but yes, I have tried even with twentytwelve theme. I'm still digging through the few necessary plugs to see what's mangling the query, as from what I can see, the permalinks themselves are well-formed.
    – somatic
    Jan 7, 2013 at 18:47
  • @somatic Of course not disabling yours :)
    – kaiser
    Jan 7, 2013 at 18:50
  • @somatic See update.
    – kaiser
    Jan 7, 2013 at 18:53

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.

  • You can also use the wrapper function get_queried_object() without intercepting the $wpdb object.
    – kaiser
    Jan 12, 2013 at 16:13
  • but which $query does that grab? I'm using this within a filter for parse_query, and need to specifically get the queried object of a specific $query that is passed from the filter...
    – somatic
    Jan 12, 2013 at 23:18
  • I just tried using the wrapper inside my filter for parse_query. It caused the same 404 errors again. I've got to find some way to replicate what get_queried_object() does without fouling up these filters...
    – somatic
    Jan 12, 2013 at 23:30

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.

  • 2
    I'm on 3.5. As of WP3.3.1 this issue on digiwp is fixed.
    – somatic
    Jan 7, 2013 at 21:12

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.