How to set a Page as the parent of a WordPress Custom Post Type

Custom Post Types in WordPress 3.x are designed to work from the root of your website i.e. a flat hierarchy.

What if you needed to set a particular page or post in your website as the parent to your new custom post types?

Here’s a solution for that.

Custom Post Types

In a recent project we created a “Case Study” custom post type and added several new case study posts to our WordPress site.

We also created a page template called “Case Studies” that listed the new custom post types.

Everything was rumbling along just super until we noticed, when viewing our new custom post types, the breadcrumb trail and URL permalink weren’t including the Case Studies page that we had clicked through from.

For example.

Our Case Studies page URL was /case-studies/ and the breadcrumb trail was also /case-studies/

When clicking on a case study custom post type “Land Owners”, both the URL and breadcrumb trail changed to “/land-owners/”.

Where had the Case Studies page gone in the hierarchy?

Let me be clear, this is not a WordPress bug but rather the way custom post types are currently implemented.  And yes, it is very annoying.

WordPress assumes that all custom post types are at the top of the hierarchy. i.e. they have no associated parent.

Unfortunately, this is exactly what we weren’t looking for.

The Solution

Of course WordPress may change the way custom post types work in future versions but here’s a fix for now.

We’re going to create a meta box for the custom post type that allows us to enter the ID of a page or post and associate that with the post_parent variable, effectively making that ID our parent page.

We’re using the custom post type called “casestudy” here so change that value to your own custom post type name and copy the following into your functions.php file.

[php] //Add the meta box callback function
function admin_init(){
add_meta_box("case_study_parent_id", "Case Study Parent ID", "set_case_study_parent_id", "casestudy", "normal", "low");
}
add_action("admin_init", "admin_init");

//Meta box for setting the parent ID
function set_case_study_parent_id() {
global $post;
$custom = get_post_custom($post->ID);
$parent_id = $custom['parent_id'][0];
?>
<p>Please specify the ID of the page or post to be a parent to this Case Study.</p>
<p>Leave blank for no heirarchy.  Case studies will appear from the server root with no assocaited parent page or post.</p>
<input type="text" id="parent_id" name="parent_id" value="<?php echo $post->post_parent; ?>" />
<?php
// create a custom nonce for submit verification later
echo ‘<input type="hidden" name="parent_id_noncename" value="’ . wp_create_nonce(__FILE__) . ‘" />';
}

// Save the meta data
function save_case_study_parent_id($post_id) {
global $post;

// make sure data came from our meta box
if (!wp_verify_nonce($_POST['parent_id_noncename'],__FILE__)) return $post_id;
if(isset($_POST['parent_id']) && ($_POST['post_type'] == "casestudy")) {
$data = $_POST['parent_id'];
update_post_meta($post_id, ‘parent_id’, $data);
}
}
add_action("save_post", "save_case_study_parent_id");
[/php]

There are three functions here.

  1. function admin_init() – This initialises the meta box for our casestudy custom post type, pointing to the call-back function that creates it
  2. function set_case_study_parent_id() – This is the call-back function that contains the HTML code needed to create the meta box on the casestudy custom post type when in edit mode
  3. function save_case_study_parent_id() – This saves the meta data from the new box into the DB

This will give you the following new meta box on your custom post type, allowing you to enter the ID of a page or post which it will set as the parent document.

We set the meta box value to the ID of our “Case Studies” page which displayed all the custom post types.

Great, now our Yoast breadcrumb trail was picking up the Case Studies parent page from our custom post type, but what about the URL?

There was just one thing left to do to bring everything together.

No slug had been explicitly defined for the custom post type, so they were being displayed in the URL from the server root with just the post title i.e. “/land-owners/”.

We needed to define a slug that would match up with our Case Studies page (slug “case-study”) so that our URL would read “/case-studies/land-owners/”.

Going back into the function that registered our custom post type, we added the following line to the $args array:

[php]‘rewrite’ => array(‘slug’ => ‘case-studies’, ‘with_front’ => true)[/php]

This rewrites the URL slug from the server root to include “case-studies” in front of the post title.

Just for reference, here’s the complete custom post type registration function with the above line included.

