How to register a custom post type with full REST API, Gutenberg, and archive support?

So, you’re looking to create a custom post type in WordPress that plays nicely with everything modern WordPress has to offer – the REST API for seamless data interaction, Gutenberg for a rich editing experience, and archive pages for organized content. Good call! The short answer is that it involves registering your post type with specific arguments that tell WordPress to enable these features. It’s a bit more than just a register_post_type() call, but entirely manageable once you know which levers to pull.

Let’s break down how to get this done in a practical, step-by-step way. We’ll focus on the code and the why behind each setting, aiming for a robust and future-proof custom post type.

Every custom post type starts with the register_post_type() function. This is the core of it all. You’ll typically place this within a custom plugin or your theme’s functions.php file (though a dedicated plugin is generally better practice for custom functionalities).

“`php

function register_my_custom_post_type() {

$labels = array(

‘name’ => _x( ‘Projects’, ‘Post Type General Name’, ‘textdomain’ ),

‘singular_name’ => _x( ‘Project’, ‘Post Type Singular Name’, ‘textdomain’ ),

‘menu_name’ => __( ‘Projects’, ‘textdomain’ ),

‘name_admin_bar’ => __( ‘Project’, ‘textdomain’ ),

‘archives’ => __( ‘Project Archives’, ‘textdomain’ ),

‘attributes’ => __( ‘Project Attributes’, ‘textdomain’ ),

‘parent_item_colon’ => __( ‘Parent Project:’, ‘textdomain’ ),

‘all_items’ => __( ‘All Projects’, ‘textdomain’ ),

‘add_new_item’ => __( ‘Add New Project’, ‘textdomain’ ),

‘add_new’ => __( ‘Add New’, ‘textdomain’ ),

‘new_item’ => __( ‘New Project’, ‘textdomain’ ),

‘edit_item’ => __( ‘Edit Project’, ‘textdomain’ ),

‘update_item’ => __( ‘Update Project’, ‘textdomain’ ),

‘view_item’ => __( ‘View Project’, ‘textdomain’ ),

‘view_items’ => __( ‘View Projects’, ‘textdomain’ ),

‘search_items’ => __( ‘Search Projects’, ‘textdomain’ ),

‘not_found’ => __( ‘No projects found’, ‘textdomain’ ),

‘not_found_in_trash’ => __( ‘No projects found in Trash’, ‘textdomain’ ),

‘featured_image’ => __( ‘Featured Image’, ‘textdomain’ ),

‘set_featured_image’ => __( ‘Set featured image’, ‘textdomain’ ),

‘remove_featured_image’ => __( ‘Remove featured image’, ‘textdomain’ ),

‘use_featured_image’ => __( ‘Use as featured image’, ‘textdomain’ ),

‘insert_into_item’ => __( ‘Insert into project’, ‘textdomain’ ),

‘uploaded_to_this_item’ => __( ‘Uploaded to this project’, ‘textdomain’ ),

‘items_list’ => __( ‘Projects list’, ‘textdomain’ ),

‘items_list_navigation’ => __( ‘Projects list navigation’, ‘textdomain’ ),

‘filter_items_list’ => __( ‘Filter projects list’, ‘textdomain’ ),

);

$args = array(

‘label’ => _x( ‘Project’, ‘Post Type Label’, ‘textdomain’ ),

‘description’ => __( ‘A post type for managing client projects.’, ‘textdomain’ ),

‘labels’ => $labels,

‘supports’ => array( ‘title’, ‘editor’, ‘thumbnail’, ‘excerpt’, ‘author’, ‘custom-fields’, ‘revisions’ ),

‘taxonomies’ => array( ‘category’, ‘post_tag’ ), // Example: you might want custom taxonomies here

‘hierarchical’ => false,

‘public’ => true,

‘show_ui’ => true,

‘show_in_menu’ => true,

‘menu_position’ => 5,

‘menu_icon’ => ‘dashicons-portfolio’, // Change this to a relevant Dashicon

‘show_in_admin_bar’ => true,

‘show_in_nav_menus’ => true,

‘can_export’ => true,

‘has_archive’ => true,

‘exclude_from_search’ => false,

‘publicly_queryable’ => true,

‘capability_type’ => ‘post’,

‘query_var’ => true,

‘rewrite’ => array( ‘slug’ => ‘projects’ ),

// The REST API, Gutenberg, and Archive support settings will be added below.

);

register_post_type( ‘project’, $args );

}

add_action( ‘init’, ‘register_my_custom_post_type’ );

“`

