Custom Post Type in WordPress

What Is a Custom Post Type?

In WordPress, everything is a “post” under the hood, but post types categorize content into buckets with unique behaviors and templates. A CPT gives you:

  • Its own admin menu and editor features
  • Custom archive and single templates (e.g., /books/, /books/the-hobbit/)
  • REST API endpoints for headless/JS apps
  • Custom taxonomies (e.g., Genre) and meta fields (e.g., ISBN)

Quick Start: Production‑Ready CPT (as a Plugin)

Create a file wp-content/plugins/my-books-cpt.php and paste:

<?php
/**
 * Plugin Name: My Books CPT
 * Description: Registers a "Book" custom post type with taxonomy, REST, and meta fields.
 * Version: 1.0.0
 * Author: You
 */

if ( ! defined( 'ABSPATH' ) ) exit; // No direct access

// 1) Register the CPT
function mb_register_book_cpt() {
    $labels = [
        'name'               => __('Books', 'mybooks'),
        'singular_name'      => __('Book', 'mybooks'),
        'menu_name'          => __('Books', 'mybooks'),
        'add_new_item'       => __('Add New Book', 'mybooks'),
        'edit_item'          => __('Edit Book', 'mybooks'),
        'view_item'          => __('View Book', 'mybooks'),
        'all_items'          => __('All Books', 'mybooks'),
        'search_items'       => __('Search Books', 'mybooks'),
        'not_found'          => __('No books found.', 'mybooks'),
    ];

    $args = [
        'labels'             => $labels,
        'public'             => true,
        'show_in_menu'       => true,
        'menu_icon'          => 'dashicons-book',
        'supports'           => ['title','editor','excerpt','thumbnail','custom-fields','revisions'],
        'has_archive'        => true,                 // /books/
        'rewrite'            => ['slug' => 'books'],   // /books/book-title/
        'show_in_rest'       => true,                  // Gutenberg + REST
        'rest_base'          => 'book',
        'capability_type'    => 'post',
        'map_meta_cap'       => true,
        'publicly_queryable' => true,
        'hierarchical'       => false,
    ];

    register_post_type( 'book', $args );
}
add_action( 'init', 'mb_register_book_cpt' );

// 2) Register the taxonomy: Genre
function mb_register_book_genre_taxonomy() {
    register_taxonomy(
        'genre',
        ['book'],
        [
            'labels'       => ['name' => __('Genres','mybooks'),'singular_name' => __('Genre','mybooks')],
            'public'       => true,
            'hierarchical' => true,        // like categories
            'show_in_rest' => true,
            'rewrite'      => ['slug' => 'genre'],
        ]
    );
}
add_action( 'init', 'mb_register_book_genre_taxonomy' );

// 3) Register meta fields (author_name, isbn) with REST
function mb_register_book_meta() {
    register_post_meta( 'book', 'author_name', [
        'type'              => 'string',
        'single'            => true,
        'default'           => '',
        'sanitize_callback' => 'sanitize_text_field',
        'show_in_rest'      => true,
        'auth_callback'     => fn() => current_user_can('edit_posts'),
    ] );

    register_post_meta( 'book', 'isbn', [
        'type'              => 'string',
        'single'            => true,
        'sanitize_callback' => 'sanitize_text_field',
        'show_in_rest'      => true,
        'auth_callback'     => fn() => current_user_can('edit_posts'),
    ] );
}
add_action( 'init', 'mb_register_book_meta' );

// 4) Flush rewrites on activation/deactivation
function mb_books_activate() {
    mb_register_book_cpt();
    mb_register_book_genre_taxonomy();
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'mb_books_activate' );

function mb_books_deactivate() {
    flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'mb_books_deactivate' );

Activate it under Plugins → Installed Plugins.

Using Your CPT

  • Find a new admin menu item: Books.
  • Add content like normal posts: title, content, featured image, excerpt.
  • Use the registered meta fields (Author Name, ISBN) via the sidebar or custom UI.

REST API Endpoints

  • List books: /wp-json/wp/v2/book
  • Single book: /wp-json/wp/v2/book/{id}
  • Filter by genre slug: /wp-json/wp/v2/book?genre=fantasy

Theme Templates for CPT

Create these files in your active theme to control layout:

  • archive-book.php – CPT archive (e.g., /books/)
  • single-book.php – Single CPT page
  • taxonomy-genre.php – Genre archive

archive-book.php (example)

<?php get_header(); ?>
<main class="site-main">
  <h1><?php post_type_archive_title(); ?></h1>

  <?php if ( have_posts() ) : ?>
    <div class="books-grid">
      <?php while ( have_posts() ) : the_post(); ?>
        <article <?php post_class(); ?>>
          <a href="<?php the_permalink(); ?>">
            <?php if ( has_post_thumbnail() ) the_post_thumbnail('medium'); ?>
            <h2><?php the_title(); ?></h2>
          </a>
          <?php if ( $author = get_post_meta( get_the_ID(), 'author_name', true ) ) : ?>
            <p>Author: <?= esc_html( $author ); ?></p>
          <?php endif; ?>
          <?php the_excerpt(); ?>
        </article>
      <?php endwhile; ?>
    </div>

    <?php the_posts_pagination(); ?>
  <?php else : ?>
    <p>No books found.</p>
  <?php endif; ?>