[php]/* Custom Post Types */
function register_custom_post_case_study() {

$labels = array(
‘name’ => _x(‘Case Studies’, ‘post type general name’),
‘singular_name’ => _x(‘Case Study’, ‘post type singular name’),
‘add_new’ => _x(‘Add New’, ‘Case Study’),
‘add_new_item’ => __(‘Add New Case Study’),
‘edit_item’ => __(‘Edit Case Study’),
‘new_item’ => __(‘New Case Study’),
‘view_item’ => __(‘View Case Study’),
‘search_items’ => __(‘Search Case Studies’),
‘not_found’ =>  __(‘No Case Studies found’),
‘not_found_in_trash’ => __(‘No Case Studies found in Trash’),
‘parent_item_colon’ => ”
);

$args = array(
‘labels’ => $labels,
‘public’ => true,
‘publicly_queryable’ => true,
‘show_ui’ => true,
‘query_var’ => true,
‘rewrite’ => array(‘slug’ => ‘case-studies’, ‘with_front’ => true),
‘capability_type’ => ‘post’,
‘hierarchical’ => false,
‘menu_position’ => null,
‘supports’ => array(‘title’,’editor’,’author’,’excerpt’,’page-attributes’)
);

register_post_type( ‘casestudy’ , $args );
}
add_action(‘init’, ‘register_custom_post_case_study’);
[/php]

Now our URL matches the breadcrumbs and the Case Studies pages is the parent of our custom post type.

You may have to go into your Permalinks settings and, without making any updates, click the Save Changes button to update the new URL structure.

If you found this useful and are living near Dublin, Ireland, why not catch up with us at our regular monthly Dublin WordPress meetup.

Dublin WordPress

Attention all WordPress Users in Dublin

If you live in and around Dublin and use WordPress as a blogger, developer, designer, trainer, business venture etc, come and join us on the last Thursday of every month at the Bull & Castle (next to Christ Church) from 6.30pm onwards.

More details at http://www.meetup.com/Dublin-WordPress/

About Wil

Wil is a professional web developer with a passion for leading edge technologies, security and server architecture. He spoke at WordCamp Sydney 2012, co-organizes the WordPress Sydney meetup, is on the sub-committee for WordCamp 2014 and contributes to the WordPress Core development. He likes pizza, sausages, chilies, beer, red wine and hyperdimensional physics.

  • Carsten Schäfer

    Hi Wil

    looks like I’ll be needing something similar to this. From when is this post? I see no date….

    my problem will be more generic. I got thousands of news items (technically posts) of an old system (different CMS) to migrate to WordPress posts. All pages have related posts, which are associated to a section/page.

    Each page’s related posts are queried from all the sub pages related posts, showing 2 most recent ones. Additionally, we can see ALL related posts from “here” and below.

    Of course, the url should append to current position

    /abc/def/article_1

    /abc/def/article_2 (even if belonging way down, not “here”)

    As your code above is hardcoding “Case Studies” (slug), this would not work.

    1. We need to give posts a page as parent (seems to work as of above)

    2. when clicked anywhere, it should appear as “member” of the current position (see example). If the parent page is stored in the post meta, I guess, the generated urls for related posts must be considering the post’s relation shiop and get the slug of the page to prepend the page slug.In order for that “wrong URL” to work, the link engine (rewrites) must accept that link, thus “know” that
    article_2 in
    /abc/def/article_2

    means “a posts ‘article_2′ ” (thus, searches for an article, after it has not found a page).

    This will be a challenge, if I maintain this requirement.
    But your post could be a valuable input to solve it. I’ll see. Not started yet.

    Thanks!

  • Tom Champion

    any reason why i’d be getting /issues/england/ rather than /england/issues/? england is the page i want as the parent page and issues is the custom post type.

  • http://www.alphagenetica.com/ Drew Hammond

    Why no monospaced font for your code snippets?

  • Gv

    Hi, i’ve done everything indicate in the article (except the ‘rewrite’ because it added 2 times what i wanted), it solve the url problem but now breadcrumb + post content display the 404 error message (it seems to use the index.php instead of single.php). Do you have any idea why it do this to me ?

    Best regards,
    Gautier

    • Gv

      OHHH i found why, in my register_post_type i deleted :

      ‘hierarchical’ => true,
      ‘has_archive’ => true,

      and it solved my problem ! :)

      • Gv

        Last thing i found : “One note that’s very, very important: your custom URLs won’t work until you go to Options → Permalink in wp-admin and re-save your current URL structure. This will flush WP’s current URL structure and add our new rewrite rules.”

  • ashrafs

    Is there a way to do this for an existing post type that is registered in a theme?