Hello, beautiful readers! In this post we are going to solve a very specific need for custom post types in WordPress. I’ve actually written about it two years ago, which is to select post parent from another post type. But today you’ll learn more deeply on the same topic with a live example.
Say there’s a project for building online courses website, we plan to have two custom post types in WordPress, which are “Courses” and “Lessons”. Registering two CPTs won’t be a big deal for us but the not-so-easy part is we’d like to set the URL structure of the lessons in such format:
http://my-online-course.com/lesson/[course-name]/[lesson-name]/
Taking my AngularJS series for example, it would look like:
http://my-online-course.com/lesson/angularjs-wp-api/lesson-1-using-angularjs-in-wp-theme/
So the workflow to manage our online course would be like, we always add a new course first, input some course information, prerequisites, curriculum etc. After that we’ll create as many lessons as needed, and for each lesson, we have to set the “parent post” for it, which is the course it’s attached to.
If you find this use case compelling, keep reading and here’s what I’m going to show you (preview the final demo if you’d like):
- Registering two custom post types: Courses and Lessons.
- Updating the “Parent” meta box so we can choose a “course” as the parent post for a “lesson”.
- Setting the exactly URL structure we want.
- Updating the permalink of the CPT to reflect the new URL structure.
Ready? Let’s go!
1. Registering custom post types
Registering custom post types should be relatively easy for you. Just visiting the WordPress Codex to see what arguments to set or you may even use some cool GUI tools to write the snippets much quicker.
An important note here is the Courses must be hierarchical but NOT the Lessons. This is a requirement for this tutorial to work. Check the gist above (line 13 and 26), be sure you set them right. We do so for two reasons:
- Courses need to be hierarchical because we can only use the
wp_dropdown_pages()
(more on this soon) to get the courses if it’s hierarchical. - Lessons can’t be hierarchical because if so WordPress will use a different way to process its permalink and that’ll break the URL structure hacks we’re going to use.
2. Updating the “Parent” meta box
This step is kind of cool that, by default a post type needs to be hierarchical and also supports “page-attributes“, to get the “Parent” field (meta box) to set the parent post, which is a select box to display a bunch of posts to choose from.
Since we didn’t set the Lessons to be hierarchical, we can’t have WordPress to create the field for us. But we can create our own Parent post field by registering a new meta box for it (line 2-5).
And even more, by naming the select to “parent_id
” (line 9, wp_dropdown_pages()
can be not only used to get pages, but also any other hierarchical post types), a default filed name that all hierarchical post type has in common, we don’t have to write an extra function to save the value to post meta, WordPress will process that automatically.
3. Setting the exactly URL structure
To build a custom URL structure for our CPT, there are three functions to do the job: add_rewrite_tag
, add_permastruct
and add_rewrite_rule
.
If you have ever set the permalinks in your WordPress Settings, you must be familiar with built-in URL structure tags like “%year%
“, “%monthnum%
“, “%day%
” or “%postname%
“, which must start and end with a %
symbol. Now in our case we’re going to create a new structure tag “%lesson%
“, so we can use it to build the custom URL structure for “Lessons” post type.
The next is to use add_permastruct()
to create a new permalink structure for the “Lessons” post type. Here we use a structure tag “%course%” that doesn’t really exist (we didn’t register it with add_rewrite_tag
). Because we don’t really want to use it globally in WordPress, it’s just like a shortcode so we can replace it with the real course name (the lesson’s parent) in the next step.
And then we need to create a new rewrite rule that tells WordPress, when someone visits URLs in this very format (/lesson/%course%/%lesson%/
), WordPress should redirect them to another URL (/index.php?lesson=%lesson%
), which is the true URL with query strings (a.k.a. the ugly link) so WordPress can actually do the queries and fetch posts for us.
The fun part is we don’t really need every tag in the URL structure to be in part of the query strings. In our case, we need only %lesson%
but not %course%
, in fact, we can even say %course%
is totally useless for WordPress to fetch this singular post, we use it to decorate the URL so our visitors will know “this lesson belongs to that course“. With only %lesson%
, the query can work fine.
4. Updating the permalink for our custom post type
In the final step, we have to update the permalink for the Lessons post type. Here comes a powerful filter called post_type_link
, we can use it to modify the permalink for any post type in WordPress. Now the most important task left is to interpret %course%
tag into the slug of the parent course we’ve set. You can see the gist above and that’s what line 7-10 is for.
Bonus tip: using completely delete plugin to better manage the courses and lessons
My plugin Completely Delete is made to better manage hierarchical posts. With it you can delete a parent post and also all its child posts (include attachments) at the same time. So it would be very helpful in our case, when trashing or deleting a certain course, all lessons can be gone with it.
Now things should work as I’ve promised you at the beginning of this tutorial. Check the demo screencast and see it actually works! Also, you can grab the full functions.php you need for this project.
I hope you enjoy this tutorial and learn something new with me. The Lunar New Year in Taiwan is coming and I wish everyone read so far will have an awesome Monkey Year! Leave comments or shoot me an email (yoren [at] 1fix.io), I’ll get back to you ASAP even when I’m on a 9-day vacation.
I’m probably going to try this myself after writing… but thought i’d ask you as well in case i fail! … could this method be extended to 3 post types and this different URL structure?
Essentially i have Games, Characters, Movesets as post types.
For the Movesets, i want the URL to be mysite.com/game/[game-name]/[character-name]
I don’t want the name of the moveset at all, since it’s just a hand-merged name
I believe so. Just you need some time to test around the rewrite rules. Good luck!
Hmm i’m not sure … my first attempt seems to be suggesting the Moveset post can’t have parents from 2 other post types. ie on the post edit screen only one parent gets saved.
Hi Yoren,
Great tutorial, the url rewrites seem to be affecting the entire site by redirecting everything to the home page if the url does not match the /lesson/course structure.
Any ideas?
Hi Jonathan, I just tested on my local dev machine but it worked well with a new post type I created. I suppose you have a more complex URL structure so the permalinks might conflict with each other.
It takes time to debug such issue… really painful. I’ve been there once before…
Great article man. It really helped me connect two custom post types I had on my project.
Thank you very much
Hey Eric, Glad I can help!
Hi Joren.
Great post.
I have many parent post and i can not choose parent post because its very long. How to limit and search drop box with ajax.
Sorry for my bad english
Hi, Chung, sorry for the late reply. To integrate AJAX into the parent metabox is out of the scope of this post. Sorry about that.
That is very interesting; you are a very skilled blogger. I have shared your website in my social networks!
Hi,
Thanks for this working well. Only one issue though, when I set my custom permalinks using the code above all the child posts use the parent pag template. I cant even target the child post type via php.
The child is named low_profile and parent is vehicle.
The following code only works when set to vehicle and displays the incorrect template on the page:
function my_theme_redirect() {
global $wp;
$plugindir = dirname( __FILE__ );
//A Specific Custom Post Type
if ($wp->query_vars[“post_type”] == ‘low_profile’) {
$templatefilename = ‘single-low_profile.php’;
if (file_exists(TEMPLATEPATH . ‘/’ . $templatefilename)) {
$return_template = TEMPLATEPATH . ‘/’ . $templatefilename;
} else {
$return_template = $plugindir . ‘/includes/templates/’ . $templatefilename;
}
do_theme_redirect($return_template);
}
}
Managed to fix it up, was changing the permalink and wordpress got confused.
hey Kyle, good to know you got it figured out!
Hi there !
Thanks for this great post ! It really helped me out.
However, I’ve spend lot of time to try to make it work for my purpose. Where “courses” become “book”, and “lesson” become “chapter”, here’s my code :
/* Add our own URL strucutre and rewrite rules */
public function add_rewrite_rules() {
// Books
add_rewrite_tag(‘%book%’, ‘([^/]+)’, ‘book=’);
add_permastruct(‘book’, ‘/%book%’, false);
add_rewrite_rule(‘^book/([^/]+)/([^/]+)/?’,’index.php?book=$matches[2]’,’top’);
// Chapters
add_rewrite_tag(‘%chapter%’, ‘([^/]+)’, ‘chapter=’);
add_permastruct(‘chapter’, ‘/%book%/%chapter%’, false);
add_rewrite_rule(‘^chapter/([^/]+)/([^/]+)/?’,’index.php?chapter=$matches[2]’,’top’);
}
/* Set permalink for Chapter */
public function permalinks($permalink, $post, $leavename) {
$post_id = $post->ID;
if($post->post_type != ‘chapter’ || empty($permalink) || in_array($post->post_status, array(‘draft’, ‘pending’, ‘auto-draft’)))
return $permalink;
$parent = $post->post_parent;
$parent_post = get_post( $parent);
// If no parent for the chapter
if ($post->post_type == ‘chapter’ && $parent == 0) {
$permalink = str_replace(‘%book%’, __( ‘chapter’ ), $permalink);
return $permalink;
}
$permalink = str_replace(‘%book%’, $parent_post->post_name, $permalink);
return $permalink;
}
/* Set permalink for Chapter */
public function permalinks($permalink, $post, $leavename) {
$post_id = $post->ID;
if($post->post_type != ‘chapter’ || empty($permalink) || in_array($post->post_status, array(‘draft’, ‘pending’, ‘auto-draft’)))
return $permalink;
$parent = $post->post_parent;
$parent_post = get_post( $parent);
// If there is no parent for the chapter
if ($post->post_type == ‘chapter’ && $parent == 0) {
$permalink = str_replace(‘%book%’, __( ‘chapter’ ), $permalink);
return $permalink;
}
$permalink = str_replace(‘%abbm_book%’, $parent_post->post_name, $permalink);
return $permalink;
}
I have two issues with this code :
1) It doesn’t work at all if the permalinks are not set as “simple”
2) The chapter works well if the permalinks are set “post name”, but not the book which redirect to 404.
I would like it to work completely regardless of the structure selected. i’m running out of idea. So if you find time to give me an advice, I would really be very grateful.
Thanks !
Oops. Some code has been pasted twice. Sorry, I can’t edit.
Of course, I only used once the part : /* Set permalink for Chapter */
Thanks, this is that I want.
But Still not understand with code. im newbie blogger, only know write
I am trying to set:
add_permastruct(‘lesson’, ‘/lesson/%lesson%/course/%course%/’, false);
But instead of my course single page is showing my parents lesson single page, how can i solve this?
+1 ! It is the same for me.
Thanks!
Hey Aly, please read “3.Setting the exactly URL structure” carefully that it depends on how you set the whole “my_add_rewrite_rules” function. Everything is connected here and it’s a very tricky one!
Hello, great job, thank you.
How can we add “featured image” to this post types?
Amazing, thanks so much!
Amazing Yoren!!
This was so helpful and smart. Keep on coming with these great solutions. I have been looking for a way to make 2 custom post types communicated and you explained it so well.
Thank you!
my question then becomes, how do you loop through the courses CPT with PHP? How does it know that lessons are attached to courses?
By checking the post_parent we can see the connections then.
Hi, thank you very much for he detailed post.
I have a question though, I want to add another column to the admin page so I can filter the seccions by the parent post. Is it possible? if so, can you point me some direction?
Thanks again ✌🏼
Hi, Thank you for a very useful post. But I’ve an issue that I dont get a different slug for posts with same title. WordPress append 1,2,3… for duplicate posts.How can I solve this?
Hey Anisha, I don’t think you have to “solve” that. Can’t relate much in such scenario.
That is something that needs to be solved, as you cannot have two posts (lessons) with the same name, even when the slug (course) is different.
Yoren!
First of all, thank you so much for share with everyone your knowledge! Your post works very well for me. But why if I want to parent more than one post? For example, choose “course” and “teacher” as the parent post for a “lesson”.
I’ve tried this but it didn’t work for me 🙁
function my_add_meta_boxes() {
add_meta_box( ‘lesson-parent’, ‘Course’, ‘lesson_attributes_meta_box’, ‘lesson’, ‘side’, ‘high’ );
add_meta_box( ‘teacher-parent’, ‘Teacher’, ‘teacher_attributes_meta_box’, ‘lesson’, ‘side’, ‘high’ );
}
add_action( ‘add_meta_boxes’, ‘my_add_meta_boxes’ );
function lesson_attributes_meta_box( $post ) {
$post_type_object = get_post_type_object( $post->post_type );
$pages = wp_dropdown_pages( array( ‘post_type’ => ‘course’, ‘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;
}
}
function teacher_attributes_meta_box( $post ) {
$post_type_object = get_post_type_object( $post->post_type );
$pages = wp_dropdown_pages( array( ‘post_type’ => ‘teacher’, ‘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;
}
}
It doesn’t work because WordPress only save one of them (course or teacher). I could tell the problem is in the “name” of the select. I’ve tried to change the name (parent_id) but it didn’t work either. Maybe it wasn’t the right way to fix it :P.
Can you help me please? Thanks in advance!
You are a lifesaver. Thank You so much.
i’m really struggling to get this to work. I set my CPT to have a page as the parent. This creates 404 issues right away:
example.com/cpt-name/page/cpt
^that gives a 404, but it’s what wordpress tries to set, I do all the saving in permalink section, too. i then try the post_type_link filter and it creates 404s still.
i just want it to look like example.com/page/cpt and work, been spending 12 hours on this
NICE!!!!!!
The next post in this series should be “How to replace slug of lessons with its sequence number” like “/c/course-name/l/1” 🙂
Thank you so much for sharing this. Is there a way to accomplish this with the CPT UI plugin? I’d rather not force the creation of post types and custom fields into a *theme*. Welcome any pointers!
Also, it’ll help to see screenshots of how this looks.
thanks. I don’t think I would have figured this out myself, at least not too quickly. for me it was States/Cities, so very similar.
Thanks you very much, this is precisely what I needed. Very helpful.
Awesome post!! I wasn’t able to get it lol
I can’t have pathologists with LAB post type as a taxonomy/category, is not showing up :/
//LAB MANAGER
function adding_labm(){
register_post_type(‘lab’,
array(
‘labels’ => array(‘name’ => __(‘LAB Manager’), ‘singular_name’ => __(‘LAB Manager’)),
‘public’ => true,
‘has_archive’ => true,
‘hierarchical’ => true,
‘rewrite’ => array(‘slug’ => ‘lab’, ‘category’ => ‘lab_category’),
‘taxonomies’ => array(‘custom_taxonmyLab’), //category, post_tag #ADD CATEGORY, POST TAG OR SOMETHING ELSE TO THIS MENU
‘supports’ => array(‘title’, ‘thumbnail’), //excerpt, custom-fields, comments, revisions, page-attributes
‘menu_icon’ => ‘data:image/svg+xml;base64,’.base64_encode(”)
)
);
$labels = array(
‘name’ => _x( ‘Pathologists’, ‘Post Type General Name’, ‘1fix’ ),
‘singular_name’ => _x( ‘Pathologist’, ‘Post Type Singular Name’, ‘1fix’ ),
‘menu_name’ => __( ‘Pathologists’, ‘1fix’ ),
‘name_admin_bar’ => __( ‘Pathologists’, ‘1fix’ )
);
$args = array(
‘label’ => __( ‘Pathologist’, ‘1fix’ ),
‘labels’ => $labels,
‘hierarchical’ => false,
‘public’ => true,
‘show_in_menu’ => ‘edit.php?post_type=lab’
);
register_post_type( ‘pathologists’, $args );
}
add_action(‘init’, ‘adding_labm’);
//NEW CODE
function my_add_meta_boxes() {
add_meta_box( ‘lesson-parent’, ‘Course’, ‘lesson_attributes_meta_box’, ‘pathologists’, ‘side’, ‘high’ );
}
add_action( ‘add_meta_boxes’, ‘my_add_meta_boxes’ );
function lesson_attributes_meta_box( $post ) {
$post_type_object = get_post_type_object( $post->post_type );
$pages = wp_dropdown_pages( array( ‘post_type’ => ‘lab’, ‘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 NEW CODE
function custom_taxonmyLab(){
$labels = array(
‘name’ => _x( ‘States’, ‘taxonomy general name’ ),
‘singular_name’ => _x( ‘State’, ‘taxonomy singular name’ ),
‘search_items’ => __( ‘Search State’ ),
‘all_items’ => __( ‘All States’ ),
‘parent_item’ => __( ‘Parent State’ ),
‘parent_item_colon’ => __( ‘Parent State:’ ),
‘edit_item’ => __( ‘Edit State’ ),
‘update_item’ => __( ‘Update State’ ),
‘add_new_item’ => __( ‘Add New State’ ),
‘new_item_name’ => __( ‘New State Name’ ),
‘menu_name’ => __( ‘State’ ),
);
$args = array(
‘labels’ => $labels,
‘public’ => true,
‘show_admin_column’ => true,
‘show_in_nav_menus’ => true,
‘hierarchical’ => true,
‘has_archive’ => true,
‘rewrite’ => array(‘slug’ => ‘lab_cat’, ‘with_front’ => false),
);
register_taxonomy(‘category_lab’, ‘lab’, $args);
flush_rewrite_rules();
}
add_action( ‘init’, ‘custom_taxonmyLab’);
//END LAB MANAGER
Great post thnk you.
I registered two custom post types and changed lesson to brand and class to boat.
All my custom post types are working fine but all my normal pages are now 404 ?
🙂
This works fine but I am stuggling with the permalinks
Parent is
example.com/course/course-name
here am trying to get
example.com/course-name
Lesson is
example.com/lesson/course-name/lesson-name/
and here I am trying to get
example.com/course-name/lesson-name/
I am so tired playing with this can anyone tell me how to achieve this?
Surely it is possible but every change I make breaks something.
Thank you in advance if anyone can please help.
Hi,
I am trying to do the same, any help???
Still useful 4 years later. Worked on first try. Sensational enough to scream : THAAAAAAANKS !!!!!!!
My name is Nannie. And I am a professional Content writer with many years of experience in writing.
My primary goal is to solve problems related to writing. And I have been doing it for many years. I have been with several organizations as a volunteer and have assisted people in many ways.
My love for writing has no end. It is like the air we breathe, something I cherish with all my being. I am a passionate writer who started at an early age.
I’m happy that I`ve already sold several copies of my works in different countries like USA, Russia and others too numerous to mention.
I also work in a company that provides assistance to many students from different parts of the world. People always come to me because I work no matter how complex their projects are. I help them to save energy, because I feel fulfilled when people come to me for writing help.
Ghost Writer – Nannie – elearninag.com Corp
Thanks. Appreciate this is quite an old post, but is it possible to see an archive of the child CPT, those that are related to the parent CPT?
Yes, since each parent post is a standard post, you can just update the `singular` template for that CPT (in this case it’s “course”) and display the child posts it has