</main>
<?php get_footer(); ?>

single-book.php (meta snippet)

<?php get_header(); ?>
<main class="site-main">
  <?php while ( have_posts() ) : the_post(); ?>
    <article <?php post_class(); ?>>
      <h1><?php the_title(); ?></h1>
      <?php if ( has_post_thumbnail() ) the_post_thumbnail('large'); ?>

      <ul class="book-meta">
        <?php if ( $author = get_post_meta( get_the_ID(), 'author_name', true ) ) : ?>
          <li><strong>Author:</strong> <?= esc_html( $author ); ?></li>
        <?php endif; ?>

        <?php if ( $isbn = get_post_meta( get_the_ID(), 'isbn', true ) ) : ?>
          <li><strong>ISBN:</strong> <?= esc_html( $isbn ); ?></li>
        <?php endif; ?>

        <li><strong>Genres:</strong> <?= get_the_term_list( get_the_ID(), 'genre', '', ', ' ); ?></li>
      </ul>

      <div class="content"><?php the_content(); ?></div>
    </article>
  <?php endwhile; ?>
</main>
<?php get_footer(); ?>

Querying Custom Post Types in PHP

Basic query

$books = new WP_Query([
  'post_type'      => 'book',
  'posts_per_page' => 6,
  'orderby'        => 'date',
  'order'          => 'DESC',
]);
if ( $books->have_posts() ) {
  while ( $books->have_posts() ) {
    $books->the_post();
    the_title('<h3>','</h3>');
  }
  wp_reset_postdata();
}

Filter by taxonomy term

$fantasy_books = new WP_Query([
  'post_type' => 'book',
  'tax_query' => [[
    'taxonomy' => 'genre',
    'field'    => 'slug',
    'terms'    => ['fantasy'],
  ]],
]);

Gutenberg & REST Support

Setting 'show_in_rest' => true on both CPT and taxonomy enables the Block Editor and REST API endpoints, making your CPT headless‑friendly and ready for custom blocks, Patterns, and JS apps.

Custom Capabilities (Granular Permissions)

Need role‑based control (e.g., editors can publish books but not pages)? Switch to custom capabilities:

// In register_post_type() args:
'capability_type' => ['book','books'],
'map_meta_cap'    => true;

Then grant caps like edit_book, publish_books, delete_books to roles using a role editor plugin or add_cap().

Improve Admin UX: Custom List Columns

// Add an Author column for Books
add_filter( 'manage_book_posts_columns', function( $cols ) {
    $cols['author_name'] = __('Author', 'mybooks');
    return $cols;
});
add_action( 'manage_book_posts_custom_column', function( $col, $post_id ) {
    if ( 'author_name' === $col ) {
        echo esc_html( get_post_meta( $post_id, 'author_name', true ) ?: '—' );
    }
}, 10, 2 );

Common Pitfalls & Quick Fixes

  • 404 on archive/single: Visit Settings → Permalinks and click Save, or flush rules on activation (as shown).
  • CPT not in Block Editor: Ensure 'show_in_rest' => true and that supports includes needed features.
  • Slug conflicts: Make sure your CPT rewrite['slug'] isn’t used by a page or another CPT.
  • Meta hidden in REST: Use register_post_meta() with show_in_rest => true.

FAQs

What’s the main benefit of a Custom Post Type?

Structure. CPTs separate content into meaningful groups (e.g., Books vs. Posts), each with its own templates, taxonomies, and admin UI, which keeps your site organized and scalable.

Should I register a CPT in a theme or a plugin?

Prefer a plugin. CPTs are content, not presentation. If you switch themes, your CPT remains intact.

Do I need a taxonomy for my CPT?

No, but a taxonomy (like Genre for Books) improves filtering, archives, and navigation. Use hierarchical if it behaves like categories; non‑hierarchical for tag‑like behavior.

How do I expose CPT data to JavaScript apps?

Enable show_in_rest. Then use /wp-json/wp/v2/book endpoints in your frontend (React/Vue/Next.js) to fetch, filter, and display entries.

Why am I getting 404s after registering a CPT?

WordPress needs to regenerate rewrite rules. Visit Settings → Permalinks and click Save, or flush on activation using flush_rewrite_rules() as shown.

Is it safe to use custom SQL with $wpdb?

Yes, for advanced needs just prepare and sanitize queries. Prefer WP_Query for most content retrieval to leverage caching and core APIs.

Conclusion

Custom Post Types are the backbone of structured WordPress sites. With the plugin scaffold above, REST endpoints, meta fields, templates, and admin polish, you’re ready to model any content—from portfolios to events cleanly and reliably.