This is a solid starting point. We’ve defined labels for a “Project” post type, set up basic supports like title and editor, and enabled some default taxonomies. Note the textdomain usage for translation readiness – very important. The real magic for our specific requirements happens in the next sections.

Understanding Labels and Supports

The labels array is crucial for user experience. These are all the strings WordPress displays in the admin area related to your custom post type. Taking the time to fill these out makes your post type feel like a native part of WordPress.

The supports array dictates what features are available when editing this post type. title, editor, thumbnail (for featured image), excerpt, and custom-fields are common and highly recommended. revisions is also great for content recovery.

If you’re looking to enhance your WordPress development skills, particularly in creating custom post types with full REST API, Gutenberg, and archive support, you might find it beneficial to explore related topics. For instance, understanding how to migrate your WordPress site between servers can be crucial when implementing new features or customizations. A helpful resource on this subject is an article that discusses the process of migrating from one CyberPanel server to another, which can provide insights into maintaining your custom setups during transitions. You can read more about it in this article: Migrating to Another Server with CyberPanel.

Enabling REST API Support

For your custom post type to be accessible via the WordPress REST API, you need to explicitly enable it. This is done with a couple of specific arguments in your register_post_type function’s $args array.

“`php

// … inside your $args array …

‘show_in_rest’ => true, // This is the key for REST API

‘rest_base’ => ‘projects’, // Optional: customize the API endpoint slug

‘rest_controller_class’ => ‘WP_REST_Posts_Controller’, // Usually this default is fine

// …

“`

show_in_rest Explained

Setting show_in_rest to true is the primary switch. Without this, your custom post type won’t be exposed through the REST API, and by extension, Gutenberg won’t be able to interact with it properly for block management and so on.

rest_base for Endpoint Clarity

The rest_base argument is optional but highly recommended. By default, WordPress uses the post type slug (e.g., project) as the REST API endpoint. So, your projects would be available at /wp-json/wp/v2/project. Using rest_base => 'projects' makes the endpoint plural, which is generally more intuitive and common practice for collection endpoints: /wp-json/wp/v2/projects. It’s a small detail that improves usability for anyone consuming your API.

rest_controller_class and Customizing API Behavior

rest_controller_class defaults to WP_REST_Posts_Controller. For most standard custom post types, this is exactly what you want. It provides all the standard CRUD (Create, Read, Update, Delete) operations you’d expect from a post type.

If you ever need to heavily customize how your custom post type interacts with the REST API – adding unique fields, altering permissions, or changing the data structure – you would create your own custom REST controller class and specify it here. But for full basic REST API support, the default is perfect.

Integrating with Gutenberg Editor

Gutenberg, the block editor, relies heavily on the REST API. If you’ve enabled show_in_rest, you’re already most of the way there. However, there are a few other arguments that specifically enhance the Gutenberg experience.

“`php

// … inside your $args array …

// Already set: ‘show_in_rest’ => true,

‘supports’ => array( ‘title’, ‘editor’, ‘thumbnail’, ‘excerpt’, ‘author’, ‘custom-fields’, ‘revisions’ ), // ‘editor’ is crucial for Gutenberg

‘public’ => true, // Often needed for Gutenberg as it uses the client-side API

‘show_ui’ => true, // Also needed for the admin UI

‘show_in_nav_menus’ => true, // If you want to link to these in menus

‘show_in_admin_bar’ => true, // If you want quick links in the admin bar

‘template’ => array(

array( ‘core/paragraph’, array( ‘placeholder’ => ‘Add your project description here…’ ) ),

array( ‘core/image’, array( ‘align’ => ‘left’ ) ),

), // Pre-defined block layout for new posts

‘template_lock’ => ‘all’, // Or ‘insert’, prevents adding/removing all blocks

// …

“`

The editor Support Argument

The editor support flag in the supports array is non-negotiable for Gutenberg. This is what tells WordPress to use the block editor for your custom post type. If you omit this, you’ll get the old Classic Editor experience (or no editor at all).

show_in_rest and public – The Core Duo

As mentioned, show_in_rest is vital. Alongside it, public => true and show_ui => true ensure that your post type is fully accessible and editable in the WordPress admin interface. Gutenberg relies on these being true.

template and template_lock for Block Editor Control

This is where you can get really creative and opinionated about the editing experience.

Defining a Default Block Layout with template

The template argument allows you to define a default initial block layout for new posts of this custom type. In the example above, we’re saying: “When someone creates a new Project, start with a paragraph block (with some placeholder text) followed by an image block aligned left.” This is fantastic for guiding content creators and ensures consistency.

You can specify any core block, and even third-party blocks if they are registered. The format is an array of arrays, where each inner array represents a block, with the first element being the block name (e.g., 'core/paragraph') and the second, an optional attributes array.

Imposing Structure with template_lock

template_lock works in conjunction with template. It gives you control over how much content creators can deviate from your defined template:

  • 'all': This is the most restrictive. Users can only edit the content of existing blocks within the template; they cannot add, remove, or rearrange blocks. This is great for highly structured content types.
  • 'insert': Users can edit existing blocks and rearrange them, but they cannot add new blocks outside of the defined template, nor can they remove blocks.
  • false: (Default) No locking. Users can add, remove, and rearrange blocks freely, even if you provided an initial template.

Choosing the right template_lock depends on how much freedom you want to give your content editors versus how much consistency you need for your post type.

Enabling Archive Support

Archive pages are a core feature of WordPress, allowing you to display a list of all posts (or your custom post type items) sorted by date, category, tag, or other criteria. Getting this working for a custom post type is straightforward with the has_archive argument.

“`php

// … inside your $args array …

‘has_archive’ => true, // This enables the archive page

‘rewrite’ => array( ‘slug’ => ‘projects’, ‘with_front’ => true ), // Important for URLs

// …

“`

has_archive – The Archive Switch

Setting has_archive to true is the primary step. This tells WordPress to generate an archive page for your custom post type. By default, this page will be accessible at a URL based on your post type slug (e.g., /project/ or /projects/ if you use rest_base and rewrite accordingly).

rewrite Arguments for Clean URLs

The rewrite argument gives you fine-grained control over the permalink structure for your custom post type’s single posts and archive pages.

rewrite['slug'] for Permalink Base

'slug' => 'projects' is what sets the base for your URLs. A single project might be /projects/my-awesome-project/, and the archive would be /projects/. Make sure this is singular or plural depending on your preference and matches rest_base if you set it. Consistency here makes things easy to remember.

rewrite['with_front'] for Permalink Prefix

'with_front' => true (the default) means that if you have a custom permalink structure like /blog/%postname%/, your custom post type’s URLs will respect that prefix. So, a project might become /blog/projects/my-awesome-project/. If you set it to false, the custom post type slugs will not be prefixed, e.g., /projects/my-awesome-project/ even if blog is in your regular posts URLs. Most of the time, true is what you want for consistency within your site’s permalink structure.

Creating an Archive Template

Once has_archive is true, WordPress looks for a specific template file to render this archive page. The standard template hierarchy applies here:

  1. archive-{post_type_slug}.php: (e.g., archive-project.php) This is the most specific. WordPress will look for this first.
  2. archive.php: If archive-{post_type_slug}.php doesn’t exist, it will fall back to the general archive template.
  3. index.php: As a last resort, it will use your theme’s main index.php file.

To provide a unique look and feel for your custom post type’s archive, create a file named archive-project.php (replacing project with your post type slug) in your theme’s root directory. Inside this file, you’ll use the standard WordPress loop to display your posts.

A basic archive-project.php might look something like this:

“`php

/**

  • The template for displaying Project archive pages

*

  • @link https://developer.wordpress.org/themes/basics/template-hierarchy/

*/

get_header(); ?>

‘ ); ?>

/ Start the Loop /

while ( have_posts() ) :

the_post();

/*

  • Include the Post-Type-specific template for the content.
  • If you want a specific display for this archive, you can create
  • content-project.php and use get_template_part( ‘template-parts/content’, get_post_type() );
  • or simply put the content directly here.

*/

get_template_part( ‘template-parts/content’, get_post_type() );

endwhile;

the_posts_navigation();

else :

get_template_part( ‘template-parts/content’, ‘none’ );

endif;

?>

get_sidebar();

get_footer();

“`

Remember to replace template-parts/content with your theme’s appropriate path, or simply inline the content display right there.

Permalinks Flush After Registration

Whenever you add or change rewrite rules or enable has_archive for a custom post type, you must flush your permalinks. The easiest way to do this is to simply visit Settings > Permalinks in your WordPress admin and click “Save Changes” (you don’t even need to change anything). This refreshes WordPress’s internal rewrite rules and ensures your new URLs work. If you forget this step, you’ll likely encounter 404 errors on your archive page.

If you’re looking to enhance your WordPress development skills, you might find it helpful to explore a related article that discusses the intricacies of creating custom post types with full REST API support, Gutenberg integration, and archive capabilities. This resource provides valuable insights and practical examples that can complement your understanding of the topic. For more information, you can check out this helpful guide that delves deeper into the subject.

Crafting Robust Custom Post Type Capabilities

Beyond the basic settings, thinking about capabilities helps make your custom post type professional and secure. By default, capability_type is 'post', which means anyone who can edit or publish regular posts can also interact with your custom post type. But what if you want more granular control?

“`php

// … inside your $args array …

‘capability_type’ => ‘project’, // Or ‘book’, ‘product’, etc.

‘map_meta_cap’ => true, // Highly recommended when using custom capabilities

‘capabilities’ => array(

‘edit_post’ => ‘edit_project’,

‘read_post’ => ‘read_project’,

‘delete_post’ => ‘delete_project’,

‘edit_posts’ => ‘edit_projects’,

‘edit_others_posts’ => ‘edit_others_projects’,

‘publish_posts’ => ‘publish_projects’,

‘read_private_posts’ => ‘read_private_projects’,

‘create_posts’ => ‘create_projects’,

),

// …

“`

capability_type Explained

When you set capability_type to a custom string (e.g., 'project'), you’re essentially saying, “I want specific capabilities named ‘edit_project’, ‘publish_projects’, etc., instead of the default ‘edit_posts’, ‘publish_posts’.”

This is powerful because it allows you to define roles that can only manage projects, or roles that can manage projects but not regular posts, for example. Without a custom capability_type, you’re stuck with whatever permissions are granted for the default post type.

map_meta_cap is Your Best Friend

Setting map_meta_cap to true is critical when you use a custom capability_type. This argument tells WordPress to map its default “meta capabilities” (like edit_post, read_post, delete_post) to your new primitive capabilities (like edit_project, read_project, delete_project).

Without map_meta_cap set to true, you’d have to create a lot of extra capabilities and WordPress wouldn’t know how to translate, for instance, “Can this user edit_post with ID X?” into your edit_project capability. It simplifies permission checks significantly.

Defining Specific capabilities

The capabilities array explicitly maps standard WordPress capabilities to your custom ones. This isn’t strictly necessary if you only change capability_type to a specific string, as WordPress will mostly intelligently guess the plural forms. However, explicitly defining them makes your code clearer, and gives you full control if you ever need to deviate from the standard pluralization.

For example, edit_post (a meta capability for a single post) maps to edit_project (a primitive capability). edit_posts (a general capability for all posts of that type) maps to edit_projects.

Adding Custom Capabilities to Roles

After defining your custom capabilities in register_post_type, you then need to grant these capabilities to specific user roles. This is usually done programmatically in your plugin’s activation hook or using a plugin like “User Role Editor.”

Example of adding capabilities to the ‘editor’ role on plugin activation:

“`php

function my_custom_post_type_activation() {

// Get the editor role

$editor_role = get_role( ‘editor’ );

// If the role exists, add the custom capabilities

if ( null !== $editor_role ) {

$editor_role->add_cap( ‘edit_project’ );

$editor_role->add_cap( ‘read_project’ );

$editor_role->add_cap( ‘delete_project’ );

$editor_role->add_cap( ‘edit_projects’ );

$editor_role->add_cap( ‘edit_others_projects’ );

$editor_role->add_cap( ‘publish_projects’ );

$editor_role->add_cap( ‘read_private_projects’ );

$editor_role->add_cap( ‘create_projects’ );

}

// Always flush rewrite rules on activation for new post types

flush_rewrite_rules();

}

register_activation_hook( __FILE__, ‘my_custom_post_type_activation’ );

function my_custom_post_type_deactivation() {

// Remove the custom capabilities from the editor role on deactivation

$editor_role = get_role( ‘editor’ );

if ( null !== $editor_role ) {

$editor_role->remove_cap( ‘edit_project’ );

$editor_role->remove_cap( ‘read_project’ );

$editor_role->remove_cap( ‘delete_project’ );

$editor_role->remove_cap( ‘edit_projects’ );

$editor_role->remove_cap( ‘edit_others_projects’ );

$editor_role->remove_cap( ‘publish_projects’ );

$editor_role->remove_cap( ‘read_private_projects’ );

$editor_role->remove_cap( ‘create_projects’ );

}

flush_rewrite_rules();

}

register_deactivation_hook( __FILE__, ‘my_custom_post_type_deactivation’ );

“`

This ensures your custom post type permissions are properly managed when your plugin is activated and cleaned up when it’s deactivated.

The Complete Picture: Assembling Your Custom Post Type

Bringing everything together, here’s what your final register_post_type call might look like, incorporating all the features we’ve discussed for full REST API, Gutenberg, and archive support, plus robust capabilities.

