WordPress Custom Post Type Example: Register a Production-Ready CPT Plugin
A practical custom post type plugin example covering register_post_type(), REST visibility, archives, rewrite rules, and production validation checks.
Published
April 16, 2026
Reading Time
5 min read
Updated
April 16, 2026

Implementation Notes
Extension points, code paths, and implementation choices that should survive contact with production.
Best For
WordPress developers, agencies, and technical teams building custom plugin or theme functionality with cleaner operational defaults.
Primary Topics
Editorial Focus
Build Pattern: Extension points, code paths, and implementation choices that should survive contact with production. Updated on April 16, 2026.
Full Report
Last reviewed: April 16, 2026
Searches for “WordPress custom post type example” usually come from developers who need more than a reference page. They need a working snippet, a decision about whether the code belongs in a plugin or the theme, and enough production context to avoid creating a content model that breaks after the next redesign.
This guide shows a plugin-first custom post type implementation for a real content type. It covers register_post_type(), REST API visibility, archives, menu labels, rewrite rules, supported editor fields, and the checks to run before the post type is used on a production site.
Who this is for
This article is for WordPress developers, agencies, and site operators who need to add structured content such as case studies, reports, jobs, documentation entries, events, or resources. The goal is not to paste a random snippet into functions.php. The goal is to create a stable content model that survives theme changes and can be maintained like application code.
Why custom post type examples are searched so often
Custom post types sit at the center of many WordPress projects because they turn WordPress from a simple blog into a structured publishing system. The common search intent is practical:
- how to register a custom post type;
- where the code should live;
- how to make the content available in the REST API;
- how to avoid permalink and rewrite problems; and
- which labels, supports, archives, and capabilities matter.
Those are implementation questions, not just documentation questions. A production-ready example should answer them together.
Use a plugin for the content model
If the custom post type represents business content, put it in a plugin. A theme can change how content looks, but it should not own whether the content type exists. If a site has “Resources” or “Case Studies”, that content should not disappear because a team switches themes.
A theme-owned custom post type is acceptable only when the content exists purely for that theme and should intentionally disappear with it. That is rare in production. Plugin-first is the safer default.
Production-ready custom post type plugin example
Create a directory such as wp-content/plugins/vulnwp-resources, then create a PHP file named vulnwp-resources.php.
<?php
/**
* Plugin Name: VulnWP Resources Content Type
* Description: Registers a Resources custom post type for structured editorial content.
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'init', 'vulnwp_register_resource_post_type' );
function vulnwp_register_resource_post_type() {
$labels = array(
'name' => __( 'Resources', 'vulnwp' ),
'singular_name' => __( 'Resource', 'vulnwp' ),
'menu_name' => __( 'Resources', 'vulnwp' ),
'name_admin_bar' => __( 'Resource', 'vulnwp' ),
'add_new' => __( 'Add New', 'vulnwp' ),
'add_new_item' => __( 'Add New Resource', 'vulnwp' ),
'edit_item' => __( 'Edit Resource', 'vulnwp' ),
'new_item' => __( 'New Resource', 'vulnwp' ),
'view_item' => __( 'View Resource', 'vulnwp' ),
'search_items' => __( 'Search Resources', 'vulnwp' ),
'not_found' => __( 'No resources found.', 'vulnwp' ),
'not_found_in_trash' => __( 'No resources found in Trash.', 'vulnwp' ),
'all_items' => __( 'All Resources', 'vulnwp' ),
'archives' => __( 'Resource Archives', 'vulnwp' ),
'attributes' => __( 'Resource Attributes', 'vulnwp' ),
'insert_into_item' => __( 'Insert into resource', 'vulnwp' ),
'uploaded_to_this_item' => __( 'Uploaded to this resource', 'vulnwp' ),
);
register_post_type(
'vulnwp_resource',
array(
'labels' => $labels,
'public' => true,
'show_in_rest' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'resources' ),
'menu_icon' => 'dashicons-media-document',
'menu_position' => 20,
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'revisions' ),
'taxonomies' => array( 'category', 'post_tag' ),
'capability_type' => 'post',
'map_meta_cap' => true,
)
);
}
What the example does well
It registers on init. Custom post types should be registered during WordPress initialization so rewrite rules, admin menus, queries, and REST routes can see the content type consistently.
It uses a prefixed post type key. The key vulnwp_resource avoids generic names such as resource that could collide with another plugin or future code.
It enables the block editor and REST API. show_in_rest makes the custom post type available to the block editor and REST API. That matters for modern WordPress and for headless projects.
It includes archives and a stable rewrite slug. If resources should have an archive page, has_archive and rewrite need to be deliberate. Do not let public URLs happen accidentally.
It supports thumbnails, excerpts, and revisions. These features are practical for editorial workflows, previews, cards, search snippets, and rollback.
Activation and rewrite rules
After adding or changing a custom post type, flush rewrite rules once. Do not call flush_rewrite_rules() on every request because it is expensive and unnecessary. The simplest safe path is to activate the plugin, then visit Settings – Permalinks and save once.
If you need activation handling in the plugin, register the post type inside the activation callback before flushing:
register_activation_hook( __FILE__, 'vulnwp_resources_activate' );
function vulnwp_resources_activate() {
vulnwp_register_resource_post_type();
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'vulnwp_resources_deactivate' );
function vulnwp_resources_deactivate() {
flush_rewrite_rules();
}
Production checklist before publishing content
- Confirm the post type key is prefixed and will not collide with another component.
- Decide whether the content type should be public, private, or admin-only.
- Confirm whether the post type should expose REST responses to unauthenticated users.
- Validate archive URLs, single URLs, and canonical behavior after saving permalinks.
- Add templates or frontend rendering only after the data model is stable.
- Check whether editorial teams need revisions, excerpts, featured images, or taxonomy filters.
- Document whether the post type is owned by a plugin, theme, or platform team.
Common mistakes
- Putting durable content models in
functions.php. This creates migration risk when the theme changes. - Using a generic post type key. Names like
book,project, orresourcecan collide. - Forgetting
show_in_rest. The content type may not behave as expected in the block editor or API-driven frontend. - Flushing rewrite rules on every request. Flush once on activation or manually through permalink settings.
- Not deciding whether the archive should exist. Public URLs should be intentional.
How this connects to headless WordPress
Headless sites often rely on custom post types because the frontend needs structured data. A well-designed post type is easier to query, cache, preview, and expose through REST. If you are building a headless stack, pair this with our Headless WordPress Security Checklist and the REST API exposure checklist.
Quick validation with WP-CLI and REST
After activation, confirm that WordPress can see the post type and that the public API behavior matches your intent. If the content type is supposed to be headless-accessible, the REST response should exist. If it is not supposed to be public, the response should be blocked or omitted by design.
# Confirm the post type is registered.
wp post-type list --fields=name,public,show_in_rest,has_archive
# Create a draft resource for testing.
wp post create --post_type=vulnwp_resource --post_status=draft --post_title="CPT Smoke Test"
# Inspect the REST collection if show_in_rest is enabled.
curl -s https://example.com/wp-json/wp/v2/vulnwp_resource
That last command should not be treated as a formality. It verifies the API surface your frontend or integration will actually consume. For production work, test with a published item, a draft item, a logged-out request, and an authenticated editor request so permissions and visibility are clear before content entry begins.


