📄 Custom Post Type Templates
Create custom templates for displaying CPT content
Master template hierarchy, archives, and single post templates
Learning Objectives
- Understand CPT template hierarchy
- Create archive templates for CPTs
- Build single post templates
- Use template parts for CPT content
- Query and display CPT data
- Customize The Loop for CPTs
- Add pagination to CPT archives
- Create custom page templates for CPTs
CPT Template Hierarchy
WordPress follows a specific template hierarchy when displaying Custom Post Types. Understanding this hierarchy allows you to create targeted templates for your CPT content.
Key Concept
Single CPT Template Hierarchy
1. single-{post_type}-{slug}.php // Specific post
2. single-{post_type}.php // All posts of this type
3. single.php // Generic single post
4. singular.php // Any singular content
5. index.php // Fallback template
Archive CPT Template Hierarchy
1. archive-{post_type}.php // Specific CPT archive
2. archive.php // Generic archive
3. index.php // Fallback template
Taxonomy Template Hierarchy
1. taxonomy-{taxonomy}-{term}.php // Specific term
2. taxonomy-{taxonomy}.php // Specific taxonomy
3. taxonomy.php // Any taxonomy
4. archive.php // Generic archive
5. index.php // Fallback template
Theme File Structure for CPTs
theme/
├── single-product.php // Single product template
├── archive-product.php // Product archive
├── single-portfolio.php // Single portfolio
├── archive-portfolio.php // Portfolio archive
├── taxonomy-product_category.php // Product category
├── template-parts/
│ ├── content/
│ │ ├── content-product.php // Product content
│ │ ├── content-portfolio.php // Portfolio content
│ │ └── content-testimonial.php // Testimonial content
│ └── loops/
│ ├── loop-product.php // Product loop
│ └── loop-portfolio.php // Portfolio loop
└── page-templates/
└── template-products.php // Custom page template
Single CPT Template
single-product.php
<?php
/**
* Single Product Template
*
* @package MyTheme
*/
get_header(); ?>
<div id="primary" class="content-area">
<main id="main" class="site-main">
<?php
while ( have_posts() ) :
the_post();
?>
<article id="post-<?php the_ID(); ?>" <?php post_class( 'single-product' ); ?>>
<header class="entry-header">
<?php the_title( '<h1 class="entry-title">', '</h1>' ); ?>
<div class="product-meta">
<?php
// Display custom fields
$price = get_post_meta( get_the_ID(), '_product_price', true );
$sku = get_post_meta( get_the_ID(), '_product_sku', true );
if ( $price ) {
echo '<span class="product-price">$' . esc_html( $price ) . '</span>';
}
if ( $sku ) {
echo '<span class="product-sku">SKU: ' . esc_html( $sku ) . '</span>';
}
?>
</div>
<?php
// Display product categories
$terms = get_the_terms( get_the_ID(), 'product_category' );
if ( $terms && ! is_wp_error( $terms ) ) :
?>
<div class="product-categories">
<span class="cat-label"><?php _e( 'Categories:', 'mytheme' ); ?></span>
<?php
foreach ( $terms as $term ) {
echo '<a href="' . esc_url( get_term_link( $term ) ) . '" class="product-category">';
echo esc_html( $term->name );
echo '</a>';
}
?>
</div>
<?php endif; ?>
</header>
<div class="product-gallery">
<?php if ( has_post_thumbnail() ) : ?>
<div class="featured-image">
<?php the_post_thumbnail( 'large' ); ?>
</div>
<?php endif; ?>
<?php
// Display gallery images
$gallery = get_post_meta( get_the_ID(), '_product_gallery', true );
if ( $gallery ) :
?>
<div class="product-thumbnails">
<?php
foreach ( $gallery as $image_id ) {
echo wp_get_attachment_image( $image_id, 'thumbnail' );
}
?>
</div>
<?php endif; ?>
</div>
<div class="entry-content">
<?php
the_content();
wp_link_pages( array(
'before' => '<div class="page-links">' . __( 'Pages:', 'mytheme' ),
'after' => '</div>',
) );
?>
</div>
<footer class="entry-footer">
<?php
// Display product tags
$tags = get_the_terms( get_the_ID(), 'product_tag' );
if ( $tags && ! is_wp_error( $tags ) ) :
?>
<div class="product-tags">
<span class="tag-label"><?php _e( 'Tags:', 'mytheme' ); ?></span>
<?php
foreach ( $tags as $tag ) {
echo '<a href="' . esc_url( get_term_link( $tag ) ) . '" class="product-tag">';
echo esc_html( $tag->name );
echo '</a>';
}
?>
</div>
<?php endif; ?>
</footer>
</article>
<?php
// Navigation to next/previous product
the_post_navigation( array(
'prev_text' => '<span class="nav-subtitle">' . __( 'Previous:', 'mytheme' ) . '</span> <span class="nav-title">%title</span>',
'next_text' => '<span class="nav-subtitle">' . __( 'Next:', 'mytheme' ) . '</span> <span class="nav-title">%title</span>',
) );
// If comments are open or we have at least one comment
if ( comments_open() || get_comments_number() ) :
comments_template();
endif;
endwhile; // End of the loop.
?>
</main>
</div>
<?php
get_sidebar();
get_footer();
Archive CPT Template
archive-product.php
<?php
/**
* Product Archive Template
*
* @package MyTheme
*/
get_header(); ?>
<div id="primary" class="content-area">
<main id="main" class="site-main">
<header class="page-header">
<h1 class="page-title"><?php post_type_archive_title(); ?></h1>
<?php
// Display archive description if set
$archive_description = get_the_archive_description();
if ( $archive_description ) :
?>
<div class="archive-description">
<?php echo wp_kses_post( wpautop( $archive_description ) ); ?>
</div>
<?php endif; ?>
<!-- Product Filters -->
<div class="product-filters">
<form method="get" class="filter-form">
<?php
// Category filter dropdown
wp_dropdown_categories( array(
'taxonomy' => 'product_category',
'name' => 'product_cat',
'show_option_all' => __( 'All Categories', 'mytheme' ),
'selected' => get_query_var( 'product_cat' ),
'hierarchical' => true,
'show_count' => true,
) );
?>
<!-- Sort dropdown -->
<select name="orderby" class="orderby">
<option value="date"><?php _e( 'Latest', 'mytheme' ); ?></option>
<option value="title"><?php _e( 'Name', 'mytheme' ); ?></option>
<option value="price"><?php _e( 'Price', 'mytheme' ); ?></option>
</select>
<button type="submit"><?php _e( 'Filter', 'mytheme' ); ?></button>
</form>
</div>
</header>
<?php if ( have_posts() ) : ?>
<div class="products-grid">
<?php
while ( have_posts() ) :
the_post();
// Include template part for product content
get_template_part( 'template-parts/content', 'product' );
endwhile;
?>
</div>
<?php
// Pagination
the_posts_pagination( array(
'mid_size' => 2,
'prev_text' => __( '« Previous', 'mytheme' ),
'next_text' => __( 'Next »', 'mytheme' ),
) );
else :
get_template_part( 'template-parts/content', 'none' );
endif;
?>
</main>
</div>
<?php
get_sidebar();
get_footer();
Template Parts for CPTs
template-parts/content-product.php
<?php
/**
* Template part for displaying products in loops
*
* @package MyTheme
*/
?>
<article id="post-<?php the_ID(); ?>" <?php post_class( 'product-item' ); ?>>
<div class="product-thumbnail">
<?php if ( has_post_thumbnail() ) : ?>
<a href="<?php the_permalink(); ?>">
<?php the_post_thumbnail( 'medium' ); ?>
</a>
<?php else : ?>
<a href="<?php the_permalink(); ?>">
<img src="<?php echo get_template_directory_uri(); ?>/images/placeholder.png" alt="<?php the_title_attribute(); ?>">
</a>
<?php endif; ?>
<?php
// Display sale badge
$on_sale = get_post_meta( get_the_ID(), '_product_on_sale', true );
if ( $on_sale ) :
?>
<span class="sale-badge"><?php _e( 'Sale!', 'mytheme' ); ?></span>
<?php endif; ?>
</div>
<div class="product-details">
<h3 class="product-title">
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
</h3>
<?php
// Display price
$price = get_post_meta( get_the_ID(), '_product_price', true );
if ( $price ) :
?>
<div class="product-price">
<span class="price">$<?php echo esc_html( $price ); ?></span>
<?php
$regular_price = get_post_meta( get_the_ID(), '_product_regular_price', true );
if ( $regular_price && $regular_price > $price ) :
?>
<span class="regular-price">$<?php echo esc_html( $regular_price ); ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="product-excerpt">
<?php the_excerpt(); ?>
</div>
<div class="product-actions">
<a href="<?php the_permalink(); ?>" class="button view-product">
<?php _e( 'View Product', 'mytheme' ); ?>
</a>
</div>
</div>
</article>
Querying CPTs in Templates
Custom Query for CPTs
<?php
// Query specific CPT
$args = array(
'post_type' => 'product',
'posts_per_page' => 12,
'orderby' => 'date',
'order' => 'DESC',
'meta_key' => '_product_featured',
'meta_value' => 'yes',
);
$products = new WP_Query( $args );
if ( $products->have_posts() ) :
while ( $products->have_posts() ) :
$products->the_post();
get_template_part( 'template-parts/content', 'product' );
endwhile;
wp_reset_postdata();
else :
echo '<p>' . __( 'No products found.', 'mytheme' ) . '</p>';
endif;
// Query with taxonomy
$args = array(
'post_type' => 'product',
'tax_query' => array(
array(
'taxonomy' => 'product_category',
'field' => 'slug',
'terms' => 'electronics',
),
),
);
// Query with multiple taxonomies
$args = array(
'post_type' => 'product',
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'product_category',
'field' => 'slug',
'terms' => 'electronics',
),
array(
'taxonomy' => 'product_tag',
'field' => 'slug',
'terms' => 'featured',
),
),
);
// Query with meta values
$args = array(
'post_type' => 'product',
'meta_query' => array(
array(
'key' => '_product_price',
'value' => 100,
'compare' => '<',
'type' => 'NUMERIC',
),
),
);
Custom Page Template for CPTs
page-templates/template-products.php
<?php
/**
* Template Name: Products Page
* Description: Custom page template to display products
*
* @package MyTheme
*/
get_header(); ?>
<div id="primary" class="content-area">
<main id="main" class="site-main">
<?php
// Display page content if exists
while ( have_posts() ) :
the_post();
?>
<header class="page-header">
<?php the_title( '<h1 class="page-title">', '</h1>' ); ?>
<?php if ( get_the_content() ) : ?>
<div class="page-content">
<?php the_content(); ?>
</div>
<?php endif; ?>
</header>
<?php endwhile; ?>
<?php
// Custom query for products
$paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
$products_args = array(
'post_type' => 'product',
'posts_per_page' => 9,
'paged' => $paged,
'orderby' => 'menu_order date',
'order' => 'DESC',
);
$products_query = new WP_Query( $products_args );
if ( $products_query->have_posts() ) :
?>
<div class="products-section">
<div class="products-grid">
<?php
while ( $products_query->have_posts() ) :
$products_query->the_post();
get_template_part( 'template-parts/content', 'product' );
endwhile;
?>
</div>
<?php
// Custom pagination for query
$big = 999999999; // need an unlikely integer
echo paginate_links( array(
'base' => str_replace( $big, '%#%', esc_url( get_pagenum_link( $big ) ) ),
'format' => '?paged=%#%',
'current' => max( 1, $paged ),
'total' => $products_query->max_num_pages,
'prev_text' => __( '« Previous', 'mytheme' ),
'next_text' => __( 'Next »', 'mytheme' ),
) );
?>
</div>
<?php
wp_reset_postdata();
else :
?>
<p><?php _e( 'No products found.', 'mytheme' ); ?></p>
<?php endif; ?>
</main>
</div>
<?php
get_sidebar();
get_footer();
CPT Template Functions
Best Practices
CPT Template Best Practices
- Use specific templates: Create targeted templates for each CPT
- Organize template parts: Keep reusable code in template-parts/
- Check for CPT features: Verify support before displaying elements
- Handle empty states: Provide messages when no content exists
- Add proper pagination: Use appropriate pagination functions
- Reset post data: Always use wp_reset_postdata() after custom queries
- Escape output: Sanitize and escape all dynamic content
- Consider performance: Optimize queries and use caching when needed
Remember that CPT templates must follow WordPress naming conventions exactly. A typo in the filename will cause WordPress to use the fallback template instead.
Practice Exercise
Create CPT Templates