44

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:

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 );
3

9 Answers 9

62

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.

10
  • Thank you, I appreciate your swift answer. This makes complete sense. I'm curious though, where do I insert the filter post_type_link at? can i just go at the bottom of the entire document? Commented Apr 6, 2013 at 16:26
  • I added it to the bottom and it 404's the page. Commented Apr 6, 2013 at 16:31
  • 2
    you have to flush rewrites, visit the permalinks settings page.
    – Milo
    Commented Apr 6, 2013 at 17:09
  • 2
    Downvote - archive page 404 Commented Apr 4, 2020 at 1:06
  • 1
    This will show the post no matter what the course slug is. It shows them on ALL course slugs. How to limit this to only show on the course slug that is selected and 404 on course that isn't selected for that person? Commented Jun 15, 2021 at 22:27
11

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

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.

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

2
  • Down vote - 404 on both single CPT and CTP archive page types Commented Apr 4, 2020 at 1:07
  • 1
    @dev did you follow all steps which i have mentioned in my answer ? did you flush your rewrite permalinks? I have tested and it is working fine. Commented Apr 4, 2020 at 3:57
1

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

0

Yep! After a lot of research I got plugin 'Custom Permalinks'. Which fulfils my requirement regards - custom URL e.g.

  • for Category
  • for Post
  • for Custom Post
  • for Custom Taxonomy etc.

Like this Custom Post Type - Post:

enter image description here

0

This is worked for me :

'rewrite' => array(
        'slug' => 'portfolio',
        'with_front' => false,
        'hierarchical' => true // to display category/subcategroy
    ),
1
  • 6
    This does not make use of the categories or their path it only makes the custom post type hierarchical. Commented Aug 25, 2017 at 11:53
0

If you're using get_post_type_archive_link(), maybe you will need to remove /%cat%/ from the URL using post_type_archive_link filter.

0

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

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.