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:

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

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

Share the post:Tweet about this on TwitterShare on Google+4Share on Facebook1Share on LinkedIn1
I'm a senior web developer helping clients build their websites to grow businesses. Currently I'm based in Taipei, Taiwan. I write things about WordPress, AngularJS and life. Whenever you'd like to find someone to talk about these topics, just get in touch!

14 Comments

Submit a comment
  • 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…

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

  • 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);
    }
    }

Submit a comment

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