Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

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:

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

share|improve this question
    
Never used such a setup before, but if it's too complicated, why not just make a template file and associate it to the review in question. –  Shane Feb 2 '12 at 16:53
    
WP 3.4 automatically fetches single-{post_type}-{slug}.php, so upgrading to WP 3.4 is another option. –  yitwail Jul 29 '12 at 17:56
add comment

4 Answers

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:

share|improve this answer
    
I want to test this out, but it looks like there's something missing in your add_filter line at the end. –  supertrue Feb 2 '12 at 20:45
    
@supertrue Good catch. :) Found another missing ) inside the filter. Fixed. Maybe you want to exchange the dash with an underline before the slug inside the template. Just to let the suffix stand out better when looking over the templates. –  kaiser Feb 2 '12 at 22:01
    
Causes this error across site: Warning: array_unshift() [function.array-unshift]: The first argument should be an array in [line containing array_unshift] –  supertrue Feb 4 '12 at 0:02
    
Ok, but then something else is intercepting the core templates. The function works fine and $templates is an array. See the core functions in this pastebin (no expiration date). Make sure that you test this with an install without plugins and the default theme. Then activate one after each other and see if the error still occures. –  kaiser Feb 4 '12 at 0:47
    
Yeah, I debugged this an I get the final absolute path of the first found template back as string. I'll have to talk to some core developer about this, before altering the answer. Also: I mixed something up: slug is only available for terms and taxonomies. You should replace $post->post_name with what fits your permalink structure. Currently there's no way to automatically do this for all cases with retrieving and replacing the path depending on your perma struct and rewrite rules. Expect another update. –  kaiser Feb 4 '12 at 1:19
show 7 more comments

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

share|improve this answer
    
Why do you exit; there? –  kaiser Feb 2 '12 at 18:26
    
@kaiser Must have been in whatever tutorial I followed at the time, if it's not necessary i'll remove it. –  Shane Feb 2 '12 at 19:48
1  
@kaiser: The exit() is necessary to prevent loading the default template. –  scribu Feb 3 '12 at 0:30
    
@scribu Already found out. Thanks. –  kaiser Feb 3 '12 at 0:31
add comment

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.

share|improve this answer
add comment

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

share|improve this answer
    
Hi Greg - welcome to WPSE. Please only post answers as answers to the questions - not follow up questions. If you have a question that isn't answered by an existing answer and is too large for a comment, please open another question :) –  Stephen Harris Apr 18 '12 at 15:07
1  
the string/array question has been removed :-) –  Gregory Apr 18 '12 at 16:04
    
"Thank you Kaiser for the sample code." - You're welcome. –  kaiser Oct 23 '12 at 13:01
    
does it work for you? first of all, '$template' shouldn't be commented in your code.. and I think that instead of '$album_tax[0]->slug' there should be '$object->post_name', isn't it? –  gregmatys Nov 18 '13 at 14:14
    
fixed the $template line. thank you. $object->post_name? no. that would return the post's slug, but I need the slug of the album that the post is linked to. –  Gregory Nov 18 '13 at 23:21
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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