Custom post types in WordPress are a great way to add and structure content to a website. Unlike a page however, custom post types cannot have a parent page assigned to them, at least not directly. This could be a major drawback if you want to have a custom post type nest under a specific page in menus and in the URL.
Why Add a Parent to a Custom Post Type
For example, if you have 2 pages, an about page and an employees page, the employees page can be nested under the about page to form a hierarchy for the website. This nesting is visible in the URL structure(www.mysite.com/about/employees), breadcrumbs, and navigation menus. Now we add the “staff” custom post type into the mix. The employees page contains a loop to display and paginate all of the staff posts, but clicking on a staff member reveals that the staff member single-staff page is rendered at www.mysite.com/staff/15 and not www.mysite.com/about/employees/15. The employees navigation menu item would not have a current or ancestor class to indicate you are at that area of the site, and any breadcrumbs plugins or functions will fail to show about or employees in the site hierarchy when on a single-staff page.
The Solution to Nesting a Custom Post Type Under a Page
To solve these issues we can do a few things. Let’s look at each issue individually.
Change a Custom Post Type URL
To change the url structure of a custom post type so that a single-staff page uses the www.mysite.com/about/employees/15 url we need to add an argument when we define the post type. If you are registering the post type manually add the ‘rewrite’ argument.
$labels = array( // ... ); $args = array( 'labels' => $labels // ... 'rewrite' => array( 'slug' => 'about/employees'), // update the URL to reflect the CPT/Page hierarchy // ... ); register_post_type( 'staff', $args );
If you are using a plugin to create the custom post type then add a hook to your functions.php file.
//functions.php add_action( 'init', 'updateStaffPostType', 99 ); /** * updateStaffPostType * * @author Joe Sexton <joe@webtipblog.com> */ public function updateStaffPostType() { global $wp_post_types; if ( post_type_exists( 'staff' ) ) { // exclude from search results $wp_post_types['staff']->rewrite = array( 'slug' => 'about/employees'); } }
To get this to work correctly you need to go to wp-admin and flush your permalinks.
Adding a Post Parent to a Custom Post Type
The next step is to add a parent to the custom post type. WordPress sadly is not reflecting this update in the URL structure or the nav menus, but it will help with breadcrumbs and any other places where your code or a plugin is referring to a post’s parent. There are two ways to add a parent to a custom post type. The first option is to add a metabox to allow users to select the parent page.
define( 'STAFF_PAGE_PARENT_ID', '2' ); // Add parent-page metaboxes to set the parent page for the custom post types... // This updates the breadcrumbs to reflect the CPT/Page hierarchy // add staff parent-page meta boxes add_action( 'add_meta_boxes', 'addStaffParentMetabox' ); add_action( 'wp_insert_post_data', 'saveStaffParent', '99', 2 ); /** * addStaffParentMetabox * * @author Joe Sexton <joe@webtipblog.com> */ function addStaffParentMetabox() { add_meta_box( 'staff_cpt_parent', 'Staff Parent Page', array( $this, 'renderStaffParentMetabox' ), 'staff' ); } /** * renderStaffParentMetabox * * @author Joe Sexton <joe@webtipblog.com> */ function renderStaffParentMetabox() { global $post; wp_nonce_field( 'stc_cpt', 'staff_parent_custom_box' ); $pages = get_pages(); echo 'Select the parent page'; echo '<select name="staff_parent">'; echo '<option value=''>Choose a page...</option>'; foreach( $pages as $page ){ echo '<option value="'.$page->ID.'"'; // of the post's parent is the current item in the loop, select it if ( $page->ID == $post->post_parent ) { echo ' selected'; // this condition is unnecessary but will allow you to default the // metabox to a specific page so users do not need to manually enter it } else if ( empty( $post->post_parent ) && $page->ID == STAFF_PAGE_PARENT_ID ) { echo ' selected'; } echo '>'.$page->post_title.'</option>'; } echo '</select>'; } /** * saveStaffParent * * @author Joe Sexton <joe@webtipblog.com> * @param array $data * @param array $postarr * @return array */ function saveStaffParent( $data, $postarr ) { global $post; if ( !wp_verify_nonce( $_POST['staff_parent_custom_box'], 'stc_cpt' ) ) return $data; // verify if this is an auto save routine. // If it is our form has not been submitted, so we dont want to do anything if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return $data; if ( $post->post_type == "staff" ){ $data['post_parent'] = $_POST['staff_parent']; } return $data; }
I defined the STAFF_PAGE_PARENT_ID constant for convenience, simply to default the parent to a certain page so user’s don’t need to select it each time they create a post type. That is not necessary if you do not know the page id that will be the parent page programmatically.
A second option to add a page as a custom post type parent is to simply store a page id to the custom post type any time one is created or saved. This option does require that you know the page id of the parent page in code.
define( 'STAFF_PAGE_PARENT_ID', '2' ); add_action( 'wp_insert_post_data', 'saveStaffParent', '99', 2 ); /** * saveStaffParent * * @author Joe Sexton <joe@webtipblog.com> * @param array $data * @param array $postarr * @return array */ function saveStaffParent( $data, $postarr ) { global $post; if ( !wp_verify_nonce( $_POST['staff_parent_custom_box'], 'stc_cpt' ) ) return $data; // verify if this is an auto save routine. // If it is our form has not been submitted, so we dont want to do anything if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return $data; if ( $post->post_type == "staff" ){ $data['post_parent'] = STAFF_PAGE_PARENT_ID; } return $data; }
Adding Menu Ancestors to Custom Post Types
The final step in this process is to add classes to ancestors of the custom post type in navigation menus. This allows parents to be styled to indicate they are current or active in the menus. I use the nav_menu_css_class filter to add css classes as needed.
add_filter( 'nav_menu_css_class' , 'addAncestorMenuClasses', 10, 2 ); /** * add ancestor menu classes * * @author Joe Sexton <joe@webtipblog.com> * @param array $classes * @param object $item * @return array */ function addAncestorMenuClasses( $classes, $item ){ $post = get_post(); if ( !$post ) return $classes; // if on a press release CPT, add ancestor class to the 'news' page menu items if ( $item->object_id == $post->post_parent && get_post_type() == 'staff' ) { $classes[] = 'current_page_ancestor'; $classes[] = 'current-page-ancestor'; $classes[] = 'current-menu-ancestor'; } return $classes; }
This will work great if you have a single page parent and no depth to your page hierarchy. In our example we have two pages so if we want to add the ancestor classes to the about page in addition to the employees page then we need to get the parent(s) of the parent and recursively add classes up the hierarchy.
Awesome article! I had to change one of your functions a little bit because the array wasn’t correct but it got me going in the right direction! Thank you 🙂
If you’d like me to send you my code, let me know.
Thank you sir. Covers all tasks to be aware of.
I had to rewrite the whole thing for our environment, but this gave me a lot of direction.
Interesting, it give some starting points, but in my opinion it’s poorly more a cheat then a real solution, because you use one predefined fixed page as parent and do it not the dynamical way. And what’s about more then one custom post type with different name?