Skip to main content

Course Progress

Loading...

📝 Registering Custom Post Types

Create custom content types with register_post_type()

Master CPT registration, configuration, and best practices

Learning Objectives

  • Register custom post types using register_post_type()
  • Configure CPT labels and arguments
  • Set up URL rewrite rules
  • Configure capabilities and permissions
  • Enable REST API support
  • Set up admin UI options
  • Handle CPT icons and menu position
  • Implement best practices for CPT registration

The register_post_type() Function

The register_post_type() function is used to create custom post types in WordPress. It should be called from the init action hook and requires a post type key and an array of arguments.

💡
Key Concept
Custom Post Types must be registered on every page load. They are not stored in the database but are registered dynamically using PHP code, typically in your theme's functions.php or a custom plugin.

Basic CPT Registration

Simple Custom Post Type

<?php
/**
 * Register a simple custom post type
 */
function mytheme_register_portfolio_cpt() {
    $args = array(
        'public' => true,
        'label'  => 'Portfolio',
        'menu_icon' => 'dashicons-portfolio',
    );
    
    register_post_type( 'portfolio', $args );
}
add_action( 'init', 'mytheme_register_portfolio_cpt' );

Complete CPT Registration with All Options

<?php
/**
 * Register a custom post type with full configuration
 */
function mytheme_register_product_cpt() {
    
    // Labels for the CPT
    $labels = array(
        'name'                  => _x( 'Products', 'Post type general name', 'mytheme' ),
        'singular_name'         => _x( 'Product', 'Post type singular name', 'mytheme' ),
        'menu_name'             => _x( 'Products', 'Admin Menu text', 'mytheme' ),
        'name_admin_bar'        => _x( 'Product', 'Add New on Toolbar', 'mytheme' ),
        'add_new'               => __( 'Add New', 'mytheme' ),
        'add_new_item'          => __( 'Add New Product', 'mytheme' ),
        'new_item'              => __( 'New Product', 'mytheme' ),
        'edit_item'             => __( 'Edit Product', 'mytheme' ),
        'view_item'             => __( 'View Product', 'mytheme' ),
        'all_items'             => __( 'All Products', 'mytheme' ),
        'search_items'          => __( 'Search Products', 'mytheme' ),
        'parent_item_colon'     => __( 'Parent Products:', 'mytheme' ),
        'not_found'             => __( 'No products found.', 'mytheme' ),
        'not_found_in_trash'    => __( 'No products found in Trash.', 'mytheme' ),
        'featured_image'        => _x( 'Product Cover Image', 'Overrides the "Featured Image" phrase', 'mytheme' ),
        'set_featured_image'    => _x( 'Set cover image', 'Overrides the "Set featured image" phrase', 'mytheme' ),
        'remove_featured_image' => _x( 'Remove cover image', 'Overrides the "Remove featured image" phrase', 'mytheme' ),
        'use_featured_image'    => _x( 'Use as cover image', 'Overrides the "Use as featured image" phrase', 'mytheme' ),
        'archives'              => _x( 'Product archives', 'The post type archive label', 'mytheme' ),
        'insert_into_item'      => _x( 'Insert into product', 'Overrides the "Insert into post" phrase', 'mytheme' ),
        'uploaded_to_this_item' => _x( 'Uploaded to this product', 'Overrides the "Uploaded to this post" phrase', 'mytheme' ),
        'filter_items_list'     => _x( 'Filter products list', 'Screen reader text', 'mytheme' ),
        'items_list_navigation' => _x( 'Products list navigation', 'Screen reader text', 'mytheme' ),
        'items_list'            => _x( 'Products list', 'Screen reader text', 'mytheme' ),
    );
    
    // Arguments for the CPT
    $args = array(
        'labels'             => $labels,
        'public'             => true,
        'publicly_queryable' => true,
        'show_ui'            => true,
        'show_in_menu'       => true,
        'query_var'          => true,
        'rewrite'            => array( 'slug' => 'products' ),
        'capability_type'    => 'post',
        'has_archive'        => true,
        'hierarchical'       => false,
        'menu_position'      => 5,
        'menu_icon'          => 'dashicons-cart',
        'supports'           => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' ),
        'show_in_rest'       => true,
        'rest_base'          => 'products',
        'rest_controller_class' => 'WP_REST_Posts_Controller',
    );
    
    register_post_type( 'product', $args );
}
add_action( 'init', 'mytheme_register_product_cpt' );

register_post_type() Arguments

