Hem / WordPress development: Custom post types and permalinks

Wordpress development: Custom post types and permalinks

Have you, like us, run into problems with permalinks and custom post types in Wordpress? Have you spent hours after hours swearing at those 404’s because Wordpress’ inability to handle taxonomy archives of custom post types? We might just have the solution for you.

Our idea of the optimal permalink structure for custom post types and associated taxonomies is:

domain.com/custom-post-type = custom post type archive
domain.com/custom-post-type/single-post = single post of the custom post type
domain.com/custom-post-type/taxonomy/term = custom taxonomy archive

This is however not how custom post types are meant to work. It was explained to us by a member of the Wordpress support team that custom post types are, despite of its name, not posts at all. The emphasis should rather be on it being a type rather than post. To achieve a permalink structure along the lines of what we want, they suggest creating custom post types in custom post types like this:

domain.com/custom-post-type = custom post type archive
domain.com/custom-post-type/sub-custom-post-type = sub custom post type archive
domain.com/custom-post-type/sub-custom-post-type/single-post = single post of sub custom post type

While we acknowledge this as one way of doing it, it’s really not what we want for two reasons:

  • What if we want to categorize the posts by more than one taxonomy? A post can never be of more than one type at a time.
  • Managing custom post types is only done today by theme coding and/or third party plugins. There are no simple means of managing custom post types, at least not in a user friendly manner.

So, instead of using custom post types in custom post types, we want to categorize custom post types by taxonomies, as with normal posts and pages. However, here is where we run into problems. While registering taxonomies for our custom post types works just fine, the permalink structure domain.com/custom-post-type/taxonomy/term yields a 404-page. Again, they tell us; it’s not a bug, it’s a feature. Let’s just say that we’ve found a way to improve on this feature. 🙂

What we do is that we register our custom post types and taxonomies as usual. Then we add the following code to our theme’s functions.php:

function custom_init() {
    global $wp_rewrite;
     
    // Declare our permalink structure
    $post_type_structure = '/%post_type%/%taxonomy%/%term%';
 
    // Make wordpress aware of our custom querystring variables
    $wp_rewrite->add_rewrite_tag("%post_type%", '([^/]+)', "post_type=");
    $wp_rewrite->add_rewrite_tag("%taxonomy%", '([^/]+)', "taxonomy=");
    $wp_rewrite->add_rewrite_tag("%term%", '([^/]+)', "term=");
 
    // Only get custom and public post types
    $args=array(
        'public'   => true,
        '_builtin' => false
    );
    $output = 'names'; // names or objects, note names is the default
    $operator = 'and'; // 'and' or 'or'
    $post_types=get_post_types($args,$output,$operator);
    $post_types_string = implode("|", $post_types);
 
    $taxonomies=get_taxonomies($args,$output,$operator); // Note the use of same arguments as with get_post_types()
    $taxonomies_string = implode("|", $taxonomies);
 
    // Now add the rewrite rules, note that the order in which we declare them are important
    add_rewrite_rule('^('.$post_types_string.')/('.$taxonomies_string.')/([^/]*)/?','index.php?post_type=$matches[1]&$matches[2]=$matches[3]','top');
    add_rewrite_rule('^('.$post_types_string.')/([^/]*)/?','index.php?post_type=$matches[1]&name=$matches[2]','top');
    add_rewrite_rule('^('.$post_types_string.')/?','index.php?post_type=$matches[1]','top');
 
    // Finally, flush and recreate the rewrite rules
    flush_rewrite_rules();
}
 
function post_type_permalink($permalink, $post_id, $leavename){
    $post = get_post($post_id);
 
    // An array of our custom query variables
    $rewritecode = array(
        '%post_type%',
        '/%taxonomy%',
        '/%term%',
        $leavename? '' : '%postname%',
        $leavename? '' : '%pagename%',
    );
 
    // Avoid trying to rewrite permalinks when not applicable
    if ('' != $permalink && !in_array($post->post_status, array('draft', 'pending', 'auto-draft'))) {
        // Fetch the post type
        $post_type = get_post_type( $post->ID );
 
        // Setting these isn't necessary if the taxonomy has rewrite = true,
        // otherwise you need to fetch the relevant data from the current post
        $taxonomy = "";
        $term = "";
 
        // Now we do the permalink rewrite
        $rewritereplace = array(
            $post_type,
            $taxonomy,
            $term,
            $post->post_name,
            $post->post_name,
        );
        $permalink = str_replace($rewritecode, $rewritereplace, $permalink);
    }
 
    return $permalink;
}
 
// Create custom rewrite rules
add_action('init', 'custom_init');
 
// Translate the custom post type permalink tags
add_filter('post_type_link', 'post_type_permalink', 10, 3);

The above lines of code are actually all there is to it. What it does is defines /%post_type%/%taxonomy%/%term% as the new permalink structure and creates permalink rewrite rules for them with top priority. The rewrite rules are created by simply checking for possible custom post types and taxonomies in the database, this enables us to write very simple conditions in plain text for minimal overhead. Although, it should be noted that we have yet to test how much overhead this causes. So, we share this solution with the standard disclaimer; use at your own risk.