I'm getting quite frustrated setting up the correct URL structure for a Cutom Post Type with a Custom taxonomy.

I have a Custom Post Type called courses with a Custom Taxonomy called course-type.

  1. The URL structure should be: site.com/courses/course-type/course-single-post/.
  2. For example: site.com/courses/science/rocket-to-the-moon/

I've managed to achieve that however not all URL parts are behaving as it should.

  • site.com/courses/ - returns a 404
  • site.com/courses/science/ - Shows an archive page with all posts in side that custom taxonomy which is correct
  • site.com/courses/science/rocket-to-the-moon/ - Shows the single Custom Post Type which is correct too

I don't know why the site.com/courses/ returns a 404 instead of showing an archive page listing all Custom Post Type's...?

This is the code that I've used:


/*Courses Custom Post Type*/
function my_custom_post_courses() {
$labels = array(
    'name'               => _x( 'Courses', 'post type general name' ),
    'singular_name'      => _x( 'Course', 'post type singular name' ),
    'add_new'            => _x( 'New course', 'reis' ),
    'add_new_item'       => __( 'Add new course' ),
    'edit_item'          => __( 'Edit course' ),
    'new_item'           => __( 'New item' ),
    'all_items'          => __( 'All courses' ),
    'view_item'          => __( 'View courses' ),
    'search_items'       => __( 'Search courses' ),
    'not_found'          => __( 'Nothing found  ' ),
    'not_found_in_trash' => __( 'Nothing found in the trash' ),
    'parent_item_colon'  => '',
    'menu_name'          => 'Courses'
$args = array(
    'labels'        => $labels,
    'description'   => 'Enter a new course',
    'public'        => true,
    'menu_position' => 5,
    'supports'      => array( 'title', 'editor', 'thumbnail', 'excerpt', 'comments' ),
    'has_archive'   => true,
    'hierarchical'  => true,
    'rewrite'       => array('slug' => 'courses/%course-type%','with_front' => false),
    'query_var'     => true,
    //'rewrite'     => true,
    //'publicly_queryable' => false,
register_post_type( 'courses', $args );
add_action( 'init', 'my_custom_post_courses' );

/* Courses custom taxonomy */
function my_taxonomies_course_type() {
$labels = array(
    'name'              => _x( 'Course type', 'taxonomy general name' ),
    'singular_name'     => _x( 'Course type', 'taxonomy singular name' ),
    'search_items'      => __( 'Search course types' ),
    'all_items'         => __( 'All course types' ),
    'parent_item'       => __( 'Parent course type' ),
    'parent_item_colon' => __( 'Parent course type:' ),
    'edit_item'         => __( 'Edit course type' ),
    'update_item'       => __( 'Update course type ' ),
    'add_new_item'      => __( 'Add new course type' ),
    'new_item_name'     => __( 'New course type' ),
    'menu_name'         => __( 'Course type' ),
$args = array(
    'labels' => $labels,
    'hierarchical'  => true,
    'public'        => true,
    'query_var'     => 'course-type',
    'rewrite'       =>  array('slug' => 'courses' ),
    '_builtin'      => false,
register_taxonomy( 'course-type', 'courses', $args );
add_action( 'init', 'my_taxonomies_course_type', 0 );

/* Permalink filter Courses */
add_filter('post_link', 'course_permalink', 1, 3);
add_filter('post_type_link', 'course_permalink', 1, 3);

function course_permalink($permalink, $post_id, $leavename) {
if (strpos($permalink, '%course-type%') === FALSE) return $permalink;
    // Get post
    $post = get_post($post_id);
    if (!$post) return $permalink;

    // Get taxonomy terms
    $terms = wp_get_object_terms($post->ID, 'course-type');
    if (!is_wp_error($terms) && !empty($terms) && is_object($terms[0]))
        $taxonomy_slug = $terms[0]->slug;
    else $taxonomy_slug = 'no-course-type';

return str_replace('%course-type%', $taxonomy_slug, $permalink);
  • Don't knowing what I'm doing wrong but your answer completely breaks the site unfortunately... Does it replace some of the code that I've already written?
    – NielsPilon
    Commented Apr 1, 2016 at 14:26
  • I had snippets from a class in my example. I just rewrote the code to use functions instead of class methods. Also please take your time to read through my answer carefully and adjust things appropriately, according to your needs. Make sure you also update your permalink settings simply just by visiting Dashboard -> Settings -> Permalinks. Commented Apr 1, 2016 at 14:37
  • Forgive me for the misunderstanding. The Q&A I linked you isn't 100% what you need, but the answer is still indeed found within those links with little modification. Check my answer below for a solution to your exact needs. Commented Apr 1, 2016 at 15:00
  • 1
    and regarding the archive link, try explicitly setting the slug for has_archive rather than setting it to true.
    – Milo
    Commented Apr 1, 2016 at 15:26

2 Answers 2


Setting has_archive to true causes WordPress to generate a rewrite rule for the archive using the rewrite slug, which is not what you want in your case. Instead, explicitly specify the archive slug as a string and the correct rules will be generated:

$args = array(
    'has_archive'   => 'courses',
    'rewrite'       => array('slug' => 'courses/%course-type%','with_front' => false),
    // the rest of your args...
register_post_type( 'courses', $args );
  • Wow super props to that one. I had no idea you could do that. That's much simpler. Commented Apr 1, 2016 at 15:57
  • 1
    Didn't knew that either and instantly fixed the permalink issue. No more 404's on any of the archive and single post pages. Thanks so much!
    – NielsPilon
    Commented Apr 4, 2016 at 6:50

With your desired permalink URL structure ... WordPress is automatically handling the creation of rewrite rules for what you entered (courses/%course-type%) for your Post Type slug. Of course you have to modify the Post Type link to substitute the place holder with the actual value (as you've already done).

However, WordPress isn't expecting that type of behavior in a Post Type slug. Typically WordPress would expect something simple like courses (instead of courses/%course-type%).

That's okay though, WordPress allows us to adapt to almost any situation with it's amazing flexibility and customization features.

All you need to do is adjust the rewrite rules to handle the "base" of your Post Type slug (courses).

This can be done cleanly in two functions which could be pasted into your currently activated theme functions.php file (or more ideally ... into a plugin file.)

Step 1 - Adjust the rewrite rules.

In this step, you're requesting new rewrite rules to add into the existing rewrite rules. The new rewrite rules are created automatically in the mbe_get_new_rewrite_rules(); function.

if ( ! function_exists( 'mbe_adjust_rewrite_rules' ) ) {

    function mbe_adjust_rewrite_rules( $rules ) {

        if ( ! function_exists( 'mbe_get_new_rewrite_rules' ) ) {
            return $rules;

        $new_rules = mbe_get_new_rewrite_rules( 'courses' );

        return ( $new_rules + $rules );


    add_action( 'rewrite_rules_array', 'mbe_adjust_rewrite_rules', 100 );


Step 2 - Create the new rewrite rules.

Please note: This function requires the name of the Post Type as a parameter.

This function will take the specified Post Type name and automatically retrieve it's rewrite slug (specified during Post Type registration).

This function will then continue to check for any slashes in the Post Type slug. If one is located, it will take everything before the first slash (AKA the base slug) and generate an appropriate rewrite rule for your Post Type's base slug. (Like it would have done if you just specified a simple rewrite slug of courses during Post Type registration instead of courses/%course-type%.)

if ( ! function_exists( 'mbe_get_new_rewrite_rules' ) ) {

    function mbe_get_new_rewrite_rules( $post_type = '' ) {

        $new_rules = array();

        $post_type_object = get_post_type_object( $post_type );

        if ( ! $post_type_object ) {
            return $new_rules;

        $post_type_slug      = $post_type_object->rewrite['slug'];
        $post_type_slug_base = substr( $post_type_slug, 0, strpos( $post_type_slug, '/' ) );

        if ( ! empty( $post_type_slug_base ) ) {
            $new_rules["{$post_type_slug_base}/?$"] = "index.php?post_type={$post_type}";

        return $new_rules;



There you have it. Three valid and functioning URLs. As per your request.

  1. domain.com/courses/
  2. domain.com/courses/course-type/
  3. domain.com/courses/course-type/course-name/

Just remember to either programmatically flush rewrite rules, or to visit: Dashboard -> Settings -> Permalinks for the new rewrite rules to take effect.

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.