register_post_type()WP 2.9.0

Creates a new post type or changes an existing one.

New Post type must be created at the time of the init hook event. It will not be created if you attach a function before init and may not work correctly if used after.

The name for the new post type should be a unique other than existing: taxonomies, post types, and reserved WordPress public and private variables.

Important: After creating a new post type. Be sure to go to the Settings → Permalinks page. This is necessary to recreate friendly URLs of the newly created post type. After visiting the admin page new rewrite rules will be recreated automatically.

Since version 4.6 a new class WP_Post_Type was added and all function code is now handled by this class, and this function became a wrapper for it.


If there is a taxonomy for a new post type, always specify that taxonomy when registering the post type using the taxonomies parameter. If you do not do this, the post type and taxonomies will not be recognized as related on some hooks such as: parse_query or pre_get_posts. This can lead to unexpected consequences and errors.

Taxonomies must be registered separately. Although you specify the taxonomy when registering the post type, the taxonomy itself must be registered separately with register_taxonomy() function.

You need to register the taxonomy first, and only then the post type to which the taxonomy is linked!

// the correct order of registering a post type and its taxonomy
register_taxonomy( ... );
register_post_type( ... );

This case will save you from bugs and a lot of wasted time in some cases.


WP_Post_Type|WP_Error. WP_Post_Type object (from WP 4.6).

Usage Template
add_action( 'init', 'register_post_types' );

