WordPress Custom Post Types — register_post_type Complete Developer Guide
In this tutorial, you'll learn to create custom post types in WordPress using register_post_type() — from labels and capabilities to meta boxes, rewrite rules, and custom archive templates.
What You'll Learn
- What custom post types are and why they exist beyond posts and pages
- The complete register_post_type() function with every parameter explained
- How the labels array controls what users see in the admin
- How supported features determine what each CPT offers
- How to register CPTs with REST API support for Gutenberg and headless use
- How to flush rewrite rules so your URLs work
- How to create archive-single-{posttype}.php and single-{posttype}.php templates
- How to add custom meta boxes with add_meta_box() and save_post
- How CPT UI lets you register CPTs without PHP
- Best practices for naming, pluralization, and when to use CPT vs page
Why It Matters
Standard posts and pages are not enough for real-world sites. A bookstore needs a "Books" content type with ISBN, author, and price fields. A movie review site needs "Movies" with release year, director, and rating. A portfolio needs "Projects" with client name, completion date, and live URL. WordPress gives you these two content types by default, but you can create unlimited custom post types that have their own admin menus, their own database storage, their own templates, and their own REST API endpoints. Without CPTs, you would either cram everything into posts with category hacks or abandon WordPress entirely.
Real-World Use
A real estate agency needs three content types: "Properties" (listings with price, location, bedrooms), "Testimonials" (client reviews), and "Agents" (staff profiles). Each has different fields, different admin screens, and different front-end templates. With custom post types, the agency creates three CPTs. Properties appear under a "Properties" menu with custom meta boxes for price and bedrooms. Agents appear under a separate "Agents" menu. Each has its own archive page (/properties/, /agents/) and single page. The API exposes all three for a mobile app.
Learning Path
flowchart LR
A[Theme Anatomy] --> B[Template Hierarchy]
B --> C["Custom Post Types
You are here"]:::current
C --> D[Custom Taxonomies]
D --> E[WP_Query]
E --> F[Hooks]
F --> G[Shortcodes]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
What Is a Custom Post Type?
A post type in WordPress is a content container. WordPress ships with five built-in post types:
- Post — blog entries (hierarchical: no)
- Page — static content (hierarchical: yes)
- Attachment — media files
- Revision — saved drafts of posts/pages
- Navigation Menu Item — menu entries
A custom post type (CPT) is any post type you register yourself. Think of CPTs as creating a new filing cabinet in your WordPress office. Posts and pages are the two default cabinets. When you register a CPT, you add a third cabinet — say "Books" — with its own drawers (categories), its own labels, and its own set of allowed features.
The register_post_type() Function
The heart of CPT creation is register_post_type(). You call it inside a function hooked to the init action:
function dodatech_register_book_post_type() {
$args = array(
'labels' => array(
'name' => 'Books',
'singular_name' => 'Book',
),
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'books' ),
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'menu_icon' => 'dashicons-book',
);
register_post_type( 'book', $args );
}
add_action( 'init', 'dodatech_register_book_post_type' );
Parameter Breakdown
| Parameter | Type | Description |
|---|---|---|
labels |
array | Human-readable names shown in the admin |
public |
bool | Whether the CPT is visible in admin and front-end |
has_archive |
bool | Whether to generate an archive page |
rewrite |
array/boolean | URL slug configuration |
supports |
array | Which meta boxes appear in the editor |
menu_icon |
string | Dashicon or custom icon URL |
show_in_rest |
bool | Enable Gutenberg and REST API support |
menu_position |
int | Position in the admin menu |
capability_type |
string | Base capability (default: 'post') |
hierarchical |
bool | Whether it supports parent/child |
The Labels Array
Labels control every text string WordPress shows for your CPT in the admin. If you skip labels, WordPress uses defaults, which will show "Posts" and "Post" everywhere. Always provide labels:
'labels' => array(
'name' => 'Books',
'singular_name' => 'Book',
'add_new' => 'Add New',
'add_new_item' => 'Add New Book',
'edit_item' => 'Edit Book',
'new_item' => 'New Book',
'view_item' => 'View Book',
'search_items' => 'Search Books',
'not_found' => 'No books found',
'not_found_in_trash' => 'No books found in Trash',
'all_items' => 'All Books',
'menu_name' => 'Books',
'name_admin_bar' => 'Book',
),
Think of the labels array as naming the filing cabinet and all its drawers. If your CPT is "Movie", you want "Add New Movie" not "Add New Post". Each label replaces a generic WordPress string with your custom one.
Supported Features
The supports parameter controls which meta boxes appear in the editor:
'supports' => array(
'title', // Title field
'editor', // Content editor (WYSIWYG)
'thumbnail', // Featured image
'excerpt', // Excerpt field
'trackbacks', // Trackbacks and pingbacks
'custom-fields', // Custom fields meta box
'comments', // Comments section
'revisions', // Revision history
'author', // Author selector
'page-attributes', // Parent and order (requires hierarchical: true)
'post-formats', // Post formats (if theme supports them)
),
You might wonder why you would remove features. Suppose you register a "Testimonial" CPT. Testimonials are short quotes — they do not need a content editor, featured image, or comments. You set 'supports' => array( 'title' ) to keep it simple. Each CPT should support only what it genuinely needs.
show_in_rest for Gutenberg and API
Set 'show_in_rest' => true to make your CPT editable with Gutenberg and accessible via the API:
$args = array(
'public' => true,
'show_in_rest' => true,
'rest_base' => 'books',
'supports' => array( 'title', 'editor', 'thumbnail' ),
);
The rest_base parameter controls the URL path for the REST endpoint: /wp-json/wp/v2/books/. Without it, the endpoint uses the CPT slug.
Flushing Rewrite Rules
After registering a CPT, your archive and single URLs will return 404 errors until you flush rewrite rules. You have two options:
- Permanent fix — Visit Settings > Permalinks and click "Save Changes". This flushes rules without changing anything.
- Programmatic fix — Call
flush_rewrite_rules()but only on theme activation, not on every page load:
function dodatech_rewrite_flush() {
dodatech_register_book_post_type();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'dodatech_rewrite_flush' );
Creating Archive Templates
WordPress uses the template hierarchy to find the right template file. For a CPT named book:
archive-book.php— archive listing all bookssingle-book.php— single book view
archive-book.php
<?php
get_header();
if ( have_posts() ) : ?>
<header class="archive-header">
<h1>Books</h1>
<?php
$total = wp_count_posts( 'book' )->publish;
echo '<p>' . $total . ' books in our collection.</p>';
?>
</header>
<div class="books-grid">
<?php while ( have_posts() ) : the_post(); ?>
<article class="book-card">
<?php the_post_thumbnail( 'medium' ); ?>
<h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<?php the_excerpt(); ?>
<a href="<?php the_permalink(); ?>" class="read-more">View Book</a>
</article>
<?php endwhile; ?>
</div>
<?php the_posts_pagination(); ?>
<?php else : ?>
<p>No books found.</p>
<?php endif;
get_footer();
single-book.php
<?php
get_header();
while ( have_posts() ) : the_post(); ?>
<article class="single-book">
<h1><?php the_title(); ?></h1>
<?php the_post_thumbnail( 'large' ); ?>
<div class="book-meta">
<?php
$isbn = get_post_meta( get_the_ID(), 'book_isbn', true );
$price = get_post_meta( get_the_ID(), 'book_price', true );
if ( $isbn ) echo '<p>ISBN: ' . esc_html( $isbn ) . '</p>';
if ( $price ) echo '<p>Price: ' . esc_html( $price ) . '</p>';
?>
</div>
<div class="book-content">
<?php the_content(); ?>
</div>
</article>
<?php endwhile;
get_footer();
Custom Meta Boxes
Meta boxes are additional fields that appear in the editor. For a Book CPT you might want ISBN, Author, and Price fields. Create them with add_meta_box():
function dodatech_add_book_meta_boxes() {
add_meta_box(
'book_details',
'Book Details',
'dodatech_book_details_callback',
'book',
'normal',
'high'
);
}
add_action( 'add_meta_boxes', 'dodatech_add_book_meta_boxes' );
function dodatech_book_details_callback( $post ) {
wp_nonce_field( 'book_details_nonce', 'book_details_nonce_field' );
$isbn = get_post_meta( $post->ID, 'book_isbn', true );
$price = get_post_meta( $post->ID, 'book_price', true );
?>
<p>
<label for="book_isbn">ISBN:</label>
<input type="text" id="book_isbn" name="book_isbn"
value="<?php echo esc_attr( $isbn ); ?>" class="widefat">
</p>
<p>
<label for="book_price">Price:</label>
<input type="text" id="book_price" name="book_price"
value="<?php echo esc_attr( $price ); ?>" class="widefat">
</p>
<?php
}
function dodatech_save_book_meta( $post_id ) {
if ( ! isset( $_POST['book_details_nonce_field'] ) ) return;
if ( ! wp_verify_nonce( $_POST['book_details_nonce_field'], 'book_details_nonce' ) ) return;
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
if ( ! current_user_can( 'edit_post', $post_id ) ) return;
if ( isset( $_POST['book_isbn'] ) ) {
update_post_meta( $post_id, 'book_isbn', sanitize_text_field( $_POST['book_isbn'] ) );
}
if ( isset( $_POST['book_price'] ) ) {
update_post_meta( $post_id, 'book_price', sanitize_text_field( $_POST['book_price'] ) );
}
}
add_action( 'save_post', 'dodatech_save_book_meta' );
CPT with Custom Taxonomies
Custom post types become far more powerful when paired with custom taxonomies. A book CPT might have a "Genre" taxonomy (hierarchical, like categories) and a "Publisher" taxonomy (non-hierarchical, like tags):
function dodatech_register_book_taxonomies() {
register_taxonomy(
'genre',
'book',
array(
'label' => 'Genres',
'hierarchical' => true,
'rewrite' => array( 'slug' => 'genre' ),
'show_in_rest' => true,
)
);
register_taxonomy(
'publisher',
'book',
array(
'label' => 'Publishers',
'hierarchical' => false,
'rewrite' => array( 'slug' => 'publisher' ),
'show_in_rest' => true,
'show_admin_column' => true,
)
);
}
add_action( 'init', 'dodatech_register_book_taxonomies' );
CPT UI Plugin
If you prefer not to write PHP, the Custom Post Type UI plugin lets you register CPTs and taxonomies through an admin interface. It generates the same register_post_type() and register_taxonomy() calls internally, but you manage everything through a visual form. Many developers use CPT UI for rapid prototyping and then export the code to their theme's functions.php for production.
Best Practices
Singular for internal name, plural for labels. Register with
'book'not'books'. The internal name should be singular. WordPress appends 's' for the REST base if not specified.Use a unique prefix. Your CPT name
bookcould conflict with another plugin. Use a prefix likedodatech_bookordt_book. This prevents collision.Choose manually vs registered via plugin. CPT UI is great for non-developers. For developer-controlled sites, register in code so the CPT travels with the theme or plugin and does not disappear if the plugin is deactivated.
Flush rewrite rules only when needed. Never call
flush_rewrite_rules()oninit. It is an expensive database operation. Use it on theme activation or after registering a new CPT during development.Match supports to purpose. A "Quote" CPT needs only a title and a textarea. An "Employee" CPT might need title, editor, thumbnail, and custom fields. Do not enable every feature for every CPT.
Common Mistakes
Registering a CPT with a reserved name. WordPress reserves names like
post,page,attachment,revision,nav_menu_item, andwp_blocks. Using these causes fatal errors. Always check the reserved list in the WordPress Codex.Calling register_post_type() before init. The
initaction fires after WordPress core loads. Registering a CPT beforeinitmeans the post type won't be recognized by WordPress core functions. Always hook toinit.Forgetting to flush rewrite rules. After adding a CPT, the single and archive URLs return 404 errors. Visit Settings > Permalinks and click Save to flush rules. Your URLs work immediately after.
Setting public to false and wondering why nothing appears. If
publicisfalse, the CPT is hidden from admin menus and not queryable on the front end. Only set it false for internal data (like a logging CPT).Not sanitizing meta box input.
update_post_metastores whatever you pass it. If a user enters<script>in the ISBN field and you display it without escaping, you create an XSS vulnerability. Always usesanitize_text_field(),esc_html(), and nonce verification.
Practice Questions
- What is the purpose of the
supportsparameter in register_post_type() and what happens if you omit it entirely? - Why must register_post_type() be called on the
initaction and not earlier? - A custom post type shows in the admin menu but returns 404 on the front end. What is the most likely cause?
Challenge: Register a "Project" CPT for a portfolio site. Add custom meta boxes for "Client Name", "Project URL", and "Completion Date". Create archive-Project.php and single-project.php templates. Register a "Project Category" hierarchical taxonomy attached to it. Do not use any plugins.
FAQ
Mini Project
Build a "Movie Review" CPT system:
- Register a
movieCPT with title, editor, thumbnail, excerpt, and custom-fields support - Add custom meta boxes for: Release Year, Director, Rating (1-10), and Runtime (minutes)
- Register a
genre(hierarchical) taxonomy and aactor(non-hierarchical) taxonomy - Create single-movie.php showing the featured image, meta data, and content
- Create archive-movie.php with a grid of movie posters and titles
- Enable show_in_rest and verify the REST endpoint returns movie data
- Write a WP_Query loop that fetches movies with a rating >= 8
What's Next
Now that you can create custom content types, learn how to organize and filter them with custom taxonomies and enhance them with custom fields:
Continue to Lesson 39: Custom Taxonomies and Custom Fields — Group, filter, and extend your CPTs.
Related lessons:
- WP_Query and The Loop — Query your CPTs with custom parameters
- Hooks — Hook into save_post and other actions to automate CPT workflows
- WordPress REST API — Expose your CPTs to external applications
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro