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 initialtemplate.
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:
archive-{post_type_slug}.php: (e.g.,archive-project.php) This is the most specific. WordPress will look for this first.archive.php: Ifarchive-{post_type_slug}.phpdoesn’t exist, it will fall back to the general archive template.index.php: As a last resort, it will use your theme’s mainindex.phpfile.
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;
?>