Skip to main content

Course Progress

Loading...

📄 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
WordPress looks for templates in a specific order. The first matching template file it finds will be used to display the content. More specific templates override generic ones.

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

Function Description Usage
is_post_type_archive() Check if CPT archive is_post_type_archive('product')
is_singular() Check if single CPT is_singular('product')
get_post_type() Get current post type $type = get_post_type()
post_type_archive_title() Display archive title post_type_archive_title('', false)
get_post_type_archive_link() Get archive URL get_post_type_archive_link('product')
get_post_types() Get registered CPTs get_post_types(array('public' => true))
get_post_type_object() Get CPT object get_post_type_object('product')

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

Build a complete template system for your CPTs:

  1. Create single-product.php template
  2. Build archive-product.php template
  3. Create content-product.php template part
  4. Add custom fields display
  5. Implement taxonomy display
  6. Add pagination to archive
  7. Create filtering system
  8. Build related products section
  9. Style templates with CSS
  10. Test responsive layout

Additional Resources