single-{$post_type}-{slug}.php for custom post types

My favorite part of the WordPress template hierarchy is the ability to quickly create template files for pages by slug, without having to edit the page in WordPress to select a template.

We can currently do this:

Read More

page-{slug}.php

But I would like to be able to do this:

single-{post_type}-{slug}.php

So that, for example, in a post type called review, I could make a template for a post called “My Great Review” at single-review-my-great-review.php

Has anybody set this up before? single-{post_type}-{slug}.php

Related posts

Leave a Reply

6 comments

  1. A) The Base in Core

    As you can see in the Codex Template Hierarchy explanation, single-{$post_type}.php is already supported.


    B) Extending the core Hierarchy

    Now there’re gladly some filters and hooks inside /wp-includes/template-loader.php.

    • do_action('template_redirect');
    • apply_filters( 'template_include', $template )
    • AND: a specific filter inside get_query_template( $type, ... ) named: "$type}_template"

    B.1) How it works

    1. Inside the template loader file, the template gets loaded by a query var/wp_query conditional: is_*().
    2. The conditional then triggers (in case of a “single” template): is_single() && $template = get_single_template()
    3. This triggers then get_query_template( $type, $templates ), where $type is single
    4. Then we have the "{$type}_template" filter

    C) The solution

    As we only want to extend the hierarchy with one template that gets loaded before the actual "single-{$object->post_type}.php" template, we’ll intercept the hierarchy and add a new template to the beginning of the templates array.

    // Extend the hierarchy
    function add_posttype_slug_template( $templates )
    {
    
        $object = get_queried_object();
    
        // New 
        $templates[] = "single-{$object->post_type}-{$object->post_name}.php";
        // Like in core
        $templates[] = "single-{$object->post_type}.php";
        $templates[] = "single.php";
    
        return locate_template( $templates );    
    }
    // Now we add the filter to the appropriate hook
    function intercept_template_hierarchy()
    {
        add_filter( 'single_template', 'add_posttype_slug_template', 10, 1 );
    }
    add_action( 'template_redirect', 'intercept_template_hierarchy', 20 );
    

    NOTE: (If you want to use something other than the default objects slug) You’ll have to adjust $slug according to your permalink-structure. Just use whatever you need from the global (object) $post.

    Trac Tickets

    As the above approach is currently not supported (you can only filter the absolute located path this way), here’s a list of trac tickets:

  2. Following the Template Hierarchy image, I don’t see such an option.

    So heres how i’d go about it:

    Solution 1 (Best in my opinion)

    Make a template file and associate it to the review

     <?php
     /*
     Template Name: My Great Review
     */
     ?>
    

    Adding the template php file in your theme directory, it would appear as a template option in your post’s edit page.

    Solution 2

    This could probably be achieved using template_redirect hook.

    In functions.php file :

     function my_redirect()
     {
          global $post;
    
          if( get_post_type( $post ) == "my_cpt" && is_single() )
          {
               if( file_exists( get_template_directory() . '/single-my_cpt-' . $post->post_name . '.php' ) )
               {
                    include( get_template_directory() . '/single-my_cpt-' . $post->post_name . '.php' );
                    exit;
               }
          }
     }
     add_action( 'template_redirect', 'my_redirect' );
    

    EDIT

    Added file_exists check

  3. The top answer (from 4 years ago) no longer works, but the WordPress codex has the solution here:

    <?php
    function add_posttype_slug_template( $single_template )
    {
        $object = get_queried_object();
        $single_postType_postName_template = locate_template("single-{$object->post_type}-{$object->post_name}.php");
        if( file_exists( $single_postType_postName_template ) )
        {
            return $single_postType_postName_template;
        } else {
            return $single_template;
        }
    }
    add_filter( 'single_template', 'add_posttype_slug_template', 10, 1 );
    ?>
    
  4. Use Page Templates

    Another approach for scalability would be to duplicate the page template drop-down functionality on the page post type for your custom post type.

    Reusable Code

    Duplication in code is not a good practice. Overtime it can cause severe bloat to a codebase when then make it very difficult for a developer to manage. Instead of creating a template for every single slug, you most likely will need a one-to-many template that can be reused instead of one-to-one post-to-template.

    The Code

    # Define your custom post type string
    define('MY_CUSTOM_POST_TYPE', 'my-cpt');
    
    /**
     * Register the meta box
     */
    add_action('add_meta_boxes', 'page_templates_dropdown_metabox');
    function page_templates_dropdown_metabox(){
        add_meta_box(
            MY_CUSTOM_POST_TYPE.'-page-template',
            __('Template', 'rainbow'),
            'render_page_template_dropdown_metabox',
            MY_CUSTOM_POST_TYPE,
            'side', #I prefer placement under the post actions meta box
            'low'
        );
    }
    
    /**
     * Render your metabox - This code is similar to what is rendered on the page post type
     * @return void
     */
    function render_page_template_dropdown_metabox(){
        global $post;
        $template = get_post_meta($post->ID, '_wp_page_template', true);
        echo "
            <label class='screen-reader-text' for='page_template'>Page Template</label>
                <select name='_wp_page_template' id='page_template'>
                <option value='default'>Default Template</option>";
                page_template_dropdown($template);
        echo "</select>";
    }
    
    /**
     * Save the page template
     * @return void
     */
    function save_page_template($post_id){
    
        # Skip the auto saves
        if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
            return;
        elseif ( defined( 'DOING_AJAX' ) && DOING_AJAX )
            return;
        elseif ( defined( 'DOING_CRON' ) && DOING_CRON )
            return;
    
        # Only update the page template meta if we are on our specific post type
        elseif(MY_CUSTOM_POST_TYPE === $_POST['post_type'])
            update_post_meta($post_id, '_wp_page_template', esc_attr($_POST['_wp_page_template']));
    }
    add_action('save_post', 'save_page_template');
    
    
    /**
     * Set the page template
     * @param string $template The determined template from the WordPress brain
     * @return string $template Full path to predefined or custom page template
     */
    function set_page_template($template){
        global $post;
        if(MY_CUSTOM_POST_TYPE === $post->post_type){
            $custom_template = get_post_meta($post->ID, '_wp_page_template', true);
            if($custom_template)
                #since our dropdown only gives the basename, use the locate_template() function to easily find the full path
                return locate_template($custom_template);
        }
        return $template;
    }
    add_filter('single_template', 'set_page_template');
    

    This is a bit of a late answer, but I thought it would be valuable since no one on the web has documented this approach as far as I can tell. Hope this helps someone out.

  5. In my case, I have Album and Track custom post types linked by an Album taxonomy. I wanted to be able to use different Single templates for the Album and Track posts depending on their Album taxonomy.

    Based on Kaiser’s answer above, I wrote this code. It works well.
    Note. I didn’t need the add_action().

    // Add an additional template option to the template hierarchy
    add_filter( 'single_template', 'add_albumtrack_taxslug_template', 10, 1 );
    function add_albumtrack_taxslug_template( $orig_template_path )
    {
        // at this point, $orig_template_path is an absolute located path to the preferred single template.
    
        $object = get_queried_object();
    
        if ( ! (
            // specify another template option only for Album and Track post types.
            in_array( $object->post_type, array( 'gregory-cpt-album','gregory-cpt-track' )) &&
            // check that the Album taxonomy has been registered.
            taxonomy_exists( 'gregory-tax-album' ) &&
            // get the Album taxonomy term for the current post.
            $album_tax = wp_get_object_terms( $object->ID, 'gregory-tax-album' )
            ))
            return $orig_template_path;
    
        // assemble template name
        // assumption: only one Album taxonomy term per post. we use the first object in the array.
        $template = "single-{$object->post_type}-{$album_tax[0]->slug}.php";
        $template = locate_template( $template );
        return ( !empty( $template ) ? $template : $orig_template_path );
    }
    

    I can now create templates named single-gregory-cpt-track-tax-serendipity.php and single-gregory-cpt-album-tax-serendipity.php and WP will use them automatically; ‘tax-serendipity’ is the slug for the first Album taxonomy term.

    for reference, the ‘single_template’ filter hook is declared in:
    /wp-includes/theme.php: get_query_template()

    Thank you Kaiser for the sample code.

    Cheers,
    Gregory

  6. Update for Brians code, I found that when the dropdown box was not being used the “default” template option was being saved into wp_page_template which caused it to try and find a template called default. this change just checks for the option “default” when saving and deletes the post meta instead (useful if you changed the template option back to default)

    elseif(MY_CUSTOM_POST_TYPE === $_POST['post_type']) {
    
    if ( esc_attr($_POST['_wp_page_template']) === "default" ) :
        delete_post_meta($post_id, '_wp_page_template');
    else :
        update_post_meta($post_id, '_wp_page_template', esc_attr($_POST['_wp_page_template']));
    endif;
    }