WordPress Development2 min read
WordPress Development

Registering Custom Post Types the Right Way

The register_post_type() arguments that decide whether archives work, the block editor loads, and content survives a theme switch.

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:

my-plugin.php
PHP 7.4+ · WordPress 5.0+
<?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.

ArgumentDefaultWhat it decides
publicfalseWhether the post type is visible on the front end and in the admin
has_archivefalseWhether /books/ lists the posts
show_in_restfalseBlock editor support and REST API access
supportstitle, editorWhich panels the edit screen shows
rewritetrueThe 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.

Classic editorshow_in_rest off

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,
) );
Block editorshow_in_rest on

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.

Flush on activation, not on every 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.