I'm a bit of a wordpress noob and I'm diving head first into a course creation plugin. The structure will go program > course > level > lesson. Right now I have lessons set up as a custom post type and levels, courses and programs are taxonomies. Basically I could use help creating a navigation path where you first pick a program, then a course, then a level, then a lesson. I'm racking my brains and I'm kinda stuck. Like I said I'm pretty new to Wordpress and php for that matter...

Is this a viable way to go about this by using a custom post types and taxonomies and does anyone have any suggestions or resources? I'm not sure if I should go about creating a custom theme template file for this either, or just use a shortcode in a page for the navigation.

                   / \
                  /   \
             Course1 Course2
                /\     /\
               /  \   /  \
             Lv1 Lv2 Lv1 Lv2
            /\   /\   /\   /\
          Lessons Lessons Lessons

Below is what I have so far (Sorry, it's quite long). It works well, I just haven't quite figured out how to organize it yet. Below I've just set up the custom post type and taxonomies. That's about it

 * Plugin Name: Course Manager
 * Description: Creates Programs, Courses, Levels and Lessons
 * Version: The Plugin's Version Number, e.g.: 0.1
function cm_lesson_cp(){
  $labels = array(
    'name'               => _x( 'Lesson', 'post type general name' ),
    'singular_name'      => _x( 'Lesson', 'post type singular name' ),
    'add_new'            => _x( 'Add New', 'Lesson' ),
    'add_new_item'       => __( 'Add New Lesson' ),
    'edit_item'          => __( 'Edit Lesson' ),
    'new_item'           => __( 'New Lesson' ),
    'all_items'          => __( 'All Lessons' ),
    'view_item'          => __( 'View Lesson' ),
    'search_items'       => __( 'Search Lessons' ),
    'not_found'          => __( 'No Lessons found' ),
    'not_found_in_trash' => __( 'No lessons found in the Trash' ), 
    'parent_item_colon'  => '',
    'menu_name'          => 'Lessons'
  $args = array(
    'labels'        => $labels,
    'description'   => 'Enter a lesson description here.',
    'public'        => true,
    'menu_position' => 4,
    'supports'      => array( 'title', 'editor', 'excerpt'),
    'has_archive'   => true,
  register_post_type( 'lesson', $args ); 
  flush_rewrite_rules( false );
add_action( 'init', 'cm_lesson_cp' );

//Custom messages for custom post type`

function lesson_messages_cp( $messages ) {
  global $post, $post_ID;
  $messages['lesson'] = array(
    0 => '', 
    1 => sprintf( __('Lesson updated. <a href="%s">View Lesson</a>'), esc_url( get_permalink($post_ID) ) ),
    2 => __('Custom field updated.'),
    3 => __('Custom field deleted.'),
    4 => __('Lesson updated.'),
    5 => isset($_GET['revision']) ? sprintf( __('Lesson restored to revision from %s'), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
    6 => sprintf( __('Lesson published. <a href="%s">View Lesson</a>'), esc_url( get_permalink($post_ID) ) ),
    7 => __('Lesson saved.'),
    8 => sprintf( __('Lesson submitted. <a target="_blank" href="%s">Preview Lesson</a>'), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
    9 => sprintf( __('Lesson scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview Lesson</a>'), date_i18n( __( 'M j, Y @ G:i' ), strtotime( $post->post_date ) ), esc_url( get_permalink($post_ID) ) ),
    10 => sprintf( __('Lesson draft updated. <a target="_blank" href="%s">Preview Lesson</a>'), esc_url( add_query_arg( 'preview', 'true', get_permalink($post_ID) ) ) ),
  return $messages;
add_filter( 'post_updated_messages', 'lesson_messages_cp' );

// Register Program Taxonomy

function program_taxonomy_cp() {
  $args = array();
  register_taxonomy( 'program', 'lesson', $args );

add_action( 'init', 'program_taxonomy_cp', 0 );

// Customize taxonomy

function program_taxonomy_setting_cp() {
  $labels = array(
    'name'              => _x( 'Program', 'taxonomy general name' ),
    'singular_name'     => _x( 'Program', 'taxonomy name' ),
    'search_items'      => __( 'Search Programs' ),
    'all_items'         => __( 'All Programs' ),
    'parent_item'       => __( 'Parent Program Category' ),
    'parent_item_colon' => __( 'Parent Program Category:' ),
    'edit_item'         => __( 'Edit Program Category' ), 
    'update_item'       => __( 'Update Program' ),
    'add_new_item'      => __( 'Add New Program' ),
    'new_item_name'     => __( 'New Program' ),
    'menu_name'         => __( 'Programs' ),
  $args = array(
    'labels' => $labels,
    'hierarchical' => true,
  register_taxonomy( 'program', 'lesson', $args );
add_action( 'init', 'program_taxonomy_setting_cp', 0 );

// Register Course Taxonomy

function course_taxonomy_cp() {
  $args = array();
  register_taxonomy( 'course', 'lesson', $args );

add_action( 'init', 'course_taxonomy_cp', 0 );

// Customize taxonomy

function course_taxonomy_setting_cp() {
  $labels = array(
    'name'              => _x( 'Course', 'taxonomy general name' ),
    'singular_name'     => _x( 'Course', 'taxonomy name' ),
    'search_items'      => __( 'Search Courses' ),
    'all_items'         => __( 'All Courses' ),
    'parent_item'       => __( 'Parent Course Category' ),
    'parent_item_colon' => __( 'Parent Course Category:' ),
    'edit_item'         => __( 'Edit Course Category' ), 
    'update_item'       => __( 'Update Course' ),
    'add_new_item'      => __( 'Add New Course' ),
    'new_item_name'     => __( 'New Course' ),
    'menu_name'         => __( 'Courses' ),
  $args = array(
    'labels' => $labels,
    'hierarchical' => true,
  register_taxonomy( 'course', 'lesson', $args );
add_action( 'init', 'course_taxonomy_setting_cp', 0 );

// Register Custom Taxonomy "Level"

function level_taxonomy_cp() {
  $args = array();
  register_taxonomy( 'glossary', 'term', $args );

add_action( 'init', 'level_taxonomy_cp', 0 );

// Customize taxonomy

function level_taxonomy_setting_cp() {
  $labels = array(
    'name'              => _x( 'Levels', 'taxonomy general name' ),
    'singular_name'     => _x( 'Level', 'taxonomy name' ),
    'search_items'      => __( 'Search Levels' ),
    'all_items'         => __( 'All Levels' ),
    'parent_item'       => __( 'Parent Level Category' ),
    'parent_item_colon' => __( 'Parent Level Category:' ),
    'edit_item'         => __( 'Edit Level Category' ), 
    'update_item'       => __( 'Update Level' ),
    'add_new_item'      => __( 'Add New Level' ),
    'new_item_name'     => __( 'New Level' ),
    'menu_name'         => __( 'Levels' ),
  $args = array(
    'labels' => $labels,
    'hierarchical' => true,
  register_taxonomy( 'level', 'lesson', $args );
add_action( 'init', 'level_taxonomy_setting_cp', 0 );

2 Answers 2


Your data model is interesting. You can answer this question better than me, but I don't know if we really can say, as @toscho does, that a lesson will never be shared between courses, or that no two levels will have the same lesson in.

I believe that a lesson and a course are both post-like things, but a level is something that makes less sense on its own and needs context (ie its lessons) to be meaningful.

If I was building this, I would relegate 'level' to a taxonomy of lessons, and I would use using Posts 2 Posts to relate lessons to courses. Then to get a lesson I would query it directly. For a level in a course I would query lessons in that level's term that were also related to the course.

For the following reasons:

  • Templating You get the benefits of single-lesson.php etc in your template hierarchy. If you do it @toscho's way, if you wanted different templates for lessons, courses etc you would need to do something slightly complex, like introduce a conditional before the template was rendered, and handle it yourself.

  • Readability A query for post_type=lesson&level=level_one is more readable than post_parent=123243 (or whatever).

  • Flexibility Want a lesson shared between two courses? Want to reuse lessons across levels? You can do it. With straight post hierarchy, you'd need to duplicate material in the database, which is a recipe for trouble.

  • Simplicity of Levels I admit that this is the point where the system is less flexible, but I think that is ok - it might not be, for your use-case! If you want to have the same name for levels (eg Level 1, Level 2, Level 3) across all your courses, why not have them in the same place? If you need to have per-course, per-level descriptions, add a postmeta field to your course with the description in it. Less flexible, but perhaps enough.

  • That makes a lot of sense using a taxonomy for the level. Levels don't need descriptions or pictures or anything. The others do. It is possible that a lesson may be shared between some courses. Are you suggesting that I use different custom post types for programs courses and lessons? The templates do need to be different for each type. The levels will be consistent from level 1 to 6 so I think taxonomies should work. Thanks for the help.
    – user57391
    Commented Aug 7, 2014 at 20:55
  • Ahhh! I missed the link to post to post. I think that would work extremely well! All makes perfect sense now.
    – user57391
    Commented Aug 7, 2014 at 21:06

You are using taxonomies to model a hierarchy. I think this is not a good solution. Terms in a taxonomy are not mutually exclusive, they are meant to be used in n:m relationships. As a result, every level in your setup can be in multiple courses, and any lesson in multiple levels. This is not what you want, right?

register_post_type() takes an argument hierarchical. Set it to true, and use just one post type to model the hierarchy. Courses are now all posts whose post_parent is 0, levels those whose post_parent is a course and so on. You can even add subpages for lessons later if you see a need for that.

You can get all lower levels on each point with get_children().

This makes it also easier to add a long, formatted description and attachments to each course or level. The built-in search will work by default and access control will be easier too.

  • +1. This is probably the best approach for how WP works out of the box. The problem when the CPT is unique is to know which level an object belong. E.G. when querying the cpt you get all levels (program, courses, level, lessons) if post_parent is 0 then the level is the first, but in other cases is hard to know if the post object is a course a lesson or else. That reflects in template hierarchy: all posts are served by the unique template single-{$cpt}.php and to serve templates conditionally becomes hard.
    – gmazzap
    Commented Aug 3, 2014 at 11:40
  • Thanks so much for the answers! That makes so much sense now. I'm going to fix the code right now and start playing around. I'll definitely have some questions on serving the templates conditionally... It must be doable though right? One thing at a time though :)
    – user57391
    Commented Aug 4, 2014 at 4:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.