Template Basics: Single & Archive Templates for CPTs

September 25, 2025

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.html
  • templates/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_archive is true and flush permalinks once.
  • No featured image showing? Add add_theme_support('post-thumbnails') and confirm CPT supports thumbnail.
  • Wrong template loading? Confirm filename matches the CPT slug exactly: single-{slug}.php, archive-{slug}.php.

Summary

  1. Create single-{post_type}.php for individual CPT items, and archive-{post_type}.php for listings.
  2. Use the Loop, featured images, and taxonomy links inside these templates.
  3. Tweak archive queries via pre_get_posts and paginate with the_posts_pagination().
  4. In block themes, create templates/single-{type}.html and templates/archive-{type}.html using Query/Loop blocks.
  5. Flush permalinks after changes; verify slugs and supports for a smooth setup.
Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.