Skip to content

Everything for WordPress, web development — and beyond

⚙️ WordPress: how to set a default template for a custom post type

⚙️ WordPress: how to set a default template for a custom post type

You handed the client a site on WordPress with a portfolio, and they call back the next day: "I'm adding a new entry, but the template keeps reverting to the default one with a sidebar. How do I lock it in?" The situation is painfully familiar: click "Add New" in a custom post type, and instead of the full-width layout you built, out comes a blog column with a sidebar. Choosing the template manually for every post is pointless busywork that frustrates both you and the client. Especially when "Full Width" with no sidebar is what you need almost every time.

By default, WordPress uses single.php from the theme for custom post types (CPTs). And single.php is usually tailored for a blog: title, content, sidebar with widgets. For portfolios, case studies, testimonials, or products, that layout is a poor fit. The good news: WordPress offers four ways to assign a template to a CPT, from the simplest (a file in the theme, no code at all) to fully programmatic via hooks and block themes. Each method gets the job done reliably; the only difference is the level of control and ease of maintenance.

💡 Quick overview:

  • Create a single-{post_type}.php file in the theme root, and WordPress will pick it up automatically via the template hierarchy
  • Hook into the template_include filter in functions.php to assign a template programmatically without placing a physical file in the theme
  • For block themes, create single-{post_type}.html in the templates/ folder; it can be edited through the Site Editor with no code
  • After any change, flush permalinks: "Settings → Permalinks → Save Changes," otherwise WordPress won't see the new template

Method 1: template file via the WordPress hierarchy

The most reliable and straightforward method. Since version 3.0, WordPress has supported custom templates for CPTs through the standard template hierarchy. Here is how it works: when a visitor opens an entry of type portfolio, WordPress looks for a template in a strict order, top to bottom, and uses the first file it finds:

single-portfolio.phpsingle.phpsingular.phpindex.php

You need to create a file named single-{post_type}.php, where {post_type} is your custom type's slug. Place it in the root of the active theme:

1/* File: /wp-content/themes/your-theme/single-portfolio.php */
2
3<?php get_header(); ?>
4
5<div class="full-width-content">
6 <?php while ( have_posts() ) : the_post(); ?>
7 <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
8 <h1><?php the_title(); ?></h1>
9 <div class="entry-content">
10 <?php the_content(); ?>
11 </div>
12 </article>
13 <?php endwhile; ?>
14</div>
15
16<?php get_footer(); ?>
17

The quickest way to start: copy the contents of page.php or template-fullwidth.php from your theme and adapt the layout for the specific CPT. If no single-{post_type}.php file exists, WordPress silently falls back to single.php, and the client ends up with a sidebar.

The upside of this method: no code in functions.php; the file simply sits in the theme and works. The downside: the template is tightly coupled to the theme. If you switch themes, the file stays in the old folder and stops working.

Method 2: programmatically via the template_include filter

The file-based method works as long as you control the theme. But if you are developing a plugin or a child theme that should not depend on a file being present in the root, you need the template_include hook.

The filter fires before the template loads and lets you point to a PHP file from any location:

1/* File: functions.php (theme) or main plugin file */
2
3add_filter( 'template_include', 'sd_cpt_default_template', 99 );
4
5function sd_cpt_default_template( $template ) {
6 if ( is_singular( 'portfolio' ) ) {
7 $custom_template = plugin_dir_path( __FILE__ ) . 'templates/single-portfolio.php';
8 if ( file_exists( $custom_template ) ) {
9 return $custom_template;
10 }
11 }
12 return $template;
13}

What is happening here:

  • is_singular('portfolio') checks whether we are on a single entry page of the portfolio type
  • If the condition is true, WordPress loads the template from the plugin folder, bypassing the theme hierarchy
  • Priority 99 ensures the filter fires last and is not overridden by the theme

This approach is convenient when the template needs to live inside a plugin and survive theme changes. The templates/single-portfolio.php file inside the plugin can use get_header() and get_footer() from the active theme, so visually everything stays consistent with the site's design.

Method 3: the {$type}_template filter

WordPress provides a dynamic hook {$type}_template, where $type is single, archive, or page. For a CPT it takes the form single-portfolio_template and lets you override the template precisely, without extra checks inside the callback:

1add_filter( 'single_template', 'sd_cpt_template_by_type' );
2
3function sd_cpt_template_by_type( $single_template ) {
4 global $post;
5
6 if ( 'portfolio' === $post->post_type ) {
7 $custom_template = get_stylesheet_directory() . '/single-portfolio.php';
8 if ( file_exists( $custom_template ) ) {
9 return $custom_template;
10 }
11 }
12 return $single_template;
13}

