register_post_type()

The register_post_type() arguments that actually change behaviour: public, show_in_rest, supports, has_archive, rewrite, capabilities.

Syntaxregister_post_type( string $post_type, array|string $args = array() ): WP_Post_Type|WP_Error

register_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 (default false) — the umbrella switch supplying defaults for publicly_queryable, show_ui, show_in_nav_menus and exclude_from_search.
  • show_in_rest (default false, added in 4.7) — exposes /wp/v2/{rest_base}. rest_base defaults to the post type key; rest_namespace (5.9) defaults to wp/v2.
  • supports (default array( 'title', 'editor' )) — any of title, editor, author, thumbnail, excerpt, trackbacks, custom-fields, comments, revisions, page-attributes, post-formats. Passing false disables all of them. Adding editor implies autosave support.
  • has_archive (default false) — true uses the post type key as the archive slug, or pass a string for a different one.
  • rewrite (default true) — false disables 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

Example · php
<?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.

Example · php
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.

Example · php
// 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.

Example · php
/**
 * 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

  • Be the first to comment on this lesson.