register_post_type()
The register_post_type() arguments that actually change behaviour: public, show_in_rest, supports, has_archive, rewrite, capabilities.
register_post_type( string $post_type, array|string $args = array() ): WP_Post_Type|WP_Errorregister_post_type() takes a key of at most 20 characters — lowercase letters, digits, dashes and underscores only — plus an array of arguments, and returns a WP_Post_Type object or a WP_Error. Call it on init; core warns that registration must not be hooked earlier. There are more than thirty arguments, and six decide nearly everything.
The six that matter
public(defaultfalse) — the umbrella switch supplying defaults forpublicly_queryable,show_ui,show_in_nav_menusandexclude_from_search.show_in_rest(defaultfalse, added in 4.7) — exposes/wp/v2/{rest_base}.rest_basedefaults to the post type key;rest_namespace(5.9) defaults towp/v2.supports(defaultarray( 'title', 'editor' )) — any oftitle,editor,author,thumbnail,excerpt,trackbacks,custom-fields,comments,revisions,page-attributes,post-formats. Passingfalsedisables all of them. Addingeditorimplies autosave support.has_archive(defaultfalse) —trueuses the post type key as the archive slug, or pass a string for a different one.rewrite(defaulttrue) —falsedisables pretty permalinks for the type.capability_type(default'post') — the noun the capabilities are built from.
Shorthand and longhand
'rewrite' => true is shorthand for array( 'slug' => $post_type, 'with_front' => true, 'feeds' => $has_archive, 'pages' => true, 'ep_mask' => EP_PERMALINK ). Likewise 'capability_type' => 'book' is shorthand for array( 'book', 'books' ); the second element only supplies the plural used for edit_books and friends.
Example
<?php
add_action( 'init', 'acme_register_book_type' );
function acme_register_book_type() {
$args = array(
'labels' => array(
'name' => __( 'Books', 'acme' ),
'singular_name' => __( 'Book', 'acme' ),
),
// Umbrella switch. Default false. Supplies the defaults for
// publicly_queryable, show_ui, show_in_nav_menus, exclude_from_search.
'public' => true,
// Default false. Without this the block editor cannot load the type
// and the admin falls back to the classic editor.
'show_in_rest' => true,
'rest_base' => 'books', // defaults to the post type key
// Default array( 'title', 'editor' ). Anything omitted is off,
// including revisions and featured images.
'supports' => array(
'title',
'editor',
'excerpt',
'thumbnail',
'revisions',
'custom-fields', // needed before registered meta reaches the editor
),
// Default false. true would use /book/; the string gives /library/.
'has_archive' => 'library',
// Longhand of 'rewrite' => true, with a different slug.
'rewrite' => array(
'slug' => 'books',
'with_front' => false, // ignore the blog prefix in the permalink base
'feeds' => true,
'pages' => true,
),
// Default 'post', which reuses edit_posts, publish_posts and so on.
'capability_type' => 'post',
'map_meta_cap' => true,
'menu_position' => 20,
'menu_icon' => 'dashicons-book-alt',
'taxonomies' => array( 'post_tag' ),
);
$post_type = register_post_type( 'book', $args );
if ( is_wp_error( $post_type ) ) {
error_log( $post_type->get_error_message() );
}
}When to use it
- Registering a public post type that works in the block editor and has its own archive URL
- Giving a client role permission to manage one content type without touching blog posts
- Changing the permalink slug of a post type without changing its internal key
- Turning on excerpts or REST support for a post type registered by another plugin
More examples
Custom capabilities that actually work
Use custom capabilities when a role should manage books without gaining access to ordinary posts — a client editor, a shop manager. The two halves are inseparable: registering the type creates the capability names, and only add_cap() gives them to anyone.
add_action( 'init', function () {
register_post_type( 'book', array(
'public' => true,
'show_in_rest' => true,
// Shorthand: 'book' alone is the same as array( 'book', 'books' ).
// The plural is what edit_books, publish_books, ... are built from.
'capability_type' => array( 'book', 'books' ),
// Without this, the meta caps (edit_post, delete_post, read_post)
// are never mapped to the primitive ones and nobody can edit anything.
'map_meta_cap' => true,
) );
} );
/**
* The capabilities now exist but no role holds them. Grant them once,
* on activation — add_cap() writes to the database on every call.
*/
register_activation_hook( __FILE__, function () {
$role = get_role( 'editor' );
foreach ( array(
'edit_book', 'read_book', 'delete_book',
'edit_books', 'edit_others_books', 'publish_books',
'read_private_books', 'delete_books', 'delete_private_books',
'delete_published_books', 'delete_others_books',
'edit_private_books', 'edit_published_books',
) as $cap ) {
$role->add_cap( $cap );
}
} );Inspecting a registered type
How to answer "is this feature actually on?" without re-reading the registration code, which may live in a theme, a plugin, or three plugins at once. post_type_supports() in particular is the right guard before adding a meta box or enqueueing editor assets.
// Everything you passed, plus every default core filled in.
$type = get_post_type_object( 'book' ); // WP_Post_Type|null
var_dump( $type->public ); // true
var_dump( $type->hierarchical ); // false (the default)
var_dump( $type->has_archive ); // 'library'
var_dump( $type->cap->edit_posts ); // the primitive cap this type checks
// Cheaper, targeted checks:
var_dump( post_type_exists( 'book' ) ); // bool
var_dump( post_type_supports( 'book', 'thumbnail' ) ); // bool
var_dump( is_post_type_hierarchical( 'book' ) ); // bool
var_dump( get_post_type_archive_link( 'book' ) ); // false if has_archive is off
// Every public type, as objects, for building a settings screen.
$public_types = get_post_types( array( 'public' => true ), 'objects' );
foreach ( $public_types as $slug => $object ) {
printf( '%s => %s' . PHP_EOL, $slug, $object->labels->name );
}Adding support to a type you do not own
The maintainable way to adjust a post type from WooCommerce, a page builder, or any plugin you cannot edit. Note the priority 20 on init: at priority 10 the type may not be registered yet, and add_post_type_support() would silently do nothing useful.
/**
* Third-party plugins register their types on init at the default priority 10.
* Hook later than they do and amend the registration rather than forking it.
*/
add_action( 'init', function () {
// Turn on excerpts and featured images for someone else's post type.
add_post_type_support( 'product', array( 'excerpt', 'thumbnail' ) );
// And remove one you do not want.
remove_post_type_support( 'product', 'comments' );
}, 20 );
/**
* For arguments that are not "supports", filter the registration args
* as they are passed to register_post_type().
*
* @param array $args Arguments for registering a post type.
* @param string $post_type Post type key.
*/
add_filter( 'register_post_type_args', function ( $args, $post_type ) {
if ( 'product' === $post_type ) {
$args['show_in_rest'] = true; // opt an older plugin's type into the block editor
}
return $args;
}, 10, 2 );
Discussion