How to Create Custom Post Types in WordPress

CPTs are the correct way to model content beyond posts/pages (products, events, recipes, portfolios, etc.). Below I’ll give a clean, production-ready recipe: concept + example plugin code you can drop in, best practices, and how to make templates and REST/Gutenberg-ready.

Quick checklist (what “proper” means)

  • Register CPT on init hook.

  • Use register_post_type() with complete labels & args.

  • Support show_in_rest for Gutenberg / REST API.

  • Register related taxonomies (optional).

  • Provide templates: single-{post_type}.php, archive-{post_type}.php.

  • Flush rewrite rules only on activation (not on every page load).

  • Prefer a plugin for CPTs (not theme) if content must persist when theme changes.

  • Secure/sanitize meta, use capabilities if needed, and keep translations-ready.

    <?php
    /*
    Plugin Name: My CPTs – Books
    Description: Registers a ‘book’ custom post type (example) the proper way.
    Version: 1.0
    Author: Your Name
    Text Domain: my-cpts
    */

    if ( ! defined( ‘ABSPATH’ ) ) {
    exit;
    }

    /**
    * Register the ‘book’ custom post type.
    */
    function mycpts_register_book_cpt() {
    $labels = array(
    ‘name’ => _x( ‘Books’, ‘Post type general name’, ‘my-cpts’ ),
    ‘singular_name’ => _x( ‘Book’, ‘Post type singular name’, ‘my-cpts’ ),
    ‘menu_name’ => _x( ‘Books’, ‘Admin Menu text’, ‘my-cpts’ ),
    ‘name_admin_bar’ => _x( ‘Book’, ‘Add New on Toolbar’, ‘my-cpts’ ),
    ‘add_new’ => __( ‘Add New’, ‘my-cpts’ ),
    ‘add_new_item’ => __( ‘Add New Book’, ‘my-cpts’ ),
    ‘new_item’ => __( ‘New Book’, ‘my-cpts’ ),
    ‘edit_item’ => __( ‘Edit Book’, ‘my-cpts’ ),
    ‘view_item’ => __( ‘View Book’, ‘my-cpts’ ),
    ‘all_items’ => __( ‘All Books’, ‘my-cpts’ ),
    ‘search_items’ => __( ‘Search Books’, ‘my-cpts’ ),
    ‘not_found’ => __( ‘No books found.’, ‘my-cpts’ ),
    ‘not_found_in_trash’ => __( ‘No books found in Trash.’, ‘my-cpts’ ),
    ‘archives’ => __( ‘Book archives’, ‘my-cpts’ ),
    ‘attributes’ => __( ‘Book attributes’, ‘my-cpts’ ),
    );

    $args = array(
    ‘labels’ => $labels,
    ‘public’ => true,
    ‘publicly_queryable’ => true,
    ‘show_ui’ => true,
    ‘show_in_menu’ => true,
    ‘menu_position’ => 5,
    ‘menu_icon’ => ‘dashicons-book’,
    ‘supports’ => array( ‘title’, ‘editor’, ‘thumbnail’, ‘excerpt’, ‘custom-fields’ ),
    ‘has_archive’ => true,
    ‘rewrite’ => array( ‘slug’ => ‘books’, ‘with_front’ => false ),
    ‘show_in_rest’ => true, // IMPORTANT: enable Gutenberg + REST API
    ‘rest_base’ => ‘books’,
    ‘capability_type’ => ‘post’,
    ‘hierarchical’ => false,
    ‘exclude_from_search’=> false,
    );

    register_post_type( ‘book’, $args );
    }
    add_action( ‘init’, ‘mycpts_register_book_cpt’ );

    /**
    * Flush rewrite rules on plugin activation to register permalinks cleanly.
    * DO NOT flush on every page load.
    */
    function mycpts_activate() {
    mycpts_register_book_cpt();
    flush_rewrite_rules();
    }
    register_activation_hook( __FILE__, ‘mycpts_activate’ );

    /**
    * Optional: cleanup on deactivation (not required, but commonly done)
    */
    function mycpts_deactivate() {
    // unregister_post_type( ‘book’ ); // WP unregister isn’t persistent here, so just flush.
    flush_rewrite_rules();
    }
    register_deactivation_hook( __FILE__, ‘mycpts_deactivate’ );

    Explanation of key args

    • labels: Accessibility + translation-friendly strings. Use _x() / __() for i18n.

    • public vs show_ui: public affects front-end availability; show_ui controls admin screens.

    • supports: controls editor features (title, editor, thumbnail, revisions, etc.).

    • rewrite: slug controls the URL; with_front decides whether WP front base is prefixed.

    • has_archive: gives /books/ archive page.

    • show_in_rest: set to true to make CPT Gutenberg-friendly and available via WP REST API.

    • rest_base: optional custom REST route.

    • capability_type & advanced capabilities: use when you want custom permissions (e.g., capability_type => array('book','books') plus map_meta_cap).

    • menu_icon: dashicon or custom SVG.

Share on Facebook Share on Twitter