Register a custom post type (and optionally a taxonomy) in a theme or plugin, following Dekode conventions.
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.
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 eventEvent 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?
| 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.
includes/post-type-{slug}.phpinc/post-type-{slug}.php (or src/post-type-{slug}.php — match the project convention)One file per post type, including its taxonomy registration.
<?php
/**
* Post type: {Post Type Name}
*
* @package {PhpNamespace}
*/
declare( strict_types=1 );
namespace {PhpNamespace};
/**
* Get the post type key.
* Use this function everywhere instead of hardcoding the string.
*/
function get_{slug}_key(): string {
return '{slug}';
}
/**
* Register the post type.
*/
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}' );
menu_icon dashicon for the content type.supports based on what the post type needs (remove unused entries).'taxonomies' array using get_{slug}_key() . '_category'.Append to the same file, after the post type registration:
/**
* Register the {slug} category taxonomy.
*/
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' );
includes/: confirm functions.php loads files via glob (e.g. glob( __DIR__ . '/includes/*.php' )). If the glob is present, no manual require is needed.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, // or 'all' to prevent changes
composer lint
composer lint-fix # if needed
composer lint # re-run to confirm clean
get_{slug}_key(): string and use it everywhere — never hardcode the post type string in multiple placesshow_in_rest: true: always — required for the block editor and REST API access_x() for name and singular_name to provide translator context — translations are handled via .pot/.po filesget_{slug}_key(), not the raw stringget_*_key() functionspost-type-{slug}.php — one file per post type (including its taxonomy)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.