WordPress Custom Post Types - A Developer's Guide
Custom post types extend WordPress beyond posts and pages. How to register them correctly, add custom fields, and query them efficiently.
Custom post types (CPTs) are one of WordPress's most powerful features and one of the most misunderstood. They let you create structured content types - team members, case studies, testimonials, events, products - beyond the default posts and pages.
This guide covers registering custom post types correctly, adding custom fields, setting up custom taxonomies, and querying CPTs efficiently.
What custom post types are (and are not)
A custom post type is a content type that shares WordPress's post infrastructure (the wp_posts table, the admin editing interface, the revision system) but is separate from standard posts.
WordPress itself uses this system internally: pages, attachments, and revisions are all post types. WooCommerce products are a custom post type.
Use CPTs when:
- Content has structured, repeatable fields (team members with name, job title, photo, bio)
- Content needs its own admin section separate from blog posts
- Content is displayed in templates with custom layouts
- Content should be queryable independently (portfolio items, events by date range)
Do not use CPTs when:
- Simple categorisation handles the difference (a "tutorial" category for posts is fine)
- The content is only displayed once (an about page is a page, not a CPT)
- The content structure is so simple it adds no value over posts
Registering a custom post type
Use register_post_type() in a plugin or in your theme's functions.php. Plugin is better for CPTs because if you switch themes, the content type should not disappear.
add_action( 'init', function() {
register_post_type( 'case_study', [
'labels' => [
'name' => 'Case Studies',
'singular_name' => 'Case Study',
'menu_name' => 'Case Studies',
'add_new' => 'Add Case Study',
'add_new_item' => 'Add New Case Study',
'edit_item' => 'Edit Case Study',
'new_item' => 'New Case Study',
'view_item' => 'View Case Study',
'search_items' => 'Search Case Studies',
'not_found' => 'No case studies found',
'not_found_in_trash' => 'No case studies in trash',
],
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_rest' => true, // enables block editor
'menu_position' => 5,
'menu_icon' => 'dashicons-portfolio',
'supports' => [ 'title', 'editor', 'thumbnail', 'excerpt' ],
'has_archive' => true, // enables /case-studies/ archive
'rewrite' => [ 'slug' => 'case-studies' ],
'capability_type' => 'post',
]);
});
Key arguments explained
public: Makes the post type visible to users and searchable. Set to false for internal post types that should not have front-end URLs.
show_in_rest: Must be true to use the Gutenberg block editor with this post type. Also required for REST API access.
supports: Which features the post type supports. Options include title, editor, thumbnail, excerpt, revisions, custom-fields, comments, author.
has_archive: If true, enables a /case-studies/ archive page that lists all case studies. WordPress must have the archive template (archive-case_study.php) in the theme.
rewrite: The slug used in URLs. /case-studies/my-case-study/.
menu_icon: Dashicons class for the admin menu icon. Browse all options at developer.wordpress.org/resource/dashicons/.
Flush rewrite rules after registration
After registering a CPT, WordPress needs to flush its rewrite rules to generate the correct URL structure. This happens automatically on activation (if your CPT is in a plugin) or you can flush manually:
// In plugin activation hook
register_activation_hook( __FILE__, function() {
// Register CPT first
my_register_case_studies_cpt();
flush_rewrite_rules();
});
Or visit Settings > Permalinks and click Save to flush manually.
Adding custom taxonomies
Custom taxonomies are category/tag systems for your custom post types. A case study might have an "Industry" taxonomy.
add_action( 'init', function() {
register_taxonomy( 'industry', 'case_study', [
'labels' => [
'name' => 'Industries',
'singular_name' => 'Industry',
'menu_name' => 'Industries',
'all_items' => 'All Industries',
'edit_item' => 'Edit Industry',
'add_new_item' => 'Add New Industry',
],
'hierarchical' => true, // true = categories, false = tags
'public' => true,
'show_ui' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'industry' ],
]);
});
Now you can filter case studies by industry at /industry/financial-services/.
Adding custom fields with ACF
The block editor handles text content. For structured data (job title, phone number, project URL, client logo), use ACF (Advanced Custom Fields).
Setting up an ACF field group
- Install ACF or ACF Pro
- ACF > Field Groups > Add New
- Name the group ("Case Study Details")
- Add fields: Text (client name), Image (client logo), URL (project URL), Textarea (results summary)
- Set the Location rule: "Post Type is equal to Case Study"
- Publish
These fields now appear in the case study editing screen below the content editor.
Outputting ACF fields in templates
In your single-case_study.php template:
<?php
$client_name = get_field( 'client_name' );
$client_logo = get_field( 'client_logo' ); // returns attachment array
$project_url = get_field( 'project_url' );
$results = get_field( 'results_summary' );
?>
<div class="case-study-header">
<?php if ( $client_logo ): ?>
<img src="<?php echo esc_url( $client_logo['url'] ); ?>"
alt="<?php echo esc_attr( $client_logo['alt'] ); ?>"
width="200" />
<?php endif; ?>
<h1><?php echo esc_html( $client_name ); ?></h1>
</div>
Querying custom post types
Use WP_Query to fetch CPT content:
$case_studies = new WP_Query([
'post_type' => 'case_study',
'posts_per_page' => 6,
'post_status' => 'publish',
'orderby' => 'date',
'order' => 'DESC',
]);
if ( $case_studies->have_posts() ) {
while ( $case_studies->have_posts() ) {
$case_studies->the_post();
// Template content
get_template_part( 'template-parts/case-study-card' );
}
wp_reset_postdata();
}
Filter by taxonomy term
$financial_case_studies = new WP_Query([
'post_type' => 'case_study',
'tax_query' => [
[
'taxonomy' => 'industry',
'field' => 'slug',
'terms' => 'financial-services',
],
],
]);
Filter by custom field value
$recent_large_projects = new WP_Query([
'post_type' => 'case_study',
'meta_query' => [
[
'key' => 'project_value',
'value' => 50000,
'compare' => '>=',
'type' => 'NUMERIC',
],
],
]);
Performance note: Queries on wp_postmeta (custom fields) are slower than queries on wp_posts columns. For high-traffic sites with large datasets, avoid meta_query for filtering on public-facing pages. Consider denormalising data into custom tables for frequently queried custom fields.
Custom templates for CPTs
WordPress template hierarchy for a CPT named case_study:
single-case_study.php- single case study viewarchive-case_study.php- list of all case studiestaxonomy-industry.php- list filtered by industry taxonomytaxonomy-industry-financial-services.php- specific term archive
Place these in your theme (or child theme) folder. WordPress automatically uses them when visiting the appropriate URL.
REST API access for CPTs
With 'show_in_rest' => true, your CPT is accessible via:
GET /wp-json/wp/v2/case_study
GET /wp-json/wp/v2/case_study/{id}
The REST API endpoint name is the post type key (case_study). To customise it:
'rest_base' => 'case-studies',
This changes the endpoint to /wp-json/wp/v2/case-studies/.
ACF fields are not included in the REST API by default. To include them, use ACF's REST API integration:
// In ACF field group settings, enable "Show in REST API" // Or programmatically: add_filter( 'acf/rest/get_fields', '__return_true' );
Related reading
// new_articles
Get notified when new guides drop
Practical WordPress guides from a working agency owner. No filler. Unsubscribe any time.
Was this article helpful?
Thanks for the feedback!