So there are a million snippets for how to get pages as parents of custom post types.

However, the reverse seems to be questionable. One would think, since everything in WordPress is technically a "post", this would be trivial. However, it is not.

Thus far, I have:

add_filter( 'page_attributes_dropdown_pages_args',
  function( $dropdown_args, $post = null ) {
    $dropdown_args['post_type'] = 'portal';
    return $dropdown_args;
  } );

And sure enough, pages now allow for me to select portal-type posts as parents. The URLs do seem to register correctly within wordpress, though they all 404.

How can I get wordpress to understand this hierarchy and load the pages that have posts as parents?

3 Answers 3


WordPress uses a set of rewrite rules to be able to convert an url into a database query.

The regex that handle urls for pages is very general, IIRC it is something like (.+.?)/?, it mathes pratically everything has not be already matched by other rules.

For this reason is not possible to write a rewrite rule that will work in your case: because you can't distinguish via regex that in a url like example.com/my-portal/sample-page the 'my-portal' part is a CPT and sample-page is a page.

Things become more complex if you have more levels of nesting: my-portal/my-portal-child/sample-page.

To handle this kind of urls, WordPress uses get_page_by_path() function: it explodes the page url by /, obtaining page slugs, then queries database for all the pages that have those slugs.

E.g if you have a page whose slug is "sample-page" and you set as parent for it the CPT "my-portal" WordPress calls:


but it doesn't return any result because it looks for a page with slug 'sample-page' whose parent is another page with slug 'my-portal'. That page doesn't exists, so you get the 404 error.

However, get_page_by_path() accepts as 3rd argument an array of post types: if you set it to array('page', 'portal') then the function will be able to correctly find the page.

So you can solve the issue by manually setting the page id (retrieved as explained above) into WP query vars.

The 'parse_request' hook is perfect for the scope:

  • it runs after the url has been parsed
  • it passes to hooking callbacks the instance of $wp object you can use to set query vars


add_action('parse_request', function ($wp) {
  // only if WP found a page
  if (isset($wp->query_vars['pagename']) && ! empty($wp->query_vars['pagename'])) {
    $page = get_page_by_path( // let's find the page object
        array('page', 'portal') // we need to set both post types
    if ($page instanceof WP_Post) { // if we find a page
        unset($wp->query_vars['pagename']); // remove pagename var
        $wp->query_vars['page_id'] = $page->ID; // replace with page_id query var

This code, in combination with the filter in OP, is all you need.

Note that code works even with nested hierarchical portals.

  • So the code by itself didn't work; var_dump($wp->query_vars) yielded 'attachment' => string 'contact' -- that's it. I'll play around, though - I think you're on the right track. Commented Feb 3, 2015 at 0:00
  • @Qix that's weird. I've just tested in my local install using code in OP to have portal as page parent, and then my code to adjust query. It worked with no problems. Only think I did is register portal CPT as hierarhical. If $wp->query_vars does not contain any 'pagename' is very problably a rewrite issue.
    – gmazzap
    Commented Feb 3, 2015 at 13:46
  • I'm sure it has to do with something else I'm doing. This is probably the correct answer but the one I posted is a sure-shot way of doing it. Commented Feb 3, 2015 at 19:15

So @gmazzap's answer was a point in the right direction. However, for whatever reason, var_dump($wp->query_vars) was only showing an array with one key, attachment, which didn't help at all.

However, it did show how I could achieve this using a little more trickery.

The below script gets a page only if the hierarchy is correct; that is, each slug's parent matches a post with the appropriate slug (0 for the first slug, of course).

As well, it skips paths with 0 or 1 slugs simply because those usually mean special pages that WordPress should handle (also, it doesn't really matter in terms of page hierarchy...)

And no, the query can't really be optimized too much. This is due to the fact MySQL doesn't support recursive selections/joins like you'd want in a hierarchical data model.

function get_page_by_slug_path( $path ) {
  global $wpdb;
  global $table_prefix;

  $slugs = explode(

  # Skip if slugs length is 1 or 0, of course.
  #   A single 'slug' URL may mean something completely different, so
  #   we'll bank on WordPress knowing what to do with it.
  if( count( $slugs ) < 2 ) {

  $parents = [ 0 ];
  foreach( $slugs as $slug ) {
    $sql = $wpdb->prepare(
      "SELECT ID FROM ${table_prefix}posts WHERE post_name='%s' AND post_parent IN ("
      . implode( ',', $parents )
      . ")",

    $results = $wpdb->get_results( $sql );

    if( count( $results ) === 0 ) {
      return null;

    $parents = array_map(
      function( $elem ) {
        return $elem->ID;


  if( count( $parents ) > 1 ) {
    trigger_error( E_USER_WARNING, "Multiple IDs for this page slug: "
      . implode( ', ', $parents ) );

  return intval( $parents[0] );

add_action('parse_request', function ($wp) {
  $path = parse_url(
    "http://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'],

  $pageID = get_page_by_slug_path( $path );

  if( $pageID ) {
    $wp->query_vars = [ 'page_id' => $pageID ];
  add_action('admin_menu', function() { remove_meta_box('pageparentdiv', 'chapter', 'normal');});
  add_action('add_meta_boxes', function() { add_meta_box('chapter-parent', 'Part', 'chapter_attributes_meta_box', 'chapter', 'side', 'high');});
  function chapter_attributes_meta_box($post) {
    $post_type_object = get_post_type_object($post->post_type);
    if ( $post_type_object->hierarchical ) {
      $pages = wp_dropdown_pages(array('post_type' => 'part', 'selected' => $post->post_parent, 'name' => 'parent_id', 'show_option_none' => __('(no parent)'), 'sort_column'=> 'menu_order, post_title', 'echo' => 0));
      if ( ! empty($pages) ) {
        echo $pages;
      } // end empty pages check
    } // end hierarchical check.

Try this answer by : http://janina.tumblr.com/post/3588081423/post-parent-different-type

  • Please read OP again. This is for pages with post parents. I've already done this for pages, but they're showing as 404s. Commented Jan 26, 2015 at 15:21

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.