The difference from Method 2 is semantic: you are explicitly saying "change the template for single entries" instead of "intercept all templates." The code is slightly cleaner, but functionally both hooks solve the problem in the same way.

Method 4: template in a block theme (Full Site Editing)

Starting with WordPress 5.9 and the shift to block themes, the approach changes. In FSE themes, templates are HTML files in the templates/ folder, not PHP. For a custom post type, create:

1/wp-content/themes/your-fse-theme/templates/single-portfolio.html

Inside, you use a block-based structure. A minimal example:

1<!-- wp:template-part {"slug":"header","theme":"your-fse-theme"} /-->
2
3<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
4<main class="wp-block-group">
5 <!-- wp:post-title {"level":1} /-->
6 <!-- wp:post-featured-image /-->
7 <!-- wp:post-content {"layout":{"type":"constrained"}} /-->
8</main>
9<!-- /wp:group -->
10
11<!-- wp:template-part {"slug":"footer","theme":"your-fse-theme"} /-->

You can edit this template directly in the Site Editor (Appearance → Editor) without touching any code. If you switch block themes, the file stays in the old folder, the same vulnerability as Method 1.

Which method to choose

A quick decision matrix for your scenario:

Scenario

Method

One theme, one CPT, no plugins

Method 1: single-{post_type}.php

Plugin with its own template

Method 2: template_include

Child theme, need to override the parent template

Method 1 or 3

FSE / block theme (Twenty Twenty-Four and newer)

Method 4: templates/single-{post_type}.html

After implementing any of the methods, be sure to go to "Settings → Permalinks" and click "Save Changes." This flushes the rewrite rules cache, and WordPress starts recognizing the new template.

⁉️🤔 Frequently asked questions

The template is not being picked up. What should I do?

First, flush permalinks ("Settings → Permalinks → Save"). Second, verify that the CPT slug in the filename matches the slug from register_post_type(). Third, if a caching plugin is active, clear the cache. In practice, the problem is most often about flushing rewrite rules after registering the CPT, not about the template code itself.

Can I assign one template to multiple CPTs at once?

Yes. In Method 2, use an array in is_singular(): is_singular( array( 'portfolio', 'testimonials', 'team' ) ). Or check in_array( $post->post_type, array('portfolio', 'team') ) inside the callback. The template file is one, and the layout will be shared across all listed types.

The template works, but the layout is broken (missing sidebar/header styles).

You inherited the template from page.php, but the theme's styles depend on CSS classes on body. Add a body_class filter in functions.php so that WordPress applies a post-type-{slug} class to <body>, and the styles will kick in:

1add_filter( 'body_class', function( $classes ) {
2 if ( is_singular( 'portfolio' ) ) {
3 $classes[] = 'single-portfolio';
4 }
5 return $classes;
6} );

What happens when I switch themes? The template disappears.

Yes, the single-portfolio.php file stays in the old theme's folder. Your options: move the file to the new theme manually, use a child theme (the file survives parent theme updates), or move the template into a plugin via Method 2, which does not depend on the active theme.

What if I need ALL CPTs to open without a sidebar by default?

Use a universal filter with is_singular() without specifying a particular post_type, but exclude the standard post and page:

1add_filter( 'template_include', function( $template ) {
2 if ( is_singular() && ! is_singular( array( 'post', 'page' ) ) ) {
3 $fullwidth = get_stylesheet_directory() . '/template-fullwidth.php';
4 if ( file_exists( $fullwidth ) ) {
5 return $fullwidth;
6 }
7 }
8 return $template;
9}, 99 );

This solution applies a single full-width template to all custom post types at once: portfolios, testimonials, team members, case studies. Standard posts and pages are not affected.

Is it worth bothering with template_include when you can just create a file?

If you have one site, one theme, and one CPT, create single-{post_type}.php and forget about it. This is a solution that lasts for years: it does not break on WordPress updates, does not depend on the PHP version, and is instantly clear to any developer who opens the theme folder. Maintenance comes down to a single file you can fix in no time.

Programmatic methods via hooks are justified in two cases. First: the template is part of a distributed plugin, and you do not want to require users to copy files into the theme. Second: CPTs are registered dynamically through ACF, Toolset, or a similar plugin, and proliferating a dozen single-*.php files in the theme is impractical. For everything else, the WordPress template hierarchy is the simplest and most reliable path, proven over years.