Argument Description Default
label Name of the post type shown in admin $post_type
labels Array of labels for this post type array()
description Short descriptive summary ''
public Whether post type is public false
hierarchical Whether post type is hierarchical false
exclude_from_search Exclude from front-end search opposite of public
publicly_queryable Whether queries can be performed value of public
show_ui Generate default UI in admin value of public
show_in_menu Show in admin menu value of show_ui
show_in_nav_menus Available in navigation menus value of public
show_in_admin_bar Show in admin bar value of show_in_menu
show_in_rest Enable REST API support false
rest_base REST API base slug $post_type
menu_position Position in admin menu null (below Comments)
menu_icon Menu icon URL or dashicon null (posts icon)
capability_type String to build capabilities 'post'
capabilities Array of capabilities capability_type used
map_meta_cap Use default meta capability handling null
supports Core features CPT supports array('title', 'editor')
register_meta_box_cb Callback for meta boxes null
taxonomies Taxonomies to register array()
has_archive Enable post type archives false
rewrite Permalink rewrite rules true
query_var Query var for this post type true - $post_type
can_export Can be exported true
delete_with_user Delete posts when user deleted null

Configuring CPT Labels

Labels customize the text displayed in the WordPress admin for your CPT:

Complete Labels Array

$labels = array(
    'name'                  => _x( 'Books', 'Post type general name', 'mytheme' ),
    'singular_name'         => _x( 'Book', 'Post type singular name', 'mytheme' ),
    'menu_name'             => _x( 'Books', 'Admin Menu text', 'mytheme' ),
    'name_admin_bar'        => _x( 'Book', 'Add New on Toolbar', 'mytheme' ),
    'add_new'               => __( 'Add New', 'mytheme' ),
    'add_new_item'          => __( 'Add New Book', 'mytheme' ),
    'new_item'              => __( 'New Book', 'mytheme' ),
    'edit_item'             => __( 'Edit Book', 'mytheme' ),
    'view_item'             => __( 'View Book', 'mytheme' ),
    'all_items'             => __( 'All Books', 'mytheme' ),
    'search_items'          => __( 'Search Books', 'mytheme' ),
    'parent_item_colon'     => __( 'Parent Books:', 'mytheme' ),
    'not_found'             => __( 'No books found.', 'mytheme' ),
    'not_found_in_trash'    => __( 'No books found in Trash.', 'mytheme' ),
    'featured_image'        => _x( 'Book Cover', 'Featured image', 'mytheme' ),
    'set_featured_image'    => _x( 'Set book cover', 'Set featured image', 'mytheme' ),
    'remove_featured_image' => _x( 'Remove book cover', 'Remove featured image', 'mytheme' ),
    'use_featured_image'    => _x( 'Use as book cover', 'Use as featured image', 'mytheme' ),
    'archives'              => _x( 'Book archives', 'The post type archive', 'mytheme' ),
    'insert_into_item'      => _x( 'Insert into book', 'Insert into post', 'mytheme' ),
    'uploaded_to_this_item' => _x( 'Uploaded to this book', 'Uploaded to post', 'mytheme' ),
    'filter_items_list'     => _x( 'Filter books list', 'Screen reader', 'mytheme' ),
    'items_list_navigation' => _x( 'Books list navigation', 'Screen reader', 'mytheme' ),
    'items_list'            => _x( 'Books list', 'Screen reader', 'mytheme' ),
);

Hierarchical Custom Post Type

Documentation CPT (Hierarchical)

<?php
/**
 * Register hierarchical CPT for documentation
 */
function mytheme_register_docs_cpt() {
    $labels = array(
        'name'          => __( 'Documentation', 'mytheme' ),
        'singular_name' => __( 'Doc', 'mytheme' ),
        'add_new_item'  => __( 'Add New Documentation Page', 'mytheme' ),
        'edit_item'     => __( 'Edit Documentation', 'mytheme' ),
        'parent_item_colon' => __( 'Parent Documentation:', 'mytheme' ),
    );
    
    $args = array(
        'labels'              => $labels,
        'public'              => true,
        'hierarchical'        => true, // Like pages
        'supports'            => array( 'title', 'editor', 'page-attributes', 'revisions' ),
        'has_archive'         => 'docs',
        'rewrite'             => array( 
            'slug' => 'docs',
            'with_front' => false,
            'pages' => true,
            'feeds' => false,
        ),
        'menu_icon'           => 'dashicons-book-alt',
        'show_in_rest'        => true,
    );
    
    register_post_type( 'documentation', $args );
}
add_action( 'init', 'mytheme_register_docs_cpt' );

URL Rewrite Configuration

Custom Rewrite Rules

// Simple rewrite
'rewrite' => array( 'slug' => 'products' ),

