📝 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
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