Parent Post From Another Post Type And A New URL Structure In WordPress

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:[course-name]/[lesson-name]/

Taking my AngularJS series for example, it would look like:

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

  1. Registering two custom post types: Courses and Lessons.
  2. Updating the “Parent” meta box so we can choose a “course” as the parent post for a “lesson”.
  3. Setting the exactly URL structure we want.
  4. 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:

  1. 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.
  2. 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.

completely delete

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], I’ll get back to you ASAP even when I’m on a 9-day vacation.

50 responses

  1. […] Parent Post From Another Post Type And A New URL Structure In WordPress […]

  2. Andrew Avatar

    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[game-name]/[character-name]
    I don’t want the name of the moveset at all, since it’s just a hand-merged name

    1. Yoren Chang Avatar
      Yoren Chang

      I believe so. Just you need some time to test around the rewrite rules. Good luck!

  3. Andrew Avatar

    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.

  4. Jonathan Avatar

    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?

    1. Yoren Chang Avatar
      Yoren Chang

      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…

  5. Great article man. It really helped me connect two custom post types I had on my project.
    Thank you very much

    1. Yoren Chang Avatar
      Yoren Chang

      Hey Eric, Glad I can help!

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

    1. Yoren Chang Avatar
      Yoren Chang

      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.

  7. That is very interesting; you are a very skilled blogger. I have shared your website in my social networks!

  8. Kyle Thomas Avatar
    Kyle Thomas


    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;

    1. Kyle Thomas Avatar
      Kyle Thomas

      Managed to fix it up, was changing the permalink and wordpress got confused.

      1. Yoren Chang Avatar
        Yoren Chang

        hey Kyle, good to know you got it figured out!

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

    // Chapters
    add_rewrite_tag(‘%chapter%’, ‘([^/]+)’, ‘chapter=’);
    add_permastruct(‘chapter’, ‘/%book%/%chapter%’, false);

    /* 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 !

    1. Oops. Some code has been pasted twice. Sorry, I can’t edit.

      Of course, I only used once the part : /* Set permalink for Chapter */

  10. Thanks, this is that I want.
    But Still not understand with code. im newbie blogger, only know write

  11. 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. +1 ! It is the same for me.


      1. Yoren Chang Avatar
        Yoren Chang

        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!

  12. Selçuk Oktay Avatar
    Selçuk Oktay

    Hello, great job, thank you.
    How can we add “featured image” to this post types?

  13. Amazing, thanks so much!

  14. 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!

  15. my question then becomes, how do you loop through the courses CPT with PHP? How does it know that lessons are attached to courses?

    1. Yoren Chang Avatar
      Yoren Chang

      By checking the post_parent we can see the connections then.

  16. 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 ✌🏼

  17. Anisha Avatar

    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?

    1. Yoren Chang Avatar
      Yoren Chang

      Hey Anisha, I don’t think you have to “solve” that. Can’t relate much in such scenario.

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

  18. Daniel Avatar


    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!

  19. You are a lifesaver. Thank You so much.

  20. 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:

    ^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 and work, been spending 12 hours on this

  21. Yuriy Avatar


    The next post in this series should be “How to replace slug of lessons with its sequence number” like “/c/course-name/l/1” 🙂

  22. Phoenix Kiula Avatar
    Phoenix Kiula

    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!

    1. Phoenix Kiula Avatar
      Phoenix Kiula

      Also, it’ll help to see screenshots of how this looks.

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

  24. Thanks you very much, this is precisely what I needed. Very helpful.

  25. 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 :/

    function adding_labm(){
    ‘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;

    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);
    add_action( ‘init’, ‘custom_taxonmyLab’);

  26. 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 ?

  27. This works fine but I am stuggling with the permalinks

    Parent is

    here am trying to get

    Lesson is

    and here I am trying to get

    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.

    1. Hi,

      I am trying to do the same, any help???

  28. […] have this code that I got from this tutorial, it’s basically a way to make a lesson (cpt) of a course (cpt) linked within the URL with a […]

  29. Still useful 4 years later. Worked on first try. Sensational enough to scream : THAAAAAAANKS !!!!!!!

  30. 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 – Corp

  31. Sean Turtle Avatar
    Sean Turtle

    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?

    1. Yoren Chang Avatar
      Yoren Chang

      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

  32. Hi,

    Excellent tutorial. Works, almost. I may have missed something so am rechecking my code but at the moment this is what works.

    I have a course, call it test-course and lessons, call them test-lesson-01, test-lesson-02, etc. I can navigate to:

    it works. But at present the lesson loads even if I put in

    Like I said I am checking my code to ensure I have followed the tutorial, but would like to know (a) if others can confirm this issue and (b) if so how to fix.


    1. Yoren Chang Avatar
      Yoren Chang

      Hi Nick,

      It’s been a while for this post, but from the code I think that’s the expected result (so it’s a non-issue), as the code actually treated the `%course%` part as something like a wild card, it doesn’t have to really match anything. To force that %course% part to also match something was beyond the scope of the post, so I went lazy and didn’t cover that

      1. Thanks for the fast reply. Was afraid you would say that! This is one of the bits of WordPress I find a pain, customising permalinks. Anyway, thanks for the reply and all the excellent tutorials, much appreciated.

Leave a Reply

Your email address will not be published. Required fields are marked *