function register_post_types(){

	register_post_type( 'post_type_name', [
		'taxonomies' => [], // post related taxonomies
		'label'  => null,
		'labels' => [
			'name'               => '____', // name for the post type.
			'singular_name'      => '____', // name for single post of that type.
			'add_new'            => 'Add ____', // to add a new post.
			'add_new_item'       => 'Adding ____', // title for a newly created post in the admin panel.
			'edit_item'          => 'Edit ____', // for editing post type.
			'new_item'           => 'New ____', // new post's text.
			'view_item'          => 'See ____', // for viewing this post type.
			'search_items'       => 'Search ____', // search for these post types.
			'not_found'          => 'Not Found', // if search has not found anything.
			'parent_item_colon'  => '', // for parents (for hierarchical post types).
			'menu_name'          => '____', // menu name.
		'description'         => '',
		'public'              => true,
		//'publicly_queryable'  => null, // depends on public
		//'exclude_from_search' => null, // depends on public
		//'show_ui'             => null, // depends on public
		//'show_in_nav_menus'   => null, // depends on public
		'show_in_menu'        => null, // whether to in admin panel menu
		//'show_in_admin_bar'   => null, // depends on show_in_menu.
		'show_in_rest'        => null, // Add to REST API. WP 4.7.
		'rest_base'           => null, // $post_type. WP 4.7.
		'menu_position'       => null,
		'menu_icon'           => null,
		//'capability_type'   => 'post',
		//'capabilities'      => 'post', // Array of additional rights for this post type.
		//'map_meta_cap'      => null, // Set to true to enable the default handler for meta caps.
		'hierarchical'        => false,
		// [ 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'trackbacks', 'custom-fields', 'comments', 'revisions', 'page-attributes', 'post-formats' ]
		'supports'            => [ 'title', 'editor' ],
		'has_archive'         => false,
		'rewrite'             => true,
		'query_var'           => true,
	] );



register_post_type( $post_type, $args );
$post_type(string) (required)

The name of the post type (maximum 20 characters). Can only contain lowercase characters, numbers, _ or -: a-z0-9_-. See sanitize_key().

Reserved names for post types. You cannot use the following names for new post types because they are used by WordPress and your code will conflict with current WordPress code or functions:

Array of arguments.
Default: array() (default parameters)

$args parameter arguments

See WP_Post_Type::set_props() for complate list of the parameters.

The name of the post type marked for translation into another language.
Note: Without the label argument you can't make a separate output template for a custom post type.
Default: $post_type

An array containing label names for a post type.

If any separate value is not specified the default one will be used:

  • For non-hierarchical post types - the names of "standard WP posts".
  • For hierarchical post types, the names of "static pages".

All allowed fields of this array:

'name'                     => '', // The main name for a post type, usually in the plural.
'singular_name'            => '', // The name for one post of that type.
'add_new'                  => '', // Text for adding a new post, like "add new" in posts in the admin panel.
								  // If you want to use a translation of the title, type it like this: _x('Add New', 'product');
'add_new_item'             => '', // Title text of the newly created post in the admin panel. As "Add New Post" for posts.
'edit_item'                => '', // The text to edit the post type. Default: edit post/edit page.
'new_item'                 => '', // New post text. Default: "New post."
'view_item'                => '', // Text to view a post of this post type. Default: "View post"/"View page".
'search_items'             => '', // Search text for these post types. Default: "Find post"/"find page".
'not_found'                => '', // Text if the search did not find anything.
								  // Default: "No post was found"/"No page was found".
'not_found_in_trash'       => '', // Text if nothing was found in the trash.
								  // Default is: "No posts found in Trash"/"No pages found in Trash".
'parent_item_colon'        => '', // Text for parent types. This parameter is not used for non-hierarchical post types.
								  // Default is: null/"Parent Page:".
'all_items'                => '', // All posts. The default is "All Posts"/"All Pages".
'archives'                 => '', // Posts Archives. Defaults to value of all_items field.
'insert_into_item'         => '', // Insert in post.
'uploaded_to_this_item'    => '', // Loaded for this post.
'featured_image'           => '', // Posts thumbnail.
'set_featured_image'       => '', // Set post thumbnail.
'remove_featured_image'    => '', // Delete post thumbnail.
'use_featured_image'       => '', // Use as post thumbnail.
'filter_items_list'        => '', // Filter post list.
'items_list_navigation'    => '', // Navigate through postings.
'items_list'               => '', // List of posts.
'menu_name'                => '', // Menu name. Defaults to name.
'name_admin_bar'           => '', // Name in the admin bar (toolbar). Defaults to singular_name.
'view_items'               => '', // Name in toolbar, for archive page of post type. Default: "View Posts" / "View Pages". FROM WP 4.7.
'attributes'               => '', // Name for post attributes metabox (for pages it is "Page Attributes" metabox).
								  // Default: "Post Attributes" or "Page Attributes". FROM WP 4.7.
'item_updated'             => '', // Note text in the post editor when the post is updated. FROM WP 5.0.
								  // Default: "Post updated." / "Page updated."
'item_published'           => '', // Note text in post editor when a post is published. FROM WP 5.0.
								  // Default: "Post published." / "Page published."
'item_published_privately' => '', // Note text in post editor when private post is published. FROM WP 5.0.
								  // Default: "Post published privately." / "Page published privately."
'item_reverted_to_draft'   => '', // The text of a note in the post editor when the post is returned to draft. FROM WP 5.0.
								  // Default: "Post reverted to draft." / "Page reverted to draft."
'item_scheduled'           => '', // Note text in the post editor when a post is scheduled for a future date. FROM WP 5.0.
								  // Default: "Post scheduled." / "Page scheduled."
'item_updated'             => '',
'item_link'                => '',
'item_link_description'    => '',

For a complete list of values see get_post_type_labels()

Default: if not set, name and singular_name will take the value of the label argument

A short description of this post type. The value is used in the REST API. The value can be obtained with get_the_post_type_description().
Default: ''

Determines whether the post type is public or not. Many others are based on this parameter, i.e. it is a kind of pre-setting for the following parameters:

  • false

    • show_ui = false - do not show the user interface (UI) for this post type.
    • publicly_queryable = false - queries related to this post type will not work on frontend.
    • exclude_from_search = true - this post type will not be taken into account in site search.
    • show_in_nav_menus = false - this post type will be hidden from navigation menu selection.
  • true
    • show_ui = true
    • publicly_queryable = true
    • exclude_from_search = false
    • show_in_nav_menus = true

Default: false


true will enable public viewing of this post type. It means that URL requests for this post type will work on the front-end.

Endpoints would include:

// without friendly URL

// with friendly URL

When false posts of this type will not be available in the front-end via normal URL requests, and you will see a 404 page when prompted for the current post type.

This parameter is checked in the WP::parse_request() method when processing the basic WP request.

Note: if the query_var parameter is empty '|null|false, WordPress will still try to process it and give a 404 page.

If publicly_queryable = false and query_var = true, then when we go to the post URL we will see the front page! WARNING with a response code of 200! So if publicly_queryable = false or public = false, you must also specify query_var = false, and in this case we will see a 404 page when going to the URL of the post, as we should.

Default: value of public argument

Whether to exclude this type of posts from the site search. 1 (true) - yes, 0 (false) - no.

If this parameter is set to true then for taxonomy terms linked to this post type no output will work (even if parameter public is true). It means, this post type will be completely excluded from queries like query_posts()!

Default: reverse value of public argument


Determines whether or not it needs to create logic to control the post type from the admin panel. I.e. whether it is necessary to create a UI of the post type so that it can be controlled.

So, for example, if you specify true and show_in_menu = false. Then we will be able to go to the post type control page, i.e. the WP engine will understand and handle such requests, but there will be no link to this page in the admin menu.

Default: value of public


Whether to show post type in the admin menu and where exactly to show post type control. The show_ui argument must be enabled!

  • false - do not show in the admin menu.
  • true - show as first level menu.
  • string - show as sub-menu of first level menu, for example, sub-menu for 'tools.php' or 'edit.php?post_type=page'. For custom post types you should specify $menu_slug see. add_menu_page().

NOTE: If string is used to show as a submenu (of any main menu created by plugin), this item will become the first in the list and will change the location of the other menu items accordingly. To prevent such behavior, you need to set the priority for the admin_menu action to 9 or lower in the plugin that creates the menu.

Default: value of show_ui argument

Make this type available from the admin bar.
Default: null (value of show_in_menu)
Enable the ability to select this post type in the navigation menu.
Default: value of public
show_in_rest(true|false) (WP 4.7)

Whether to include post type in REST API. true - will add a post type to the wp/v2 route.

Also affects the Gutenberg block editor:

  • true - the Gutenberg editor is enabled for this post type.
  • false - the classic editor will be used.

Default: false

rest_base(string) (WP 4.7)
A slug in the REST API. By default, the name of the post type.
Default: $post_type
rest_controller_class(string) (WP 4.7)
The name of the REST API Controller class.
Default: 'WP_REST_Posts_Controller'
rest_controller(WP_REST_Controller) (WP 5.3)

The controller instance for this post type's REST API endpoints.

Lazily computed. Should be accessed using {@see WP_Post_Type::get_rest_controller()}.

rest_namespace(string) (WP 5.9)
namespase of REST API route (route URL prefix).
Default: wp/v2

The position where the new post type menu should be located:

  • 1 — at the very top of the menu.
  • 2-3 — under "Console".
  • 4-9 — under "Posts".
  • 10-14 — under "Media Files".
  • 15-19 — under "Links."
  • 20-24 — under "Pages."
  • 25-59 — under "Comments" (default, null).
  • 60-64 — under "Appearance."
  • 65-69 — under "Plugins".
  • 70-74 — under "Users".
  • 75-79 — under "Tools".
  • 80-99 — under "Settings".
  • 100+ — under separator after "Settings".

Default: null

Link to the image that will be used for this menu.

With the release of WordPress 3.8 there is a new package of icons Dashicons, which is part of the WordPress core. It's a set of over 150 vector images. To install one of the icons, write its name in this option. For example, the icon of Posts, is dashicons-admin-post, for Links - dashicons-admin-links.

All variants how you can specify the icon:

'dashicons-admin-post' // dashicon name.

get_template_directory_uri() .'/images/icon.png' // image url.

'data:image/svg+xml;base64,' . base64_encode('<svg width="20" height="20"><path fill="black" /></svg>')
// Directly specify an SVG icon in base64 format.
// You can get a ready-made SVG at this link:
// There are two things to consider here:
// 1) You need to specify the attribute fill="black" for path, so that WordPress can change the color of the icon to match the chosen theme.
// 2) You need to change the height/width of the icon to 20px, because this is the base size in WordPress.

'none' // will add the ".wp-menu-image empty" class to the element, this will allow you to set the icon via CSS.

Default: null (Posts menu icon)


The string that will be the marker to set permissions for this post type.
The built-in markers are: post and page.

You can specify an array. The first value would be used for singular and the second for plural, for example: [ 'story', 'stories' ]. If a string is passed, then for plurals 's' lettes adds at the end.

This parameter is used to build a list of user capabilities that will be written to the 'capabilities' parameter.

When setting a non-standard marker (not post or page), the map_meta_cap parameter can be set to true or false:

  • If you set true - then WordPress will automatically generate a permission group for the capabilities parameter based on the data specified here. In this case, the capabilities specified in the capabilities parameter will complement the existing list of rights.

  • If you set false - then WordPress will not generate anything, and you will have to prescribe all possible capabilities for this post type in the capabilities parameter yourself.

Example: let's say we specified here the string bill that is equal to [ 'bill', 'bills' ], then WordPress will automatically generate the following caps for the 'capabilities' parameter:

[cap] => stdClass Object(
	// Meta caps
	[edit_post] => edit_bill
	[read_post] => read_bill
	[delete_post] => delete_bill

	// Primitive caps, NOT used in map_meta_cap()
	[edit_posts] => edit_bills
	[edit_others_posts] => edit_others_bills
	[publish_posts] => publish_bills
	[read_private_posts] => read_private_bills
	[read] => read
	[delete_posts] => delete_bills
	[delete_private_posts] => delete_private_bills
	[delete_published_posts] => delete_published_bills
	[delete_others_posts] => delete_others_bills
	[edit_private_posts] => edit_private_bills
	[edit_published_posts] => edit_published_bills

	// Primitive caps, used in the map_meta_cap()
	[create_posts] => edit_bills

To see what rights were created, look in the global variable: $GLOBALS['wp_post_types']['bill'].

Default: "post"


An array of caps for this post type.

The array element looks like KEY => VALUE, where:

  • KEY is the common name of the user capability.
  • VALUE is the corresponding name of the real capability which will be checked with current_user_can(). An example of this check:
    current_user_can( KEY )
    // when this code fires, it turns into
    current_user_can( VALUE )

By default, there are 8 array elements available that define the rights for this post type (even if map_meta_cap = false), these are:

  • edit_post, read_post, delete_post - 3 permissions control editing, reading, and deleting of a particular post. These are meta-rights: not primitive rights which require computation on the fly. They are not written in each user's permission list, but are turned into primitive permissions on the fly by the map_meta_cap() function.
  • edit_posts - controls the ability to edit post of this post type.
  • create_posts - alias: the same as the edit_posts permission.
  • edit_others_posts - controls the ability to edit post of this post type that belong to another user. If the post type does not support authors, the behavior of this argument will be similar to 'edit_posts'.
  • publish_posts - controls the ability to publish post of this post type.
  • read_private_posts - controls the ability to read personal posts.

Note: primitive capabilities like *_posts are used in the WP kernal in many places to check different user allows.

There are 8 other primitive rights that do not directly belong to the WP kernel, but belong to the map_meta_cap() function and are checked there. They are set automatically if the parameter map_meta_cap = true is given:

  • read - allows to view a post in the front-end.
  • delete_posts - allows to delete a post of this post type.
  • delete_private_posts - allows to delete a private post of this post type.
  • delete_published_posts - allows to delete published posts of this post type.
  • delete_others_posts - Allow to delete posts belonging to other authors. If a post has no author, the behavior is passed to 'delete_posts'.
  • edit_private_posts - allows to edit private posts.
  • edit_published_posts - allows editing of published posts.
  • create_posts - allows to create new posts.

Note: For a user to be able to create new post, his role must have the 'edit_posts' permission.

This parameter is usually set automatically based on 'capability_type'. For example, if you set 'capability_type' and 'map_meta_cap' and look in the $GLOBALS['wp_post_types']['post_type'] variable, we will see such an object:

[cap] => stdClass Object (
	// meta caps
	[edit_post]   => "edit_{$capability_type}"
	[read_post]   => "read_{$capability_type}"
	[delete_post] => "delete_{$capability_type}"

	// Primitive rights are used outside of map_meta_cap():
	[edit_posts]         => "edit_{$capability_type}s"
	[edit_others_posts]  => "edit_others_{$capability_type}s"
	[publish_posts]      => "publish_{$capability_type}s"
	[read_private_posts] => "read_private_{$capability_type}s"

	// Primitive rights are used in map_meta_cap():
	[read]                   => "read",
	[delete_posts]           => "delete_{$capability_type}s"
	[delete_private_posts]   => "delete_private_{$capability_type}s"
	[delete_published_posts] => "delete_published_{$capability_type}s"
	[delete_others_posts]    => "delete_others_{$capability_type}s"
	[edit_private_posts]     => "edit_private_{$capability_type}s"
	[edit_published_posts]   => "edit_published_{$capability_type}s"
	[create_posts]           => "edit_{$capability_type}s"

Example of non-standard usage of capabilities:

'capabilities'     => array(
	'delete_posts'           => 'edit_theme_options',
	'delete_post'            => 'edit_theme_options',
	'delete_published_posts' => 'edit_theme_options',
	'delete_private_posts'   => 'edit_theme_options',
	'delete_others_posts'    => 'edit_theme_options',
	'edit_post'              => 'edit_css',
	'edit_posts'             => 'edit_css',
	'edit_others_posts'      => 'edit_css',
	'edit_published_posts'   => 'edit_css',
	'read_post'              => 'read',
	'read_private_posts'     => 'read',
	'publish_posts'          => 'edit_theme_options',

Default: uses the 'capability_type' parameter value to build a list of permissions


Set 'true' to enable the default map_meta_cap() handler for special capabilities. It converts meta (ambiguous) capabilities (ex: 'edit_post' - one user can and another can't) into primitive capabilities (ex: 'edit_posts' - all users can). This option should be enabled if a post type has special permissions (other than 'post').

Note: if not set (leave null), the default value logic branches out:

  • If 'post' or 'page' is specified in 'capability_type' and no 'capabilities' parameter is specified, then 'map_meta_cap = true'.
  • In all other cases 'map_meta_cap = false'.

    // code part of WP_Post_Type::set_props
    // Back compat with quirky handling in version 3.0. #14122.
    if (
    	empty( $args['capabilities'] )
    	&& null === $args['map_meta_cap']
    	&& in_array( $args['capability_type'], [ 'post', 'page' ] )
    ) {
    	$args['map_meta_cap'] = true;
    // If not set, default to false.
    if ( null === $args['map_meta_cap'] ) {
    	$args['map_meta_cap'] = false;

Note: if set to false, the default "Administrator" role will not be able to edit this post type. To remove this restriction, you will have to add edit_{post_type}'s right to the administrator role.

Default: null


Whether posts of this type will have a tree-like structure (like basic static WP pages).

  • true - yes, they will be tree-like.
  • false - no, they will be flat - linked to taxonomy (categories).

Default: false


The features supported by the post type. Fields/metaboxes on post type creation/editing page.

If false no additional metaboxes will be displayed, including those that are set by default.

  • title – title block (by default).
  • editor – block for entering the content (by default).
  • author – author selection block.
  • thumbnail – post thumbnail selection block. You should also enable theme support of this feature. See post-thumbnails.
  • excerpt – short post description block.
  • trackbacks – enable trackbacks and pings support (not responsible for blocks).
  • custom-fields – block to set/edit custom fields.
  • comments – comments block (discussion).
  • revisions – revision block.
  • page-attributes – the block of static page attributes (template and post parent, if 'hierarchical' is enabled).
  • post-formats – block of post formats, if they are enabled for the theme.

The value can also be specified as an array in order to pass additional parameters to the feature. For example:

	[ 'my_feature', [ 'field' => 'value' ]

Note: If the custom post type uses thumbnails, be sure to check that the theme also supports thumbnails or use the add_theme_support( 'post-thumbnails' ).

Default: [ 'title', 'editor' ]

Provide a callback function that sets up the meta boxes for the edit form. Do remove_meta_box() and add_meta_box() calls in the callback function.
Default: null

An array of registered taxonomies that will be associated with this post type, for example: category, post_tag.

You can link taxonomies to the post type later with the register_taxonomy_for_object_type() function.

Taxonomies themselves should be registered with the register_taxonomy() function.

Default: []

The index of the endpoint. Post type will be associated with this endpoint. As a rule, this parameter is not used. Here you can specify the following constants or a combination of that constants concatinated with & or |:

  • EP_DAY
  • EP_ALL

An endpoint is something that is added to the end of a URL, for example /trackback/. Endpoints are attached to the post type (added to rewrite rules) with add_rewrite_endpoint().

This parameter allows you to specify endpoints you want to relate with the post type (for the post URL). For example, if you specify permalink_epmask = EP_PAGES & EP_TAGS, then our post type will have all the additional URL options (endpoints) that are provided for static pages and tags.

By default permalink_epmask = EP_PERMALINK - this means that the URL (friendly URL) of the post of this post type will have all endpoints that are normally uses for regular WordPress posts, it is: pagination, comment page, etc.

If you don't want to add any endpoints to the new post type, you should specify EP_NONE. Or vice versa, specify EP_ALL when you want to add all endpoints.



Turn on support for archive pages for this post type. E.g. the URL of the post looks like this:, then the URL of the archive will be See get_post_type_archive_link().

If you specify a string, it will be used in friendly URL. For example, specify here typepage and post archive page URL becoms

The archive file template for the theme will look like archive-type.php.

The new friendly URL rule will be added as well if the rewrite argument is enabled.

Default: false


Whether to use the friendly URL for this post type. To not use friendly URL, specify 'false'. Default: true - the post type name is used as a prefix in the friendly URL. You can specify additional parameters in the array to build specific rules for URL:

  • slug(string)
    Prefix for the friendly URL: /prefix/post_type. Use [ 'slug' => $slug ] to create your prefix.

    In this parameter you can specify placeholders like %category%. But they must be registered separately, or they must be created with add_rewrite_tag() so WP knows about them.

    Default: post type name

  • with_front(true|false)
    Whether to add global prefix at the beginning of the URL. The prefix is taken from $wp_rewite->front prop.

    For example, name of our new post type is 'news' and posts Permalink settings structure is set to blog/%postname%, then

    • if 'false' we will get: /news/postname.
    • if 'true' we will get: /blog/news/postname.

    Default: true

  • feeds(true|false)
    Whether to add a rewrite rules for the RSS feed of this post type.

    Default: 'has_archive' argument value

  • pages(true|false)
    Whether to add a friendly URL rule for paginating an archive of entries of this type. Pr: /post_type/page/2.
    Whether to add a friendly URL rule single post pagination. Ex: /post_type/page/2. I.e., if 'true' is specified, the post can use the shortcode <!--nextpage--> - See wp_link_pages().

    Default: true

Default: true (post type name is used as a prefix)


Sets the name of the query parameter for this post type.

  • false - disables the query parameter. The post will not be accessible by /?{query_var}={post_slug} URL.
  • string - specifies the name of the query parameter. /?{query_var_string}={post_slug}.

Note: if publicly_queryable = false and query_var = true, then when we go to the URL of the post, we see the front page. WARNING page with a 200 response code! So, if you disable the post from public using 'public = false' or 'publicly_queryable = false', you must also set false for this parameter - 'query_var = false'. In this case, when going to the URL of the post, you will see a 404 page, as expected.

Note: This parameter adds the specified value or post type name (if true is specified) to the list of allowed WordPress query parameters, see add_rewrite_tag(). Because WordPress removes any query parameters that it doesn't know about.

Example: we register the post type 'book' and specify the 'bookname' in this parameter. Now, if we go to the book page at the link /book/harry-potter, the following code processing in this page - get_query_var( 'bookname' ) will return 'harry-potter'. And if we didn't put anything in this parameter (it would be true), we would have to use get_query_var( 'book' ) to get 'harry-potter'.

Default: true (value of $post_type parameter)

Ability to export this post type.
Default: true

Whether to delete posts of this type when deleting a user.

  • true - delete posts of this type belonging to the user when deleting the user. If Trash is enabled, the posts will NOT be deleted, but will be placed in the trash.
  • false - when deleting a user his posts of this type will not be processed in any way - will NOT be trashed or deleted.
  • null - post will be deleted or moved to the trash if post type supports the 'author' feature - `post_type_supports( 'author' ). Otherwise posts are not trashed or deleted.

Default: null

template(arary) (WP 5.0.0)

Array of blocks that will be used as the initial state for the editor.

Each element must be an array containing the block name and optional attributes.

Read more here:

Default: array()

template_lock(true|false) (WP 5.0.0)

Should the block template be locked if the template parameter is set.

  • If set to all, the user cannot insert new blocks, move existing blocks and delete blocks.
  • If set to insert, the user can move existing blocks, but cannot insert new blocks or delete blocks.

Read more here:

Default: false


Post type capabilities. Set automatically. Example value when cabability_type = post:

	'edit_post' => 'edit_post',
	'read_post' => 'read_post',
	'delete_post' => 'delete_post',
	'edit_posts' => 'edit_posts',
	'edit_others_posts' => 'edit_others_posts',
	'delete_posts' => 'delete_posts',
	'publish_posts' => 'publish_posts',
	'read_private_posts' => 'read_private_posts',
	'read' => 'read',
	'delete_private_posts' => 'delete_private_posts',
	'delete_published_posts' => 'delete_published_posts',
	'delete_others_posts' => 'delete_others_posts',
	'edit_private_posts' => 'edit_private_posts',
	'edit_published_posts' => 'edit_published_posts',
	'create_posts' => 'edit_posts',

Whether this post type is a native or "built-in" post_type.

For internal use! True if it is a built-in/internal WP post type.

Default: false

URL segment to use for edit link of this post type. For internal use!
Default: 'post.php?post=%d'



#1 Registering a new post type

An example of registering a custom post type "book". It also shows how to enable update messages and support the help section.

add_action( 'init', 'my_custom_init' );

function my_custom_init(){

	register_post_type( 'book', [
		'labels'             => [
			'name'               => 'Books', // main name of the post type
			'singular_name'      => 'Book', // single Book post name
			'add_new'            => 'Add new',
			'add_new_item'       => 'Add new book',
			'edit_item'          => 'Edit Book',
			'new_item'           => 'New book',
			'view_item'          => 'View book',
			'search_items'       => 'Find book',
			'not_found'          => 'No books found',
			'not_found_in_trash' => 'No books found in trash',
			'parent_item_colon'  => '',
			'menu_name'          => 'Books',
		'public'             => true,
		'publicly_queryable' => true,
		'show_ui'            => true,
		'show_in_menu'       => true,
		'query_var'          => true,
		'rewrite'            => true,
		'capability_type'    => 'post',
		'has_archive'        => true,
		'hierarchical'       => false,
		'menu_position'      => null,
		'supports'           => [ 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' ]
	] );

Messages when you publish or update the post of book type:

// Messages when publish or update the book post
add_filter( 'post_updated_messages', 'book_updated_messages' );

function book_updated_messages( $messages ) {
	global $post;

	$messages['book'] = array(
		0 => '', // Not used. Messages are used from index 1.
		1 => sprintf( 'Book updated. <a href="%s">View book entry</a>',
				esc_url( get_permalink($post->ID) )
		2 => 'Custom field updated.'
		3 => 'Custom field deleted.'
		4 => 'Book updated.',
		/* %s: revision date and time */.
		5 => isset( $_GET['revision'] )
				? sprintf( 'Book entry restored from revision %s', wp_post_revision_title( (int) $_GET['revision'], false ) )
				: false,
		6 => sprintf( 'Book published. <a href="%s">Go to book entry</a>',
				esc_url( get_permalink($post->ID) )
		7 => 'Book saved.',
		8 => sprintf( 'Book saved. <a target="_blank" href="%s">Preview book entry</a>',
				esc_url( add_query_arg( 'preview', 'true', get_permalink($post->ID) ) )
		9 => sprintf( 'Book entry scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview Book entry</a>',
			  // How to format dates in PHP can be found here:
			  date_i18n( __( 'M j, Y @ G:i' ), strtotime( $post->post_date ), esc_url( get_permalink($post->ID) )
		10 => sprintf( 'Book entry draft updated. <a target="_blank" href="%s">Preview book entry</a>',
				esc_url( add_query_arg( 'preview', 'true', get_permalink($post->ID) ) )

	return $messages;

The help section of the book post type:

// The "help" section of the book post type
add_action( 'contextual_help', 'add_help_text', 10, 3 );

function add_help_text( $contextual_help, $screen_id, $screen ){

	// $contextual_help .= print_r( $screen );
	if( 'book' == $screen->id ) {
		$contextual_help = '
		<p>A reminder when editing a book:</p>
			<li>Specify the genre, such as Fiction or History.</li>
			<li>Specify the author of the book.</li>
		<p>If you want to schedule a publication for the future:</p>
			<li>In the block with the "publish" button, click edit date.</li>
			<li>Change the date to the desired, future date and confirm the changes with the "OK" button below.</li>
		<p><strong>For more information, contact:</strong></p>
		<p><a href="/" target="_blank">WordPress Blog</a></p>
		<p><a href="" target="_blank">Support Forum</a></p>
	elseif( 'edit-book' == $screen->id ) {
		$contextual_help = '<p>This is the help section shown for the record type Book, etc.</p>';

	return $contextual_help;

#2 Adding a taxonomy element to friendly URL

You can set non-standard friendly URL for a new post type with the rewrite parameter. This example shows how to add a taxonomy to the friendly URL.

Suppose we register a post type catalog and a taxonomy products for it. Next, we need that when we publish a post and select taxonomy element for it. This element is added to the URL and as a result the link to the post type looks like this:

To do this, you must specify the slug argument in the rewrite parameter when registering the post type:

'rewrite' => [ 'slug'=>'catalog/%products%', 'with_front' => false ],
'has_archive' => 'catalog', // If you need the archive page, specify its shortcut here (but not 'true')

Now we need to add a hook to replace %products% when getting a link to a post through the get_permalink() function and its derived functions:

## Filter a custom type of friendly URL
// apply_filters( 'post_type_link', $post_link, $post, $leavename, $sample );
add_filter( 'post_type_link', 'products_permalink', 1, 2 );

function products_permalink( $permalink, $post ){

	// Exit if it is not our post type: without %products% placeholder
	if( strpos( $permalink, '%products%' ) === false ){
		return $permalink;

	// Getting the elements of the tax
	$terms = get_the_terms( $post, 'products' );

	// If there is an item we will replace the pholder
	if( ! is_wp_error( $terms ) && !empty( $terms ) && is_object( $terms[0] ) ){
		$taxonomy_slug = $terms[0]->slug;
	// There is no element, but it should be...
	else {
		$taxonomy_slug = 'no-products';

	return str_replace( '%products%', $taxonomy_slug, $permalink );

The result will be a friendly URL as above and the link will be recognized by WordPress rewrite rules.

For hierarchical taxonomies

For hierarchical taxonomies, you will need to collect the entire path, the function: get_term_parents_list()

$tax_slug =  get_term_parents_list( $term_id, $tax_name, [
	'separator' => '/',
	'format'    => 'slug',
	'link'      => false,
	'inclusive' => true,
] );

It is also important that the hierarchical parameter be false when register the post type!


#3 Adding Taxonomy to friendly URL (post and taxonomy have the same prefix)

This example shows how to create a faq (questions) post type and categories for it (taxonomy faqcat). And the taxonomy will have the same prefix as the post type:

  • Post: `{category}/{label-record}
  • Taxonomy:{category}

Here it is important to register taxonomy first, and then the post type:

// register post type and taxonomy
add_action( 'init', 'register_faq_post_type' );

// Filter a friendly URL of post type
add_filter( 'post_type_link', 'faq_permalink', 1, 2 );

function register_faq_post_type() {

	// question category - faqcat
	register_taxonomy( 'faqcat', [ 'faq' ], [
		'label'             => 'Question cat',
		'labels'            => [
			'name'              => 'Question Sections',
			'singular_name'     => 'Question Section',
			'search_items'      => 'Search Question Section',
			'all_items'         => 'All Question Sections',
			'parent_item'       => 'Parenthesis section of the question',
			'parent_item_colon' => 'Parent section of the question:',
			'edit_item'         => 'Edit Question Section',
			'update_item'       => 'Update Question Section',
			'add_new_item'      => 'Add Question Section',
			'new_item_name'     => 'New Question Section',
			'menu_name'         => 'Question Section',
		'description'       => 'Categories for the Questions section',
		'public'            => true,
		'show_in_nav_menus' => false,   // same as public
		'show_ui'           => true,    // same as public
		'show_tagcloud'     => false,   // same as show_ui
		'hierarchical'      => true,
		'rewrite'           => [ 'slug' => 'faq', 'hierarchical' => false, 'with_front' => false, 'feed' => false ],
		// Whether or not to allow auto-creation of a taxonomy column in an associated post type table. (WP 3.5)
		'show_admin_column' => true,
	] );

	// post type - questions - faq
	register_post_type( 'faq', [
		'label'               => 'Questions',
		'labels'              => [
			'name'          => 'Questions',
			'singular_name' => 'Question',
			'menu_name'     => 'Question Archive',
			'all_items'     => 'All Questions',
			'add_new'       => 'Add Question',
			'add_new_item'  => 'Add a new question',
			'edit'          => 'Edit',
			'edit_item'     => 'Edit an issue',
			'new_item'      => 'New question',
		'description'         => '',
		'public'              => true,
		'publicly_queryable'  => true,
		'show_ui'             => true,
		'show_in_rest'        => false,
		'rest_base'           => '',
		'show_in_menu'        => true,
		'exclude_from_search' => false,
		'capability_type'     => 'post',
		'map_meta_cap'        => true,
		'hierarchical'        => false,
		'rewrite'             => [
			'slug'       => 'faq/%faqcat%',
			'with_front' => false,
			'pages'      => false,
			'feeds'      => false,
			'feed'       => false,
		'has_archive'         => 'faq',
		'query_var'           => true,
		'supports'            => [ 'title', 'editor' ],
		'taxonomies'          => [ 'faqcat' ],
	] );

function faq_permalink( $permalink, $post ) {

	// stop if it is not our post type: without placeholder %faqcat%
	if( strpos( $permalink, '%faqcat%' ) === false ){
		return $permalink;

	// Get the tax elements
	$terms = get_the_terms( $post, 'faqcat' );
	// change holder if there is element
	if( ! is_wp_error( $terms ) && ! empty( $terms ) && is_object( $terms[0] ) ){
		$term_slug = array_pop( $terms )->slug;
	// There is no element, but there should be...
		$term_slug = 'no-faqcat';

	return str_replace( '%faqcat%', $term_slug, $permalink );


Post Type UI plugin

There is a handy plugin that allows you to register new post types and new taxonomies: Custom Post Type UI

Rename post type labels

If a post type is already registered, but we need to name it something else, use the code below.

This code shows how to rename the standard post type "Posts" to "Articles":

// Replace Posts name to Articles
add_filter( 'post_type_labels_post', 'rename_posts_labels' );
function rename_posts_labels( $labels ){

	// replace automatically
	foreach( $labels as & $label ){
		$label = str_replace( ['post', 'Post' ], [ 'article', 'Article' ], $label );
	unset( $label );

	return $labels;

// Replace Posts name to Articles (manually)
add_filter( 'post_type_labels_post', 'rename_posts_labels_manually' );
function rename_posts_labels_manually( $labels ){

	/* original
		stdClass Object
			[name] => Posts
			[singular_name] => Post
			[add_new] => Add New
			[add_new_item] => Add New Post
			[edit_item] => Edit Post
			[new_item] => New Post
			[view_item] => View Post
			[view_items] => View Posts
			[search_items] => Search Posts
			[not_found] => No posts found.
			[not_found_in_trash] => No posts found in Trash.
			[parent_item_colon] =>
			[all_items] => All Posts
			[archives] => Post Archives
			[attributes] => Post Attributes
			[insert_into_item] => Insert into post
			[uploaded_to_this_item] => Uploaded to this post
			[featured_image] => Featured image
			[set_featured_image] => Set featured image
			[remove_featured_image] => Remove featured image
			[use_featured_image] => Use as featured image
			[filter_items_list] => Filter posts list
			[filter_by_date] => Filter by date
			[items_list_navigation] => Posts list navigation
			[items_list] => Posts list
			[item_published] => Post published.
			[item_published_privately] => Post published privately.
			[item_reverted_to_draft] => Post reverted to draft.
			[item_scheduled] => Post scheduled.
			[item_updated] => Post updated.
			[item_link] => Post Link
			[item_link_description] => A link to a post.
			[menu_name] => Posts
			[name_admin_bar] => Post

	$new = array(
		'name'                     => 'Articles',
		'singular_name'            => 'Article',
		'add_new'                  => 'Add New',
		'add_new_item'             => 'Add New Article',
		'edit_item'                => 'Edit Article',
		'new_item'                 => 'New Article',
		'view_item'                => 'View Article',
		'view_items'               => 'View Articles',
		'search_items'             => 'Search Articles',
		'not_found'                => 'No articles found.',
		'not_found_in_trash'       => 'No articles found in Trash.',
		'all_items'                => 'All Articles',
		'archives'                 => 'Article Archives',
		'attributes'               => 'Article Attributes',
		'insert_into_item'         => 'Insert into article',
		'uploaded_to_this_item'    => 'Uploaded to this article',
		'featured_image'           => 'Featured image',
		'set_featured_image'       => 'Set featured image',
		'remove_featured_image'    => 'Remove featured image',
		'use_featured_image'       => 'Use as featured image',
		'filter_items_list'        => 'Filter articles list',
		'filter_by_date'           => 'Filter by date',
		'items_list_navigation'    => 'Articles list navigation',
		'items_list'               => 'Articles list',
		'item_published'           => 'Article published.',
		'item_published_privately' => 'Article published privately.',
		'item_reverted_to_draft'   => 'Article reverted to draft.',
		'item_scheduled'           => 'Article scheduled.',
		'item_updated'             => 'Article updated.',
		'item_link'                => 'Article Link',
		'item_link_description'    => 'A link to a article.',
		'menu_name'                => 'Articles',
		'name_admin_bar'           => 'Article',

	return (object) array_merge( (array) $labels, $new );


  • Global. Array. $wp_post_types List of post types.


Since 2.9.0 Introduced.
Since 3.0.0 The show_ui argument is now enforced on the new post screen.
Since 4.4.0 The show_ui argument is now enforced on the post type listing screen and post editing screen.
Since 4.6.0 Post type object returned is now an instance of WP_Post_Type.
Since 4.7.0 Introduced show_in_rest, rest_base and rest_controller_class arguments to register the post type in REST API.
Since 5.0.0 The template and template_lock arguments were added.
Since 5.3.0 The supports argument will now accept an array of arguments for a feature.
Since 5.9.0 The rest_namespace argument was added.

register_post_type() code WP 6.5.2

function register_post_type( $post_type, $args = array() ) {
	global $wp_post_types;

	if ( ! is_array( $wp_post_types ) ) {
		$wp_post_types = array();

	// Sanitize post type name.
	$post_type = sanitize_key( $post_type );

	if ( empty( $post_type ) || strlen( $post_type ) > 20 ) {
		_doing_it_wrong( __FUNCTION__, __( 'Post type names must be between 1 and 20 characters in length.' ), '4.2.0' );
		return new WP_Error( 'post_type_length_invalid', __( 'Post type names must be between 1 and 20 characters in length.' ) );

	$post_type_object = new WP_Post_Type( $post_type, $args );

	$wp_post_types[ $post_type ] = $post_type_object;


	 * Fires after a post type is registered.
	 * @since 3.3.0
	 * @since 4.6.0 Converted the `$post_type` parameter to accept a `WP_Post_Type` object.
	 * @param string       $post_type        Post type.
	 * @param WP_Post_Type $post_type_object Arguments used to register the post type.
	do_action( 'registered_post_type', $post_type, $post_type_object );

	 * Fires after a specific post type is registered.
	 * The dynamic portion of the filter name, `$post_type`, refers to the post type key.
	 * Possible hook names include:
	 *  - `registered_post_type_post`
	 *  - `registered_post_type_page`
	 * @since 6.0.0
	 * @param string       $post_type        Post type.
	 * @param WP_Post_Type $post_type_object Arguments used to register the post type.
	do_action( "registered_post_type_{$post_type}", $post_type, $post_type_object );

	return $post_type_object;