Every plugin that stores its own kind of content ends up calling register_post_type(). The call is short, but a few arguments decide whether your archive works, whether the block editor loads, and whether the post type survives a theme switch.
This guide walks through the arguments that matter in practice and the mistakes that cause the most support tickets. The code samples are what the Custom Post Type Generator writes, so you can build your own version in a few clicks.
Register on the init hook
Post types must be registered on init. Registering earlier runs before rewrite rules exist; registering later means requests that need the post type have already been parsed. The generator wraps everything in a prefixed function and hooks it for you:
<?php
/**
* Registers the "book" post type.
*/
function my_plugin_register_book() {
$labels = array(
'name' => _x( 'Books', 'Post type general name', 'my-plugin' ),
'singular_name' => _x( 'Book', 'Post type singular name', 'my-plugin' ),
'menu_name' => _x( 'Books', 'Admin Menu text', 'my-plugin' ),
'add_new_item' => __( 'Add New Book', 'my-plugin' ),
'edit_item' => __( 'Edit Book', 'my-plugin' ),
'all_items' => __( 'All Books', 'my-plugin' ),
);
$args = array(
'labels' => $labels,
'public' => true,
'has_archive' => true,
'show_in_rest' => true,
'menu_position' => 5,
'menu_icon' => 'dashicons-book',
'supports' => array( 'title', 'editor', 'thumbnail' ),
);
register_post_type( 'book', $args );
}
add_action( 'init', 'my_plugin_register_book' );Keep keys short and prefixed
The post type key is limited to 20 characters and is stored in the post_type column of every row in wp_posts. Choose it once: existing posts keep the old key, so renaming it later means migrating content.
| Argument | Default | What it decides |
|---|---|---|
public | false | Whether the post type is visible on the front end and in the admin |
has_archive | false | Whether /books/ lists the posts |
show_in_rest | false | Block editor support and REST API access |
supports | title, editor | Which panels the edit screen shows |
rewrite | true | The permalink slug, which defaults to the key |
Turn on show_in_rest
Without show_in_rest, the block editor is not available for the post type and it will not appear in REST API responses. The editor silently falls back to the classic screen, which is the most common “why is my CPT different” report.
The post type works, but the edit screen is the classic editor and the REST API returns nothing.
register_post_type( 'book', array(
'public' => true,
) );
Posts open in the block editor and are available at /wp-json/wp/v2/book.
register_post_type( 'book', array(
'public' => true,
'show_in_rest' => true,
) );
Flush rewrite rules once
After adding a post type with an archive, visit Settings → Permalinks once. Never call flush_rewrite_rules() on every request: it rewrites the rules option on each page load.
In a plugin, register the post type and call flush_rewrite_rules() inside the function you pass to register_activation_hook(). Archive URLs work straight after activation and the rules are only rebuilt once.
Put it in a plugin, not the theme
Content types belong to the site, not its design. Register them in a plugin or must-use plugin so switching themes does not hide your content.
