Template Basics: Single & Archive Templates for CPTs
After registering a Custom Post Type (CPT), you’ll usually want custom layouts for its single item pages and its archive (listing) page. WordPress makes this easy with the template hierarchy:
single-{post_type}.php→ Single item (e.g.,single-book.php)archive-{post_type}.php→ Archive list (e.g.,archive-book.php)- Fallbacks:
single.php/archive.php/index.php
1) File Structure & Prerequisites
wp-content/
themes/yourtheme/
single-book.php (single CPT template)
archive-book.php (archive CPT template)
template-parts/
content-book.php (optional reusable part)
Make sure your CPT has archives enabled in its registration (in a plugin or functions.php):
<?php
register_post_type( 'book', array(
'label' => 'Books',
'public' => true,
'has_archive' => true, // <— enables /books/ archive
'rewrite' => array( 'slug' => 'books' ),
'supports' => array( 'title','editor','thumbnail','excerpt' ),
'show_in_rest'=> true,
) );
Tip: After adding templates or changing slugs, go to Settings → Permalinks and click Save Changes once to flush rewrite rules.
2) Single Template: single-book.php
Base example with featured image, meta, and taxonomy links:
<?php get_header(); ?>
<main id="primary" class="site-main single-book">
<?php if ( have_posts() ) : while ( have_posts() ) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<header class="entry-header">
<h1 class="entry-title"><?php the_title(); ?></h1>
<?php
// Example taxonomy: genre
$genres = get_the_terms( get_the_ID(), 'genre' );
if ( $genres && ! is_wp_error( $genres ) ) {
echo '<p class="book-genres"><strong>Genres:</strong> ';
echo implode( ', ', array_map( function( $t ){
return '<a href="' . esc_url( get_term_link( $t ) ) . '">' . esc_html( $t->name ) . '</a>';
}, $genres ) );
echo '</p>';
}
?>
</header>
<?php if ( has_post_thumbnail() ) : ?>
<figure class="entry-thumb"><?php the_post_thumbnail( 'large' ); ?></figure>
<?php endif; ?>
<div class="entry-content">
<?php the_content(); ?>
</div>
<footer class="entry-footer">
<p class="book-meta">Published: <time datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>"><?php echo esc_html( get_the_date() ); ?></time></p>
<?php
wp_link_pages( array(
'before' => '<div class="page-links">Pages:',
'after' => '</div>'
) );
?>
</footer>
</article>
<nav class="post-navigation">
<div class="nav-previous"><?php previous_post_link( '%link', '← Previous Book' ); ?></div>
<div class="nav-next"><?php next_post_link( '%link', 'Next Book →' ); ?></div>
</nav>
<?php comments_template(); ?>
<?php endwhile; endif; ?>
</main>
<?php get_footer(); ?>
3) Archive Template: archive-book.php
Lists CPT items with pagination and (optional) taxonomy filter display:
<?php get_header(); ?>
<main id="primary" class="site-main archive-book">
<header class="page-header">
<h1 class="page-title"><?php post_type_archive_title(); ?></h1>
<?php
// Optional: display top-level Genre filters
$terms = get_terms( array( 'taxonomy' => 'genre', 'hide_empty' => true, 'parent' => 0 ) );
if ( ! is_wp_error( $terms ) && $terms ) {
echo '<ul class="genre-filters">';
foreach ( $terms as $t ) {
echo '<li><a href="' . esc_url( get_term_link( $t ) ) . '">' . esc_html( $t->name ) . '</a></li>';
}
echo '</ul>';
}
?>
</header>
<?php if ( have_posts() ) : ?>
<div class="book-grid">
<?php while ( have_posts() ) : the_post(); ?>
<article id="post-<?php the_ID(); ?>" <?php post_class('book-card'); ?>>
<a href="<?php the_permalink(); ?>" class="book-card__link">
<div class="book-card__thumb">
<?php if ( has_post_thumbnail() ) { the_post_thumbnail( 'medium' ); } ?>
</div>
<h2 class="book-card__title"><?php the_title(); ?></h2>
<p class="book-card__excerpt"><?php echo esc_html( wp_trim_words( get_the_excerpt(), 20 ) ); ?></p>
</a>
</article>
<?php endwhile; ?>
</div>
<div class="pagination"><?php the_posts_pagination(); ?></div>
<?php else : ?>
<p>No items found.</p>
<?php endif; ?>
</main>
<?php get_footer(); ?>
4) Reuse Markup via Template Parts
Keep templates clean by moving a card/list item into template-parts/content-book.php and include it with:
<?php get_template_part( 'template-parts/content', 'book' ); ?>
5) Customizing the Archive Query (Optional)
Change sorting, posts per page, or filter by taxonomy using pre_get_posts:
<?php
add_action( 'pre_get_posts', function( $q ){
if ( is_admin() || ! $q->is_main_query() ) return;
if ( $q->is_post_type_archive( 'book' ) ) {
$q->set( 'posts_per_page', 12 );
$q->set( 'orderby', 'date' );
$q->set( 'order', 'DESC' );
// Example: filter by a genre term from a query var ?genre=fantasy
if ( ! empty( $_GET['genre'] ) ) {
$q->set( 'tax_query', array(
array(
'taxonomy' => 'genre',
'field' => 'slug',
'terms' => sanitize_text_field( wp_unslash( $_GET['genre'] ) ),
),
) );
}
}
} );
6) Minimal CSS (Optional)
.archive-book .book-grid {
display: grid; gap: 24px;
grid-template-columns: repeat(auto-fill, minmax(240px,1fr));
}
.book-card { border: 1px solid #eee; padding: 16px; border-radius: 8px; background: #fff; }
.book-card__thumb img { width: 100%; height: auto; display: block; }
.book-genres { margin: .5rem 0 1rem; color: #666; }
7) Block Theme (FSE) Equivalents
If you’re using a block theme, create HTML templates instead:
templates/single-book.htmltemplates/archive-book.html
Example: templates/archive-book.html
<!--
Title: Books Archive
Template Types: archive
-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<!-- wp:heading {"level":1} --><h1>Books</h1><!-- /wp:heading -->
<!-- wp:query {"queryId":1,"query":{"postType":"book","perPage":12}} -->
<!-- wp:post-template -->
<!-- wp:group {"className":"book-card"} -->
<!-- wp:post-featured-image {"sizeSlug":"medium"} /-->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-excerpt {"moreText":"Read more"} /-->
<!-- /wp:group -->
<!-- /wp:post-template -->
<!-- wp:query-pagination /-->
<!-- /wp:query -->
<!-- /wp:group -->
For single pages, use templates/single-book.html with blocks like Post Title, Featured Image, Post Content, and Post Terms (for taxonomies).
8) Common Gotchas
- 404 on archives? Ensure
has_archiveis true and flush permalinks once. - No featured image showing? Add
add_theme_support('post-thumbnails')and confirm CPT supportsthumbnail. - Wrong template loading? Confirm filename matches the CPT slug exactly:
single-{slug}.php,archive-{slug}.php.
Summary
- Create
single-{post_type}.phpfor individual CPT items, andarchive-{post_type}.phpfor listings. - Use the Loop, featured images, and taxonomy links inside these templates.
- Tweak archive queries via
pre_get_postsand paginate withthe_posts_pagination(). - In block themes, create
templates/single-{type}.htmlandtemplates/archive-{type}.htmlusing Query/Loop blocks. - Flush permalinks after changes; verify slugs and supports for a smooth setup.
🎨 Want to learn more? Visit our WordPress Customization Hub for tips and advanced techniques.