Custom Post Types
Custom Post Types let you model entities beyond posts and pages — products, events, team members, properties. Register one with register_post_type and the WP admin gets a full CRUD UI for free.
Register, taxonomies, meta, REST
EXAMPLE
<?php
// 1) Register a CPT
add_action('init', function () {
register_post_type('product', [
'label' => 'Products',
'public' => true,
'menu_position' => 5,
'menu_icon' => 'dashicons-cart',
'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'],
'has_archive' => true,
'rewrite' => ['slug' => 'products', 'with_front' => false],
'show_in_rest' => true, // Gutenberg + REST API
'capability_type'=> 'post',
'labels' => [
'name' => 'Products',
'singular_name' => 'Product',
'add_new_item' => 'Add New Product',
'edit_item' => 'Edit Product',
'view_item' => 'View Product',
],
]);
// 2) Custom taxonomy — like categories, but for your CPT
register_taxonomy('product_category', 'product', [
'hierarchical' => true, // like categories (true) or tags (false)
'label' => 'Product Categories',
'rewrite' => ['slug' => 'product-cat'],
'show_in_rest' => true,
]);
register_taxonomy('product_tag', 'product', [
'hierarchical' => false,
'label' => 'Product Tags',
'show_in_rest' => true,
]);
});
// 3) Flush rewrite rules — register at activation, flush there
register_activation_hook(__FILE__, function () {
flush_rewrite_rules();
});
register_deactivation_hook(__FILE__, 'flush_rewrite_rules');
// 4) Add meta fields (price, SKU)
add_action('init', function () {
register_post_meta('product', 'price', [
'type' => 'number',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'floatval',
'auth_callback' => fn() => current_user_can('edit_posts'),
]);
register_post_meta('product', 'sku', [
'type' => 'string',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'sanitize_text_field',
]);
});
// 5) Query CPTs
$query = new WP_Query([
'post_type' => 'product',
'posts_per_page' => 12,
'meta_query' => [
['key' => 'price', 'value' => 100, 'compare' => '>=', 'type' => 'NUMERIC'],
],
'tax_query' => [
['taxonomy' => 'product_category', 'field' => 'slug', 'terms' => 'gadgets'],
],
]);
while ($query->have_posts()) {
$query->the_post();
$price = get_post_meta(get_the_ID(), 'price', true);
the_title('<h2>', '</h2>');
echo '<p class="price">$' . number_format((float) $price, 2) . '</p>';
}
wp_reset_postdata();
// 6) Single-product template — single-product.php
// Theme falls back: single-product.php → single.php → index.php
// 7) Add a custom column to the admin list
add_filter('manage_product_posts_columns', function ($cols) {
$cols['price'] = 'Price';
$cols['sku'] = 'SKU';
return $cols;
});
add_action('manage_product_posts_custom_column', function ($col, $post_id) {
if ($col === 'price') echo '$' . number_format((float) get_post_meta($post_id, 'price', true), 2);
if ($col === 'sku') echo esc_html(get_post_meta($post_id, 'sku', true));
}, 10, 2);
// 8) REST API auto-exposed when show_in_rest = true
// GET /wp-json/wp/v2/product/?per_page=20
// GET /wp-json/wp/v2/product/123
// POST /wp-json/wp/v2/product (with auth)
// 9) Best practices
// • Always pass show_in_rest => true unless you have a strong reason not to
// • Use a unique slug (longer / namespaced) to avoid plugin conflicts: 'mycorp_product'
// • Use register_post_meta with auth_callback to gate edits
// • Don't register CPTs inside a theme — move into a plugin (CPTs survive theme switches)
Why it matters
Custom Post Types + show_in_rest give you a typed admin UI and a REST API for free. Build the model once; theme renders + headless apps + Gutenberg blocks all read from the same source.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
register_post_type('book', [
'public' => true,
'label' => 'Books',
'supports' => ['title', 'editor', 'thumbnail'],
]);
Try it Yourself »
Exercise
Register a CPT with…
('product', $args);
register_post_type.
Discussion
Loading…