// Advanced rewrite options
'rewrite' => array(
    'slug'       => 'shop/products',     // Custom URL base
    'with_front' => false,                // Don't prepend front base
    'feeds'      => true,                 // Enable feeds
    'pages'      => true,                 // Enable pagination
    'ep_mask'    => EP_PERMALINK,        // Endpoint mask
),

// Disable rewrite (use query vars)
'rewrite' => false,

// Custom archive slug
'has_archive' => 'product-catalog',      // Different from post type slug
After changing rewrite rules, you must flush the rewrite rules by visiting Settings → Permalinks in the admin or calling flush_rewrite_rules().

Custom Capabilities

CPT with Custom Capabilities

<?php
/**
 * Register CPT with custom capabilities
 */
function mytheme_register_event_cpt() {
    $labels = array(
        'name'          => __( 'Events', 'mytheme' ),
        'singular_name' => __( 'Event', 'mytheme' ),
    );
    
    $args = array(
        'labels'          => $labels,
        'public'          => true,
        'capability_type' => array( 'event', 'events' ),
        'map_meta_cap'    => true,
        'capabilities'    => array(
            'edit_post'              => 'edit_event',
            'read_post'              => 'read_event',
            'delete_post'            => 'delete_event',
            'edit_posts'             => 'edit_events',
            'edit_others_posts'      => 'edit_others_events',
            'publish_posts'          => 'publish_events',
            'read_private_posts'     => 'read_private_events',
            'delete_posts'           => 'delete_events',
            'delete_private_posts'   => 'delete_private_events',
            'delete_published_posts' => 'delete_published_events',
            'delete_others_posts'    => 'delete_others_events',
            'edit_private_posts'     => 'edit_private_events',
            'edit_published_posts'   => 'edit_published_events',
        ),
        'supports'        => array( 'title', 'editor', 'thumbnail' ),
        'has_archive'     => true,
        'menu_icon'       => 'dashicons-calendar-alt',
    );
    
    register_post_type( 'event', $args );
}
add_action( 'init', 'mytheme_register_event_cpt' );

/**
 * Add capabilities to roles
 */
function mytheme_add_event_caps() {
    $role = get_role( 'administrator' );
    
    $caps = array(
        'edit_event',
        'read_event',
        'delete_event',
        'edit_events',
        'edit_others_events',
        'publish_events',
        'read_private_events',
        'delete_events',
        'delete_private_events',
        'delete_published_events',
        'delete_others_events',
        'edit_private_events',
        'edit_published_events',
    );
    
    foreach ( $caps as $cap ) {
        $role->add_cap( $cap );
    }
}
add_action( 'admin_init', 'mytheme_add_event_caps' );

REST API Configuration

Enable REST API Support

$args = array(
    // ... other args
    'show_in_rest' => true,
    'rest_base' => 'products',
    'rest_controller_class' => 'WP_REST_Posts_Controller',
    'rest_namespace' => 'wp/v2',
);

// After registration, endpoints available at:
// GET    /wp-json/wp/v2/products
// GET    /wp-json/wp/v2/products/{id}
// POST   /wp-json/wp/v2/products
// PUT    /wp-json/wp/v2/products/{id}
// DELETE /wp-json/wp/v2/products/{id}

Registering Multiple CPTs

Organized CPT Registration

<?php
/**
 * Register all custom post types
 */
class MyTheme_Post_Types {
    
    public function __construct() {
        add_action( 'init', array( $this, 'register_post_types' ) );
    }
    
    public function register_post_types() {
        $this->register_portfolio();
        $this->register_testimonial();
        $this->register_service();
        $this->register_team_member();
    }
    
    private function register_portfolio() {
        $labels = array(
            'name'          => __( 'Portfolio', 'mytheme' ),
            'singular_name' => __( 'Portfolio Item', 'mytheme' ),
            'add_new_item'  => __( 'Add New Portfolio Item', 'mytheme' ),
        );
        
        register_post_type( 'portfolio', array(
            'labels'       => $labels,
            'public'       => true,
            'has_archive'  => true,
            'menu_icon'    => 'dashicons-portfolio',
            'supports'     => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
            'show_in_rest' => true,
        ) );
    }
    
    private function register_testimonial() {
        $labels = array(
            'name'          => __( 'Testimonials', 'mytheme' ),
            'singular_name' => __( 'Testimonial', 'mytheme' ),
        );
        
        register_post_type( 'testimonial', array(
            'labels'              => $labels,
            'public'              => true,
            'exclude_from_search' => true,
            'publicly_queryable'  => false,
            'show_in_nav_menus'   => false,
            'menu_icon'           => 'dashicons-format-quote',
            'supports'            => array( 'title', 'editor', 'thumbnail' ),
        ) );
    }
    
