A WordPress custom post type (CPT) is a content type beyond the five built in types (post, page, attachment, revision, nav_menu_item). CPTs let you store structured content like Products, Events, Properties, Recipes, Testimonials or Portfolio items, each with its own admin menu, edit screen, taxonomies, URL structure and template hierarchy. Custom post types were introduced in WordPress 2.9 (December 2009) and became the standard approach to structured content with the register_post_type() function in WordPress 3.0 (June 2010). A CPT is registered in PHP via register_post_type( $slug, $args ) on the init hook, or visually via plugins like Custom Post Type UI (700,000 plus active installs), Pods (100,000 plus) and Meta Box (400,000 plus). Since WordPress 5.0 (December 2018) CPTs can opt into the block editor by setting show_in_rest to true, which also exposes them on the REST API at /wp-json/wp/v2/<slug>. The five default types remain: post, page, attachment (media), revision and nav_menu_item. WooCommerce alone registers the product CPT plus shop_order, shop_coupon, product_variation and ten more. The Events Calendar uses tribe_events. ACF Pro can register CPTs through its UI since version 6.1 (March 2023). Custom post types remain the backbone of any non blog WordPress site in 2025.
Why use a custom post type?
Use a CPT when content does not belong with blog posts or static pages. Typical cases:
- E commerce products: WooCommerce registers product, shop_order, shop_coupon as CPTs. Each has price, stock, SKU and variations.
- Events: Modern Tribe Events Calendar uses the tribe_events CPT with start date, end date, venue and organizer taxonomies.
- Real estate: a Property CPT with bedrooms, bathrooms, price and a Location taxonomy.
- Portfolio: a Project CPT with client, year, technology stack and a featured image.
- Testimonials, team members, FAQs: small CPTs without single page templates, often shown via shortcode or block.
- Recipes: title, ingredients, instructions, prep time, cook time and yield, often paired with Schema.org Recipe markup.
- Job listings: a Job CPT with location, department, salary range and an apply form.
Pages or posts could technically hold any of this, but a CPT gives a separate admin menu, custom columns in the list table, dedicated capabilities for editor roles and a logical URL structure (/products/ instead of /blog/<product>).
How to register a custom post type
Place this in a plugin file (preferred) or in functions.php of the theme:
add_action( 'init', 'mysite_register_product_cpt' );
function mysite_register_product_cpt() {
$labels = array(
'name' => 'Products',
'singular_name' => 'Product',
'menu_name' => 'Products',
'add_new' => 'Add new',
'add_new_item' => 'Add new product',
'edit_item' => 'Edit product',
'all_items' => 'All products',
'search_items' => 'Search products',
);
$args = array(
'labels' => $labels,
'public' => true,
'show_in_rest' => true, // block editor and REST API
'has_archive' => true, // /products/ archive
'rewrite' => array( 'slug' => 'products', 'with_front' => false ),
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'custom-fields' ),
'menu_icon' => 'dashicons-cart',
'menu_position' => 20,
'capability_type' => 'post',
'show_in_nav_menus' => true,
);
register_post_type( 'product', $args );
}Important arguments
| Argument | What it does |
|---|---|
| public | Sets show_ui, show_in_nav_menus, publicly_queryable and exclude_from_search defaults. |
| show_in_rest | Required for the block editor and the REST API endpoint /wp-json/wp/v2/<slug>. |
| has_archive | Generates an archive at /<slug>/ rendered by archive-<slug>.php. |
| supports | Which meta boxes/features are enabled: title, editor, thumbnail, excerpt, comments, custom-fields, revisions, page-attributes. |
| capability_type | Default capability mapping (post or page). Use map_meta_cap and custom capabilities for finer control. |
| rewrite | URL slug and rewrite behavior. Always flush rewrite rules on plugin activation. |
| taxonomies | Attach existing taxonomies (category, post_tag) on registration. |
| menu_icon | Dashicon name or SVG data URI for the admin menu. |
Plugin vs theme registration
Always register CPTs in a plugin, not the theme. If you put register_post_type() in functions.php and the user switches themes, the CPT disappears from the admin menu and content becomes inaccessible (the database rows still exist with the old post_type value, but you cannot edit them through the admin). The community calls this plugin territory vs theme territory: content structure belongs to plugins, presentation belongs to themes.
Template hierarchy for CPTs
WordPress looks for these template files for a CPT slug like product:
- Single post: single-product-<slug>.php, single-product.php, single.php, singular.php, index.php.
- Archive: archive-product.php, archive.php, index.php.
- Taxonomy archive: taxonomy-<tax>-<term>.php, taxonomy-<tax>.php, taxonomy.php, archive.php.
Block themes use HTML templates in templates/single-product.html and templates/archive-product.html.
CPTs and the REST API
When show_in_rest => true is set, the CPT is exposed at /wp-json/wp/v2/<rest_base>. The default rest_base is the post type slug. Headless frontends consume this endpoint. You can also expose custom meta fields by registering them with register_post_meta() and show_in_rest => true. Without REST exposure the block editor falls back to the classic editor.
CPTs and the block editor
For a CPT to use the block editor (Gutenberg), it must declare show_in_rest => true and have supports include editor. Without REST it falls back to the classic editor. Optionally use template in the args to lock a default block layout on new posts and template_lock to prevent users from removing or moving blocks.
Plugins that register CPTs without code
- Custom Post Type UI (CPT UI): 700,000 plus active installs. Visual editor for CPTs and taxonomies. Free.
- ACF Pro: since version 6.1 (March 2023) registers CPTs and taxonomies in its own UI alongside field groups. 2 million plus sites use ACF.
- Pods Framework: 100,000 plus installs. Full content modeling with CPTs, custom taxonomies and custom tables.
- Meta Box: CPT registration plus advanced fields and relationships.
- Toolset Types: commercial, popular for CPTs plus front end forms.
Performance and scaling
- All CPT content is stored in the same
wp_poststable as posts and pages. Performance is identical until tables grow past ~100,000 rows where indexing onpost_typematters. - Avoid registering more than 20 to 30 CPTs on one site. Each adds an admin menu, rewrite rules and capability checks.
- Flush rewrite rules on plugin activation with
flush_rewrite_rules(), but never call it on every request (it is expensive). - If a CPT has hundreds of thousands of entries, consider a custom table with
$wpdbinstead. Examples: WooCommerce switched orders from posts to a custom table (HPOS) in WooCommerce 8.0 (August 2023).
Common pitfalls
- Registering on the wrong hook. Always use
init, notplugins_loadedorafter_setup_theme. - Forgetting
show_in_rest. Without it, the CPT does not appear in the block editor or REST API. - Not flushing rewrite rules. Visiting
/products/returns 404 until rules are flushed. - Putting CPT registration in the theme. Switching themes breaks the CPT.
- Slug collisions with pages. A CPT with slug
productsconflicts with a page named Products at the same URL. - Setting
capability_typeto a custom value without registering matching capabilities. Editors then cannot edit the CPT.
What InspectWP checks
InspectWP detects custom post types in the rendered HTML and crawls REST API endpoints when they are exposed. The report identifies popular CPT providers (WooCommerce, Events Calendar, ACF) and flags publicly accessible CPT archives.