Custom post type with a linked taxonomy
Register a bookable-room post type with a hierarchical amenity taxonomy, REST support and a safe rewrite flush on theme switch.
WordPressDatabasecpttaxonomyrewriterest
Code
<?php
/**
* Room post type and Amenity taxonomy.
*
* @package Acme_Theme
*/
defined( 'ABSPATH' ) || exit;
function acme_register_room_content(): void {
register_post_type(
'acme_room',
array(
'labels' => array(
'name' => __( 'Rooms', 'acme-theme' ),
'singular_name' => __( 'Room', 'acme-theme' ),
'add_new_item' => __( 'Add new room', 'acme-theme' ),
'all_items' => __( 'All rooms', 'acme-theme' ),
),
'public' => true,
'has_archive' => 'rooms',
'menu_icon' => 'dashicons-admin-home',
'show_in_rest' => true, // Block editor + REST.
'rewrite' => array( 'slug' => 'rooms', 'with_front' => false ),
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'custom-fields' ),
)
);
register_taxonomy(
'acme_amenity',
array( 'acme_room' ),
array(
'labels' => array(
'name' => __( 'Amenities', 'acme-theme' ),
'singular_name' => __( 'Amenity', 'acme-theme' ),
),
'hierarchical' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rewrite' => array( 'slug' => 'amenity' ),
)
);
// Expose a numeric nightly rate on the REST response.
register_post_meta(
'acme_room',
'acme_nightly_rate',
array(
'type' => 'number',
'single' => true,
'show_in_rest' => true,
'sanitize_callback' => 'floatval',
'auth_callback' => static fn(): bool => current_user_can( 'edit_posts' ),
)
);
}
add_action( 'init', 'acme_register_room_content' );
// Flush once, after the types exist, when the theme is activated.
function acme_flush_on_activation(): void {
acme_register_room_content();
flush_rewrite_rules();
}
add_action( 'after_switch_theme', 'acme_flush_on_activation' );
How it works
What it solves
A theme that lists rooms, services or properties needs its own content type, not pages with copied layouts. This snippet registers a post type and a taxonomy that belongs to it, with block-editor support, a clean archive URL and typed meta exposed to the REST API.
How it works
register_post_type() and register_taxonomy() run on init, the only hook where both are safe. The taxonomy is attached to the post type through its object-type array, so the admin gets a filterable column automatically. register_post_meta() declares the nightly rate with a type, a sanitizer and an auth_callback, which is what lets the block editor and REST clients read and write it without a custom endpoint.
On after_switch_theme the types are registered once more and rewrite rules are flushed, so /rooms/ works immediately after activation.
Adapting it
Change the acme_ prefix, the labels and the rooms slug. Add 'show_in_nav_menus' => false for internal types. Hierarchical taxonomies behave like categories (checkbox list); set hierarchical to false for a tag-like input. If the types should survive a theme change, move this file into a small plugin and flush on plugin activation instead.
Pitfalls
- Never call
flush_rewrite_rules()on every request; it is expensive. Activation is enough. - Post type keys are limited to 20 characters and should be prefixed to avoid clashes.
- Without
show_in_restthe block editor falls back to the classic editor for that type. - After renaming the slug, visit Settings, Permalinks and save once.
How to use it
Save as inc/post-types.php and load it from functions.php with require get_template_directory() . '/inc/post-types.php';.
Activate the theme (or save permalinks) once, then add rooms from the new admin menu.
Query them anywhere with new WP_Query( array( 'post_type' => 'acme_room' ) ), or filter by tax_query on acme_amenity.
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.