    private function register_service() {
        $labels = array(
            'name'          => __( 'Services', 'mytheme' ),
            'singular_name' => __( 'Service', 'mytheme' ),
        );
        
        register_post_type( 'service', array(
            'labels'       => $labels,
            'public'       => true,
            'has_archive'  => 'our-services',
            'rewrite'      => array( 'slug' => 'service' ),
            'menu_icon'    => 'dashicons-hammer',
            'supports'     => array( 'title', 'editor', 'thumbnail', 'page-attributes' ),
            'hierarchical' => true,
            'show_in_rest' => true,
        ) );
    }
    
    private function register_team_member() {
        $labels = array(
            'name'          => __( 'Team', 'mytheme' ),
            'singular_name' => __( 'Team Member', 'mytheme' ),
        );
        
        register_post_type( 'team_member', array(
            'labels'       => $labels,
            'public'       => true,
            'has_archive'  => 'team',
            'rewrite'      => array( 'slug' => 'team' ),
            'menu_icon'    => 'dashicons-groups',
            'supports'     => array( 'title', 'editor', 'thumbnail' ),
            'show_in_rest' => true,
        ) );
    }
}

// Initialize the class
new MyTheme_Post_Types();

Menu Icons and Position

Menu Icon Options

// Using Dashicons (built-in WordPress icons)
'menu_icon' => 'dashicons-cart',
'menu_icon' => 'dashicons-portfolio',
'menu_icon' => 'dashicons-calendar',
'menu_icon' => 'dashicons-groups',
'menu_icon' => 'dashicons-building',
'menu_icon' => 'dashicons-book',

// Using custom image
'menu_icon' => get_template_directory_uri() . '/images/custom-icon.png',

// Using SVG (base64 encoded)
'menu_icon' => 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiPjwvc3ZnPg==',

// Menu position values
'menu_position' => 5,   // Below Posts
'menu_position' => 10,  // Below Media
'menu_position' => 15,  // Below Links
'menu_position' => 20,  // Below Pages
'menu_position' => 25,  // Below Comments
'menu_position' => 60,  // Below first separator
'menu_position' => 65,  // Below Plugins
'menu_position' => 70,  // Below Users
'menu_position' => 75,  // Below Tools
'menu_position' => 80,  // Below Settings
'menu_position' => 100, // Below second separator

Best Practices

CPT Registration Best Practices

  • Use init hook: Always register CPTs on the init action
  • Prefix post type names: Avoid conflicts with plugins
  • Keep names under 20 characters: Database limitation
  • Use lowercase and underscores: No spaces or capitals
  • Make labels translatable: Use __() and _x() functions
  • Enable REST API: For Gutenberg and modern features
  • Set appropriate capabilities: Control user access properly
  • Flush rewrite rules: After adding or modifying CPTs
Create a custom plugin for CPTs instead of adding them to your theme. This way, content remains accessible even when switching themes.

Common Registration Patterns

CPT with Custom Taxonomies

<?php
function mytheme_register_product_with_taxonomies() {
    // Register CPT
    register_post_type( 'product', array(
        'labels' => array(
            'name' => __( 'Products', 'mytheme' ),
            'singular_name' => __( 'Product', 'mytheme' ),
        ),
        'public' => true,
        'has_archive' => true,
        'taxonomies' => array( 'product_category', 'product_tag' ),
        'supports' => array( 'title', 'editor', 'thumbnail' ),
        'show_in_rest' => true,
    ) );
    
    // Register custom taxonomy
    register_taxonomy( 'product_category', 'product', array(
        'labels' => array(
            'name' => __( 'Product Categories', 'mytheme' ),
            'singular_name' => __( 'Product Category', 'mytheme' ),
        ),
        'hierarchical' => true,
        'show_in_rest' => true,
    ) );
    
    register_taxonomy( 'product_tag', 'product', array(
        'labels' => array(
            'name' => __( 'Product Tags', 'mytheme' ),
            'singular_name' => __( 'Product Tag', 'mytheme' ),
        ),
        'hierarchical' => false,
        'show_in_rest' => true,
    ) );
}
add_action( 'init', 'mytheme_register_product_with_taxonomies' );

Practice Exercise

💻
Register Custom Post Types

Create a complete CPT registration system:

  1. Register a "Portfolio" CPT with full labels
  2. Add support for title, editor, thumbnail, excerpt
  3. Enable REST API support
  4. Set custom rewrite rules
  5. Add a custom menu icon
  6. Register a "Service" hierarchical CPT
  7. Create a "Testimonial" CPT without archives
  8. Implement custom capabilities for one CPT
  9. Organize code in a class structure
  10. Test all CPTs in WordPress admin

Additional Resources