I'm trying without success to have a URL structure like domain.com/term/postname, but I just get 404 for all non-CPT pages.

function add_video_post_type() {

    $labels = array(
        'name'                => _x( 'Videos', 'Post Type General Name', 'kibble' ),
        'singular_name'       => _x( 'Video', 'Post Type Singular Name', 'kibble' ),
        'menu_name'           => __( 'Videos', 'kibble' ),
        'parent_item_colon'   => __( 'Parent Item:', 'kibble' ),
        'all_items'           => __( 'All Items', 'kibble' ),
        'view_item'           => __( 'View Item', 'kibble' ),
        'add_new_item'        => __( 'Add New Item', 'kibble' ),
        'add_new'             => __( 'Add New', 'kibble' ),
        'edit_item'           => __( 'Edit Item', 'kibble' ),
        'update_item'         => __( 'Update Item', 'kibble' ),
        'search_items'        => __( 'Search Item', 'kibble' ),
        'not_found'           => __( 'Not found', 'kibble' ),
        'not_found_in_trash'  => __( 'Not found in Trash', 'kibble' ),
    $rewrite = array(
        'slug'                => '%video_genre%',
        'with_front'          => true,
        'pages'               => true,
        'feeds'               => true,
    $args = array(
        'label'               => __( 'video_type', 'kibble' ),
        'description'         => __( 'Video Posts', 'kibble' ),
        'labels'              => $labels,
        'supports'            => array( 'title', 'editor', 'thumbnail', ),
        'taxonomies'          => array( 'video_genre' ),
        'hierarchical'        => false,
        'public'              => true,
        'show_ui'             => true,
        'show_in_menu'        => true,
        'show_in_nav_menus'   => true,
        'show_in_admin_bar'   => true,
        'menu_position'       => 5,
        'menu_icon'           => 'dashicons-format-video',
        'can_export'          => true,
        'has_archive'         => true,
        'exclude_from_search' => false,
        'publicly_queryable'  => true,
        'rewrite'             => $rewrite,
        'capability_type'     => 'page',
    register_post_type( 'video_type', $args );


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

function custom_video_genre() {

    $labels = array(
        'name'                       => _x( 'Video Categories', 'Taxonomy General Name', 'kibble' ),
        'singular_name'              => _x( 'Video Category', 'Taxonomy Singular Name', 'kibble' ),
        'menu_name'                  => __( 'Categories', 'kibble' ),
        'all_items'                  => __( 'All Items', 'kibble' ),
        'parent_item'                => __( 'Parent Item', 'kibble' ),
        'parent_item_colon'          => __( 'Parent Item:', 'kibble' ),
        'new_item_name'              => __( 'New Item Name', 'kibble' ),
        'add_new_item'               => __( 'Add New Item', 'kibble' ),
        'edit_item'                  => __( 'Edit Item', 'kibble' ),
        'update_item'                => __( 'Update Item', 'kibble' ),
        'separate_items_with_commas' => __( 'Separate items with commas', 'kibble' ),
        'search_items'               => __( 'Search Items', 'kibble' ),
        'add_or_remove_items'        => __( 'Add or remove items', 'kibble' ),
        'choose_from_most_used'      => __( 'Choose from the most used items', 'kibble' ),
        'not_found'                  => __( 'Not Found', 'kibble' ),
    $args = array(
        'labels'                     => $labels,
        'hierarchical'               => true,
        'public'                     => true,
        'show_ui'                    => true,
        'show_admin_column'          => true,
        'show_in_nav_menus'          => true,
        'show_tagcloud'              => true,
    register_taxonomy( 'video_genre', 'video_type', $args );


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

    add_filter('post_type_link', 'filter_post_type_link', 10, 2);

    function filter_post_type_link( $link, $post ) {

    if ( strpos( $link, '%video_genre%' ) === false )
        return $link;

    $terms = wp_get_object_terms( $post->ID, 'video_genre' );

    if ( current( $terms ) ) :

        $new_parent = current( $terms );

        foreach ( $terms as $term ) {
            if ( $term->parent == 0 ) {
                $new_parent = $term;

        $new_parent = $new_parent ? $new_parent : $terms[0];

        $category_string = $new_parent->slug;

        $category_string = trim( $category_string, '/' );


    // set a default
    $category_string = ( isset( $category_string ) && $category_string ) ? $category_string : 'uncategorized';

    $link = str_replace( '%video_genre%', $category_string, $link );
    return $link;
  • Works fine for me. – Brad Dalton Feb 26 '14 at 3:08
  • For 'normal' pages and posts, it shows 404 error. And yes, I had flushed it. – Daniel Feb 26 '14 at 3:10
  • Weird. Doesn't 404 for me. – Brad Dalton Feb 26 '14 at 3:54
  • Reproduced. Turning off your code makes regular permalinks work, turning it on makes 'em 404! – Bendoh Feb 26 '14 at 16:47
  • Weird, getting exatly what Daniel is saying – Pieter Goosen Feb 26 '14 at 17:02

Short answer

Add this to your functions.php file or wherever you have the rest of this code, assuming this is all in your theme. If this is a plugin, the first two lines need to happen on setup_theme, so you need to hook into that action.

Also, if it were me, I'd put everything into a class. If you did that, use a class property instead of a global $video_type_rewrite_rules variable.

$GLOBALS['wp_rewrite']->use_verbose_page_rules = true;

function wpse_135707_video_type_rewrite_rules( $rules ) {
    $GLOBALS['video_type_rewrite_rules'] = $rules;
    return array();
add_filter( 'video_type_rewrite_rules', 'wpse_135707_video_type_rewrite_rules' );

function wpse_135707_add_video_type_to_rewrite_rules_array( $rules ) {
    return array_merge( $rules, $GLOBALS['video_type_rewrite_rules'] );
add_filter( 'rewrite_rules_array', 'wpse_135707_add_video_type_to_rewrite_rules_array' );

Long Answer

What's happening here is your rewrite rules are too generic. The way WordPress rewrite rules work, the rules are regular expressions in a specifically-ordered array. When a request comes in, the array is looped-through in an attempt to match the path. Once a match is found (with one exception, which I'll get to soon), the code exits the loop and the remaining rules are ignored. Normally, a taxonomy or post type will have a static "anchor" for the rules, so two post types won't conflict. That is, if your post type's permastruct was 'video/%video_genre%' this wouldn't have been a problem. In other words, %video_genre% gets replaced with a regular expression which is extremely generic. Without a fixed string of text in the regular expression, it will match any path at that url depth (same number of slashes in the URL).

All that said, you can set a permalink structure to /%category%/%postname%/, so why doesn't that cause pages to break? There's an exception for some permalink structures, and the solution above takes advantage of that. This is where $use_verbose_page_rules comes in; if the url structure starts with the postname, category, tag, or author, WordPress sets $use_verbose_page_rules to true, which causes two changes. First, the rewrite rule order changes, setting pages before posts, which isn't a big deal. Second, when that rule matches a request, WordPress checks to see if the page actually exists before committing to that rewrite rule. If it doesn't exist, it continues looping over the rules. This is the one exception where that happens. The main downside here is that every request which is not a page results in an extra database query with a search on text fields. If you have a massive database (hundreds of thousands of posts), this operation can have a noticeable impact on your page load. Anyway, the other thing we're doing here is moving the post type's rewrite rules after the page rewrite rules, so the page will match first, check to see if it exists, and if not, match the video rules.

Other Options

  1. By far the best option is to avoid rewrite rules which are this generic. To do this, provide static text in the rewrite rules for the post type, as mentioned above (e.g. 'video/%video_genre%'.
  2. Hook into the request process to see if a video exists before "committing" to that being the request. This is kind of a pain, but I covered it in this question. This option allows you to have any permalink structure you want, and have other post types with generic rewrite rules like this one.

The solution I provided is not as flexible as (2), and not as ideal as (1), but it's a good compromise between the two. It's also worth noting that your site's permalink structure has to either have a static base or be date-based. That is, if you prefix your permalink structure with the category, tag, or author, or had no prefix, these rules would conflict.

Lastly, be sure to flush your rewrites after making any of these changes.

  • I couldn't test it since I'm getting Notice: Undefined index: video_type_rewrite_rules and Warning: array_merge(): Argument #2 is not an array. Any ideas? – Daniel Feb 28 '14 at 2:14
  • Please ignore above. I did test it, but I'm still getting 404 for non-CPT pages. – Daniel Feb 28 '14 at 2:58
  • When I take a fresh installation with no plugins, add this functions.php file, and set the permalink structure to "Day and Name" (/%year%/%monthnum%/%day%/%postname%/), then I can access Videos, Posts, and Pages no problem. You might want to check your permalink structure and plugins. – Matthew Boynes Feb 28 '14 at 4:05
  • If you set the permalink to any structure rather then postname, it will works. I don't know why. However, that don't solve my issue. – Daniel Feb 28 '14 at 7:20
  • Why doesn't it solve your issue? Is your permalink structure set to /%postname%/? (I'm not referring to your custom post type, I'm referring to posts, which is set in Settings->Permalinks – Matthew Boynes Feb 28 '14 at 14:44

I tried your code and resetting permalink is behaving abnormal. But I have noticed a weird behavior while setting permalink with a new register_taxanomy() term.

I tried a plugin to reset the permalink of custom post type and succeeded with a little change.

Plugin is: WP Permastructure

What I did is this: 1) Register the post type in functions.php without defining rewrite parameter, because I was going to manage it with the plugin (link above).

2) Removed all the unnecessary code and kept only which can register a post-type and a taxonomy.

3) Installed the plugin. Visit Settings>Permalinks.

4) Set the permalink for Video post type.

Now the Weird Behaviour

a) If I set the permalink of video post type to /%video_genre%/%postname%/ things are not working and getting the same behavior as you are getting. 404 for non-CPT posts.

b) If I set the permalink to /%video_genre%/%postname%/%post_id%. Including the post_id too. It is working and displaying video_genre in the url. All other posts are working fine too.

Weird right!!

c)Then I decided to come up a something close to this behavior and set the permalink to /%video_genre%/%postname%/1. Adding /1 at the end of it. And guess what it works. Couldn't found any explanation to this behavior.

As long as it satisfies your need. It's good. Give it a try.

  • Without knowing what is causing the weird behaviour is too risky for me to use this solution. However, I'll give it a try. – Daniel Feb 26 '14 at 8:06
  • 1
    Interesting, but not really unexpected behaviour given that @Daniel is after /%term%/%postname%/. That's very generic in terms of the regex it generates to match the URL and so is very prone to rewrite conflicts. What's happening is that the URL matches a regex earlier in the array for the wrong thing so fails to match anything in the database. You can debug this using print_r($wp); in your header.php to see what rules are being matched. The addition of /1 or %post_id% to the permalink makes it unique enough to avoid conflicts. You should use %post_id% as the query will be faster – sanchothefat Feb 27 '14 at 12:58

My results are what you experienced: With this code enabled, plain posts return 404. If I disable the code, but do NOT reset my permalinks, I get the wrong post.

The answer above doesn't really explain what's happening, so I let loose my debugger.

What I'm seeing is that for plain posts it's trying to set the video_genre and video_type query vars after parsing the permalink for plain posts, which is incorrect.

I thought it looked weird that you were using a substitution variable in your rewrite slug, and it turns out I was correct that it was the problem. Using those substitution variables gives you permalink match patterns like:

%video_genre%/feed/(feed|rdf|rss|rss2|atom)/ => $post_type=video_type&feed=$matches[1]


([^/]+)/([^/]+)(/[0-9]+)?/?$ => index.php?video_genre=$matches[1]&video_type=$matches[2]&page=$matches[3]

Which is the rule that is matching my request for a regular post, thus setting video_genre and video_type query variables and not the correct ones that would actually result in a post.

So while the subtitution variable is useful for creating the link as you're doing in your filter_post_type_link, it breaks the permalink matching system.


What you're trying to do creates an inherit conflict in the permalink structure, because WordPress doesn't know if you're trying to access a video_type CPT or a regular post.

What I would do instead, is set the rewrite argument to false where you call register_post_type. Then in your post_type_link filter, return a link explicitly based on the post type being video_type rather than relying on the the substitution variables you were using.

function filter_post_type_link( $link, $post ) {
    if( $post->post_type != 'video_type' )
        return $link;

    $terms = wp_get_object_terms( $post->ID, 'video_genre' );

    if ( current( $terms ) ) :

        $new_parent = current( $terms );

        foreach ( $terms as $term ) {
            if ( $term->parent == 0 ) {
                $new_parent = $term;

        $new_parent = $new_parent ? $new_parent : $terms[0];

        $category_string = $new_parent->slug;

        $category_string = trim( $category_string, '/' );


    // set a default
    $category_string = ( isset( $category_string ) && $category_string ) ? $category_string : 'uncategorized';

    $link = home_url( path_join( $category_string, $post->post_name ) );
    return $link;

And then parse the request manually and set the required variables:

function wpse135707_parse_request( $wp ) {
    $terms = get_terms( 'video_genre', array( 'hide_empty' => false, 'fields' => 'id=>slug' ) );

    if( preg_match( '#^(' . join( '|', $terms ) . ')/([^/]+)#', $wp->request, $matches ) ) {
        $wp->query_vars = array(
            'post_type' => 'video_type',
            'name' => $matches[2]
add_action( 'parse_request', 'wpse135707_parse_request' );

There is a downside to this approach, however, in that there could be a conflict with other permalinks. This will parse any URL starting with any video_genre slug and assume it's a video_type post, which is probably fine for your purposes.

  • It didn't work. I'm using a fresh install base and I had flushed it. I still getting 404. – Daniel Feb 27 '14 at 3:05
  • I would have to know what you have for existing video genres and video_type posts, as well as what your plain permalink format is to really help diagnose why. Did you remember to set 'rewrite' => false in register_post_type? – Bendoh Feb 27 '14 at 18:11
  • I'm testing it in a fresh install, using the default WP theme. – Daniel Feb 28 '14 at 18:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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