| name | add-post-type |
| description | Register a custom post type (and optionally a taxonomy) in a theme or plugin, following Dekode conventions. |
Add a Custom Post Type
Register a custom post type and its associated taxonomy in a theme's includes/ directory or a dedicated plugin. Covers labels, REST support, block template defaults, and the key-function pattern used across agency projects.
Step 0 — Understand the purpose
Ask the user: "What kind of content will this post type represent?" — a brief description of what it stores and where it will appear on the site.
Based on the description, suggest 2–3 slug options (lowercase with underscores), for example:
Based on your description, here are some suggestions:
| # | Slug | Singular | Plural |
|---|
| 1 | event | Event | Events |
| 2 | case_study | Case Study | Case Studies |
| 3 | project | Project | Projects |
Which would you like to use, or do you have your own preference?
- The user may pick one of the suggestions or provide their own.
- Confirm the final choice before proceeding.
Step 1 — Gather remaining information
| Input | Required | Notes |
|---|
| Post type slug | Yes | Lowercase with underscores, max 20 chars — e.g. event, case_study |
| Singular name | Yes | English, e.g. Event |
| Plural name | Yes | English, e.g. Events |
| Rewrite slug | Yes | URL slug, e.g. events |
| Location | Yes | Theme includes/ or a plugin's src/ / inc/ — identify the exact package |
| Namespace | Yes | Check the surrounding package, e.g. ProjectBase\Theme\PostTypes |
| Needs taxonomy? | Yes | Should a category taxonomy be registered alongside this post type? (yes/no) |
| Taxonomy slug | If yes | e.g. event_category |
| Taxonomy singular name | If yes | English, e.g. Category |
| Taxonomy plural name | If yes | English, e.g. Categories |
| Taxonomy rewrite slug | If yes | URL slug, e.g. event-categories |
Scan the target package for an existing post type file to confirm the namespace and file naming pattern used by the project.
Step 2 — Create the file
- Theme:
includes/post-type-{slug}.php
- Plugin:
inc/post-type-{slug}.php (or src/post-type-{slug}.php — match the project convention)
One file per post type, including its taxonomy registration.
Step 3 — Write the post type registration
<?php
declare( strict_types=1 );
namespace {PhpNamespace};
function get_{slug}_key(): string {
return '{slug}';
}
function register_{slug}(): void {
$labels = [
'name' => _x( '{PluralName}', 'Post type general name', '{text-domain}' ),
'singular_name' => _x( '{SingularName}', 'Post type singular name', '{text-domain}' ),
'add_new' => __( 'Add new', '{text-domain}' ),
'add_new_item' => __( 'Add new {singularName}', '{text-domain}' ),
'edit_item' => __( 'Edit {singularName}', '{text-domain}' ),
'new_item' => __( 'New {singularName}', '{text-domain}' ),
'view_item' => __( 'View {singularName}', '{text-domain}' ),
'view_items' => __( 'View {pluralName}', '{text-domain}' ),
'search_items' => __( 'Search {pluralName}', '{text-domain}' ),
'not_found' => __( 'No {pluralName} found.', '{text-domain}' ),
'not_found_in_trash' => __( 'No {pluralName} found in trash.', '{text-domain}' ),
'all_items' => __( 'All {pluralName}', '{text-domain}' ),
'menu_name' => __( '{PluralName}', '{text-domain}' ),
'name_admin_bar' => __( '{SingularName}', '{text-domain}' ),
];
\register_post_type(
get_{slug}_key(),
[
'labels' => $labels,
'public' => true,
'show_in_rest' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-admin-post',
'rewrite' => [
'slug' => '{rewrite-slug}',
'with_front' => false,
],
'supports' => [ 'title', 'editor', 'thumbnail', 'revisions', 'custom-fields' ],
'taxonomies' => [],
]
);
}
\add_action( 'init', __NAMESPACE__ . '\\register_{slug}' );
- Choose an appropriate
menu_icon dashicon for the content type.
- Adjust
supports based on what the post type needs (remove unused entries).
- If a taxonomy is registered, add it to the
'taxonomies' array using get_{slug}_key() . '_category'.
Step 4 — Add the taxonomy (if needed)
Append to the same file, after the post type registration:
function register_{slug}_category(): void {
$labels = [
'name' => _x( '{TaxPluralName}', 'Taxonomy general name', '{text-domain}' ),
'singular_name' => _x( '{TaxSingularName}', 'Taxonomy singular name', '{text-domain}' ),
'search_items' => __( 'Search {taxPluralName}', '{text-domain}' ),
'all_items' => __( 'All {taxPluralName}', '{text-domain}' ),
'edit_item' => __( 'Edit {taxSingularName}', '{text-domain}' ),
'update_item' => __( 'Update {taxSingularName}', '{text-domain}' ),
'add_new_item' => __( 'Add new {taxSingularName}', '{text-domain}' ),
'new_item_name' => __( 'New {taxSingularName} name', '{text-domain}' ),
'menu_name' => __( '{TaxPluralName}', '{text-domain}' ),
];
\register_taxonomy(
get_{slug}_key() . '_category',
[ get_{slug}_key() ],
[
'labels' => $labels,
'hierarchical' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => '{tax-rewrite-slug}' ],
]
);
}
\add_action( 'init', __NAMESPACE__ . '\\register_{slug}_category' );
Step 5 — Verify auto-loading
- Theme
includes/: confirm functions.php loads files via glob (e.g. glob( __DIR__ . '/includes/*.php' )). If the glob is present, no manual require is needed.
- Plugin: if the plugin does not use glob auto-loading, require the file explicitly from the plugin entry point.
Step 6 — Add a default block template (optional)
For post types that should open with a preset editor layout, add inside the register_{slug}() args array:
'template' => [
[ 'core/post-title' ],
[ 'core/post-featured-image' ],
[ 'core/post-content' ],
],
'template_lock' => false,
Step 7 — Lint
composer lint
composer lint-fix
composer lint
Conventions
- Key function: always define
get_{slug}_key(): string and use it everywhere — never hardcode the post type string in multiple places
show_in_rest: true: always — required for the block editor and REST API access
- Labels: write all strings in English; use
_x() for name and singular_name to provide translator context — translations are handled via .pot/.po files
- Rewrite slug: use whatever URL slug makes sense for the project (can be English or localised)
- Taxonomy coupling: always reference the post type via
get_{slug}_key(), not the raw string
- No hardcoded strings: post type and taxonomy keys must only appear in their respective
get_*_key() functions
- File naming:
post-type-{slug}.php — one file per post type (including its taxonomy)
Output
A single post-type-{slug}.php file containing the post type and taxonomy registration, both hooked to init. The post type is visible in the WordPress admin, accessible via the REST API, and auto-loaded by the theme or plugin. Lint passes clean.