“`php

/**

  • Plugin Name: My Custom Post Types
  • Description: Registers a custom post type ‘Project’ with full REST API, Gutenberg, and archive support.
  • Version: 1.0
  • Author: Your Name

*/

if ( ! defined( ‘ABSPATH’ ) ) {

exit; // Exit if accessed directly.

}

function register_my_custom_project_type() {

$labels = array(

‘name’ => _x( ‘Projects’, ‘Post Type General Name’, ‘textdomain’ ),

‘singular_name’ => _x( ‘Project’, ‘Post Type Singular Name’, ‘textdomain’ ),

‘menu_name’ => __( ‘Projects’, ‘textdomain’ ),

‘name_admin_bar’ => __( ‘Project’, ‘textdomain’ ),

‘archives’ => __( ‘Project Archives’, ‘textdomain’ ),

‘attributes’ => __( ‘Project Attributes’, ‘textdomain’ ),

‘parent_item_colon’ => __( ‘Parent Project:’, ‘textdomain’ ),

‘all_items’ => __( ‘All Projects’, ‘textdomain’ ),

‘add_new_item’ => __( ‘Add New Project’, ‘textdomain’ ),

‘add_new’ => __( ‘Add New’, ‘textdomain’ ),

‘new_item’ => __( ‘New Project’, ‘textdomain’ ),

‘edit_item’ => __( ‘Edit Project’, ‘textdomain’ ),

‘update_item’ => __( ‘Update Project’, ‘textdomain’ ),

‘view_item’ => __( ‘View Project’, ‘textdomain’ ),

‘view_items’ => __( ‘View Projects’, ‘textdomain’ ),

‘search_items’ => __( ‘Search Projects’, ‘textdomain’ ),

‘not_found’ => __( ‘No projects found’, ‘textdomain’ ),

‘not_found_in_trash’ => __( ‘No projects found in Trash’, ‘textdomain’ ),

‘featured_image’ => __( ‘Featured Image’, ‘textdomain’ ),

‘set_featured_image’ => __( ‘Set featured image’, ‘textdomain’ ),

‘remove_featured_image’ => __( ‘Remove featured image’, ‘textdomain’ ),

‘use_featured_image’ => __( ‘Use as featured image’, ‘textdomain’ ),

‘insert_into_item’ => __( ‘Insert into project’, ‘textdomain’ ),

‘uploaded_to_this_item’ => __( ‘Uploaded to this project’, ‘textdomain’ ),

‘items_list’ => __( ‘Projects list’, ‘textdomain’ ),

‘items_list_navigation’ => __( ‘Projects list navigation’, ‘textdomain’ ),

‘filter_items_list’ => __( ‘Filter projects list’, ‘textdomain’ ),

);

$args = array(

‘label’ => _x( ‘Project’, ‘Post Type Label’, ‘textdomain’ ),

‘description’ => __( ‘A post type for managing client projects.’, ‘textdomain’ ),

‘labels’ => $labels,

‘supports’ => array( ‘title’, ‘editor’, ‘thumbnail’, ‘excerpt’, ‘author’, ‘custom-fields’, ‘revisions’ ),

‘taxonomies’ => array( ‘category’, ‘post_tag’ ), // Use built-in or register custom taxonomies here

‘hierarchical’ => false, // Set to true if you want ‘parent’ relationships (like pages)

‘public’ => true,

‘show_ui’ => true,

‘show_in_menu’ => true,

‘menu_position’ => 5,

‘menu_icon’ => ‘dashicons-portfolio’,

‘show_in_admin_bar’ => true,

‘show_in_nav_menus’ => true,

‘can_export’ => true,

‘has_archive’ => true, // Enable archive page

‘exclude_from_search’ => false,

‘publicly_queryable’ => true,

‘rewrite’ => array( ‘slug’ => ‘projects’, ‘with_front’ => true ),

// REST API Support

‘show_in_rest’ => true,

‘rest_base’ => ‘projects’,

‘rest_controller_class’ => ‘WP_REST_Posts_Controller’,

// Gutenberg Integration Specifics

‘template’ => array(

array( ‘core/paragraph’, array( ‘placeholder’ => ‘Add a short overview of your project here…’ ) ),

array( ‘core/heading’, array( ‘level’ => 2, ‘placeholder’ => ‘Problem Statement’ ) ),

array( ‘core/paragraph’ ),

array( ‘core/heading’, array( ‘level’ => 2, ‘placeholder’ => ‘Solution Provided’ ) ),

array( ‘core/gallery’ ),

array( ‘core/paragraph’ ),

),

‘template_lock’ => false, // Set to ‘all’ or ‘insert’ to restrict editing freedom

// Custom Capability Types for fine-grained permissions

‘capability_type’ => ‘project’,

‘map_meta_cap’ => true,

‘capabilities’ => array(

‘edit_post’ => ‘edit_project’,

‘read_post’ => ‘read_project’,

‘delete_post’ => ‘delete_project’,

‘edit_posts’ => ‘edit_projects’,

‘edit_others_posts’ => ‘edit_others_projects’,

‘publish_posts’ => ‘publish_projects’,

‘read_private_posts’ => ‘read_private_projects’,

‘create_posts’ => ‘create_projects’,

),

);

register_post_type( ‘project’, $args );

}

add_action( ‘init’, ‘register_my_custom_project_type’ );

/**

  • Handle activation/deactivation hooks for roles and permalinks.

*/

function my_custom_project_type_activation() {

// Register the post type to ensure it’s known when flushing rewrites

register_my_custom_project_type();

// Add capabilities to roles

$roles_to_modify = array( ‘administrator’, ‘editor’ ); // Adjust as needed

foreach ( $roles_to_modify as $role_name ) {

$role = get_role( $role_name );

if ( null !== $role ) {

$role->add_cap( ‘edit_project’ );

$role->add_cap( ‘read_project’ );

$role->add_cap( ‘delete_project’ );

$role->add_cap( ‘edit_projects’ );

$role->add_cap( ‘edit_others_projects’ );

$role->add_cap( ‘publish_projects’ );

$role->add_cap( ‘read_private_projects’ );

$role->add_cap( ‘create_projects’ );

}

}

flush_rewrite_rules();

}

register_activation_hook( __FILE__, ‘my_custom_project_type_activation’ );

function my_custom_project_type_deactivation() {

// Clean up capabilities from roles

$roles_to_modify = array( ‘administrator’, ‘editor’ ); // Must match activation

foreach ( $roles_to_modify as $role_name ) {

$role = get_role( $role_name );

if ( null !== $role ) {

$role->remove_cap( ‘edit_project’ );

$role->remove_cap( ‘read_project’ );

$role->remove_cap( ‘delete_project’ );

$role->remove_cap( ‘edit_projects’ );

$role->remove_cap( ‘edit_others_projects’ );

$role->remove_cap( ‘publish_projects’ );

$role->remove_cap( ‘read_private_projects’ );

$role->remove_cap( ‘create_projects’ );

}

}

flush_rewrite_rules();

}

register_deactivation_hook( __FILE__, ‘my_custom_project_type_deactivation’ );

“`

This comprehensive example gives you a really powerful and flexible custom post type ready for modern WordPress development. Remember to change textdomain to your actual text domain for translation, and 'project' to your chosen post type slug.