Add category base to URL in custom post type/taxonomy

I am building an LMS type system in WordPress, controlled by Custom Post types.
The post type is called Lessons (with a slug of courses) and it has one custom taxonomy (category) called courses.

The domain URL structure shows right now as:

Read More

domain.example/courses/lesson-name.

I want it to become:

domain.example/courses/[course-name{category}]/lesson-name

or essentially:

/[cpt]/%category%/%postname%/

here is the plugin I wrote that is controlling the CPTs now.

function rflms_post_type() {
    $labels = array(
        'name'                => _x( 'Lessons', 'Post Type General Name', 'text_domain' ),
        'singular_name'       => _x( 'Lesson', 'Post Type Singular Name', 'text_domain' ),
        'menu_name'           => __( 'Lessons', 'text_domain' ),
        'parent_item_colon'   => __( 'Parent Product:', 'text_domain' ),
        'all_items'           => __( 'All Lessons', 'text_domain' ),
        'view_item'           => __( 'View Lesson', 'text_domain' ),
        'add_new_item'        => __( 'Add New Lesson', 'text_domain' ),
        'add_new'             => __( 'New Lesson', 'text_domain' ),
        'edit_item'           => __( 'Edit Lesson', 'text_domain' ),
        'update_item'         => __( 'Update Lesson', 'text_domain' ),
        'search_items'        => __( 'Search Lessions', 'text_domain' ),
        'not_found'           => __( 'No Lessons Found', 'text_domain' ),
        'not_found_in_trash'  => __( 'No Lessons Found in Trash', 'text_domain' ),
    );

    $args = array(
        'label'               => __( 'Lessons', 'text_domain' ),
        'description'         => __( 'Referable Lessons', 'text_domain' ),
        'labels'              => $labels,
        'hierarchical'        => false,
        'public'              => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'show_in_nav_menus'   => true,
        'show_in_admin_bar'   => true,
        'supports'        => array('premise-member-access', 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments'),
        'menu_position'       => 5,
        'menu_icon'           => null,
        'can_export'          => true,
        'has_archive'         => true,
        'exclude_from_search' => false,
        'publicly_queryable'  => true,
        'capability_type'     => 'post',
        'rewrite'                    => array('slug' => 'courses'),
    );

    register_post_type( 'lessons', $args );

// Hook into the 'init' action

}
add_action( 'init', 'rflms_post_type', 0 );

// Register Custom Taxonomy
function custom_taxonomy()  {
    $labels = array(
        'name'                       => _x( 'Courses', 'Taxonomy General Name', 'text_domain' ),
        'singular_name'              => _x( 'Course', 'Taxonomy Singular Name', 'text_domain' ),
        'menu_name'                  => __( 'Courses', 'text_domain' ),
        'all_items'                  => __( 'All Courses', 'text_domain' ),
        'parent_item'                => __( 'Parent Course', 'text_domain' ),
        'parent_item_colon'          => __( 'Parent Course:', 'text_domain' ),
        'new_item_name'              => __( 'New Course Name', 'text_domain' ),
        'add_new_item'               => __( 'Add New Course', 'text_domain' ),
        'edit_item'                  => __( 'Edit Course', 'text_domain' ),
        'update_item'                => __( 'Update Course', 'text_domain' ),
        'separate_items_with_commas' => __( 'Separate Courses with commas', 'text_domain' ),
        'search_items'               => __( 'Search Courses', 'text_domain' ),
        'add_or_remove_items'        => __( 'Add or Remove Courses', 'text_domain' ),
        'choose_from_most_used'      => __( 'Choose from Most Used courses', 'text_domain' ),
    );

    $args = array(
        'labels'                     => $labels,
        'hierarchical'               => true,
        'public'                     => true,
        'show_ui'                    => true,
        'show_admin_column'          => true,
        'show_in_nav_menus'          => true,
        'show_tagcloud'              => false,
        'rewrite'                    => array('slug' => 'courses'),
    );

    register_taxonomy( 'course', 'lessons', $args );
}

// Hook into the 'init' action
add_action( 'init', 'custom_taxonomy', 0 );

Related posts

Leave a Reply

9 comments

  1. Change your rewrite to add the course query var:

    'rewrite' => array('slug' => 'courses/%course%')
    

    Then filter post_type_link to insert the selected course into the permalink:

    function wpa_course_post_link( $post_link, $id = 0 ){
        $post = get_post($id);  
        if ( is_object( $post ) ){
            $terms = wp_get_object_terms( $post->ID, 'course' );
            if( $terms ){
                return str_replace( '%course%' , $terms[0]->slug , $post_link );
            }
        }
        return $post_link;  
    }
    add_filter( 'post_type_link', 'wpa_course_post_link', 1, 3 );
    

    There are also plugins like Custom Post Type Permalinks that can do this for you.

  2. The solution for me had three parts. In my case the post type is called trainings.

    1. Add 'rewrite' => array('slug' => 'trainings/%cat%') to the register_post_type function.
    2. Change the slug to have a dynamic category.
    3. “Listen” to the new dynamic URL and load the appropriate template.

    So here is how to change the permalink dynamically for a given post type. Add to functions.php:

    function vx_soon_training_post_link( $post_link, $id = 0 ) {
        $post = get_post( $id );
        if ( is_object( $post ) ) {
            $terms = wp_get_object_terms( $post->ID, 'training_cat' );
            if ( $terms ) {
                return str_replace( '%cat%', $terms[0]->slug, $post_link );
            }
        }
    
        return $post_link;
    }
    
    add_filter( 'post_type_link', 'vx_soon_training_post_link', 1, 3 );
    

    …and this is how to load the appropriate template on the new dynamic URL. Add to functions.php:

    function archive_rewrite_rules() {
        add_rewrite_rule(
            '^training/(.*)/(.*)/?$',
            'index.php?post_type=trainings&name=$matches[2]',
            'top'
        );
        //flush_rewrite_rules(); // use only once
    }
    
    add_action( 'init', 'archive_rewrite_rules' );
    

    Thats it! Remember to refresh the permalinks by saving the permalinks again in de backend. Or use the flush_rewrite_rules() function.

  3. You need to update the line where you have registered a custom post type using the register_post_type function.

    'rewrite' => array('slug' => 'courses/%cat%')

    To dynamically change the permalink of the post type, you have to add this code in functions.php:

    function change_link( $post_link, $id = 0 ) {
        $post = get_post( $id );
        if( $post->post_type == 'courses' )
        {
           if ( is_object( $post ) ) {
              $terms = wp_get_object_terms( $post->ID, array('course') );
              if ( $terms ) {
                 return str_replace( '%cat%', $terms[0]->slug, $post_link );
             }
          }
        }
        return   $post_link ;
    }
    add_filter( 'post_type_link', 'change_link', 1, 3 );
    
    //load the template on the new generated URL otherwise you will get 404's the page
    
    function generated_rewrite_rules() {
       add_rewrite_rule(
           '^courses/(.*)/(.*)/?$',
           'index.php?post_type=courses&name=$matches[2]',
           'top'
       );
    }
    add_action( 'init', 'generated_rewrite_rules' );
    

    After that, you need to flush the rewrite permalinks. Go to wp-admin > Settings > permalinks and update permalink settings then press the “Save Changes” button. It’ll return URLs like this: domain.example/courses/[course-name{category}]/lesson-name

  4. Got the solution!

    To have hierarchical permalinks for custom post type install Custom Post Type Permalinks(https://wordpress.org/plugins/custom-post-type-permalinks/) plugin.

    Update registered post type. I have post type name as help center

    function help_centre_post_type(){
        register_post_type('helpcentre', array( 
            'labels'            =>  array(
                'name'          =>      __('Help Center'),
                'singular_name' =>      __('Help Center'),
                'all_items'     =>      __('View Posts'),
                'add_new'       =>      __('New Post'),
                'add_new_item'  =>      __('New Help Center'),
                'edit_item'     =>      __('Edit Help Center'),
                'view_item'     =>      __('View Help Center'),
                'search_items'  =>      __('Search Help Center'),
                'no_found'      =>      __('No Help Center Post Found'),
                'not_found_in_trash' => __('No Help Center Post in Trash')
                                    ),
            'public'            =>  true,
            'publicly_queryable'=>  true,
            'show_ui'           =>  true, 
            'query_var'         =>  true,
            'show_in_nav_menus' =>  false,
            'capability_type'   =>  'page',
            'hierarchical'      =>  true,
            'rewrite'=> [
                'slug' => 'help-center',
                "with_front" => false
            ],
            "cptp_permalink_structure" => "/%help_centre_category%/%post_id%-%postname%/",
            'menu_position'     =>  21,
            'supports'          =>  array('title','editor', 'thumbnail'),
            'has_archive'       =>  true
        ));
        flush_rewrite_rules();
    }
    add_action('init', 'help_centre_post_type');
    

    And here is registered taxonomy

    function themes_taxonomy() {  
        register_taxonomy(  
            'help_centre_category',  
            'helpcentre',        
            array(
                'label' => __( 'Categories' ),
                'rewrite'=> [
                    'slug' => 'help-center',
                    "with_front" => false
                ],
                "cptp_permalink_structure" => "/%help_centre_category%/",
                'hierarchical'               => true,
                'public'                     => true,
                'show_ui'                    => true,
                'show_admin_column'          => true,
                'show_in_nav_menus'          => true,
                'query_var' => true
            ) 
        );  
    }  
    add_action( 'init', 'themes_taxonomy');
    

    This is line makes your permalink work

    "cptp_permalink_structure" => "/%help_centre_category%/%post_id%-%postname%/",
    

    you can remove %post_id% and can keep /%help_centre_category%/%postname%/"

    Don’t forget to flush permalinks from dashboard.

  5. To anyone interested in the solution, without having to tinker with raw PHP code, I highly recommend the plugin Permalink Manager Lite by Maciej Bis. It’s a life saver.

    It has a visual mechanism to remove or add whatever part you want in the custom post type’s URL based on ‘permastructs’:

    Screenshot of Permalink Manager Lite

    (With all the pain involved in simple URL structuring with custom post types, we were about to give up on WP and move to another CMS. But this plugin in conjunction with ACF and CPTUI or Pods makes WordPress fairly professional.)

  6. This is worked for me :

    'rewrite' => array(
            'slug' => 'portfolio',
            'with_front' => false,
            'hierarchical' => true // to display category/subcategroy
        ),
    
  7. I found @chetan-vaghela ‘s answer almost perfect; in my use case I also wanted to be able to see a list of all posts by this post type like a typical archive page (i.e. /courses/, without any taxonomy after it). I just had to add one additional rewrite rule as follows:

    function generated_rewrite_rules() {
        add_rewrite_rule(
            '^courses/(.*)/(.*)/?$',
            'index.php?post_type=courses&name=$matches[2]',
            'top'
        );
    }