SponsoredHosinger Special Offer

WP WordPress Theme Development

Build classic and block WordPress themes from scratch: the template hierarchy, The Loop, the_title() and the_content(), template parts, menus, the Customizer, child themes and theme.json.

Templates for Custom Post Types

WordPress Theme Development Lesson 18 of 30 ~5 min read

Overview

When a plugin registers a custom post type such as book, your theme can give it its own look by adding single-book.php and archive-book.php — the template hierarchy picks them up automatically.

Remember the rule from lesson 1: the post type itself is registered in a plugin. The theme only provides templates for it. If the theme is switched, the books survive and simply fall back to single.php.

Which files to create

Visitor seesTemplate
One book: /books/the-hobbit/single-book.php
All books: /books/archive-book.php (needs has_archive set when the type is registered)
Books in a genre: /genre/fantasy/taxonomy-genre.php

single-book.php with custom fields

<?php
/**
 * Displays a single Book.
 */
get_header();
?>

<main id="main" class="site-main book">

    <?php while ( have_posts() ) : the_post(); ?>

        <?php
        // Custom fields saved by the books plugin.
        $isbn   = get_post_meta( get_the_ID(), '_gdc_isbn', true );
        $author = get_post_meta( get_the_ID(), '_gdc_book_author', true );
        $pages  = (int) get_post_meta( get_the_ID(), '_gdc_pages', true );
        ?>

        <article id="post-<?php the_ID(); ?>" <?php post_class( 'book__article' ); ?>>
            <div class="book__layout">
                <div class="book__cover">
                    <?php the_post_thumbnail( 'medium_large' ); ?>
                </div>

                <div class="book__details">
                    <?php the_title( '<h1 class="book__title">', '</h1>' ); ?>

                    <dl class="book__facts">
                        <?php if ( $author ) : ?>
                            <dt><?php esc_html_e( 'Author', 'gdc-starter' ); ?></dt>
                            <dd><?php echo esc_html( $author ); ?></dd>
                        <?php endif; ?>

                        <?php if ( $pages ) : ?>
                            <dt><?php esc_html_e( 'Pages', 'gdc-starter' ); ?></dt>
                            <dd><?php echo esc_html( number_format_i18n( $pages ) ); ?></dd>
                        <?php endif; ?>

                        <?php if ( $isbn ) : ?>
                            <dt><?php esc_html_e( 'ISBN', 'gdc-starter' ); ?></dt>
                            <dd><?php echo esc_html( $isbn ); ?></dd>
                        <?php endif; ?>
                    </dl>

                    <?php the_terms( get_the_ID(), 'genre', '<p class="book__genres">' . esc_html__( 'Genres: ', 'gdc-starter' ), ', ', '</p>' ); ?>

                    <div class="entry-content"><?php the_content(); ?></div>
                </div>
            </div>
        </article>

    <?php endwhile; ?>

</main>

<?php
get_footer();

Reading custom fields safely

  • get_post_meta( $id, $key, true ) — the third argument true returns a single value. Without it you get an array.
  • It returns an empty string when the field is not set, so the if ( $author ) checks keep empty rows out of the page.
  • Escape on output: esc_html() for text, esc_url() for links. Field values come from users and must never be printed raw.
  • number_format_i18n() formats numbers for the site language — 1,234 in English, 1.234 in German.

archive-book.php

<?php
get_header();
?>

<main id="main" class="site-main">
    <header class="page-header">
        <h1 class="page-title"><?php post_type_archive_title(); ?></h1>
    </header>

    <?php if ( have_posts() ) : ?>
        <div class="book-grid">
            <?php while ( have_posts() ) : the_post(); ?>
                <?php get_template_part( 'template-parts/content', 'book' ); ?>
            <?php endwhile; ?>
        </div>
        <?php the_posts_pagination(); ?>
    <?php else : ?>
        <?php get_template_part( 'template-parts/content', 'none' ); ?>
    <?php endif; ?>
</main>

<?php
get_footer();

Changing how many books an archive shows

Do not run a new query in the template. Adjust the main query before it runs with pre_get_posts:

<?php
function gdc_starter_books_per_page( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }

    if ( $query->is_post_type_archive( 'book' ) ) {
        $query->set( 'posts_per_page', 12 );
        $query->set( 'orderby', 'title' );
        $query->set( 'order', 'ASC' );
    }
}
add_action( 'pre_get_posts', 'gdc_starter_books_per_page' );
Watch out: Always guard pre_get_posts with is_admin() and is_main_query(). Without them you change every query on the site — including dashboard lists and sidebar widgets.

Practice

  • Register a book post type (the Plugin Development path shows how), then create single-book.php.
  • Add custom fields and print them with get_post_meta(), escaping each one.
  • Show 12 books per page with pre_get_posts.
Create a free account to save which lessons you've finished. Save my progress