Lime & Ledger Pressworks Chapter No. 43

Adding a Flat Taxonomy to ChroniclePress

Vol. 60 ❦ Developers
June 2026

2 min read

This guide explains how to add a new non-hierarchical (flat) taxonomy to ChroniclePress and wire it up to the built-in picker UI. The system is designed so that a Layer or third-party plugin can do this entirely through WordPress filters.

How It Works #

The picker system has three moving parts:

  1. Your taxonomy β€” registered with WordPress, attached to whichever post type(s) you need
  2. A field definition β€” added to a field group via the cp_field_groups filter, using 'type' => 'flat_taxonomy'
  3. CP_Flat_Tax_Picker β€” the core class that handles rendering, REST API, and saving automatically for any field definition with that type

You provide steps 1 and 2. Step 3 requires no work from you.

Step 1 β€” Register Your Taxonomy #

Register your taxonomy as you normally would in WordPress, with two important details:

  • Set 'hierarchical' => false (the picker is designed for flat taxonomies only)
  • Set 'meta_box_cb' => false to suppress the WordPress default tag box (ChroniclePress native picker replaces it)
🐘
PHP
class My_Occupation_Taxonomy {

    public function __construct() {
        add_action( 'init', array( $this, 'register_taxonomy' ), 5 );
    }

    public function register_taxonomy() {
        register_taxonomy(
            'cp_occupation',       // your taxonomy slug
            array(),               // post types added below
            array(
                'hierarchical'      => false,   // REQUIRED: flat only
                'meta_box_cb'       => false,   // REQUIRED: suppress WP default UI
                'labels'            => array(
                    'name'          => __( 'Occupations',      'my-layer' ),
                    'singular_name' => __( 'Occupation',       'my-layer' ),
                    'search_items'  => __( 'Search Occupations','my-layer' ),
                    'all_items'     => __( 'All Occupations',  'my-layer' ),
                    'edit_item'     => __( 'Edit Occupation',  'my-layer' ),
                    'update_item'   => __( 'Update Occupation','my-layer' ),
                    'add_new_item'  => __( 'Add New Occupation','my-layer' ),
                    'new_item_name' => __( 'New Occupation',   'my-layer' ),
                    'not_found'     => __( 'No occupations found.','my-layer' ),
                    'menu_name'     => __( 'Occupations',      'my-layer' ),
                ),
                'show_ui'           => true,
                'show_in_menu'      => true,
                'show_admin_column' => true,
                'query_var'         => true,
                'rewrite'           => array( 'slug' => 'occupation' ),
                'show_in_rest'      => true,
                'rest_base'         => 'occupations',
                'capabilities'      => array(
                    'manage_terms'  => 'manage_categories',
                    'edit_terms'    => 'manage_categories',
                    'delete_terms'  => 'manage_categories',
                    'assign_terms'  => 'edit_posts',
                ),
            )
        );

        // Attach to whichever ChroniclePress post type(s) you need.
        register_taxonomy_for_object_type( 'cp_occupation', 'cp_person' );
    }
}

new My_Occupation_Taxonomy();

Step 2 β€” Add the Field to a Group #

Use the cp_field_groups filter to inject your field into an existing group, or to add a new group of your own.

Option A β€” Inject into an existing group #

This adds an Occupation picker to the Identity & Background group on the People edit screen.

🐘
PHP
add_filter( 'cp_field_groups', function ( $groups, $post_type ) {

    if ( 'cp_person' !== $post_type ) {
        return $groups;
    }

    // Find the 'identity' group and append your field.
    foreach ( $groups as &$group ) {
        if ( 'identity' !== ( $group['id'] ?? '' ) ) {
            continue;
        }
        $group['fields'][] = array(
            'key'         => 'cp_person_occupation',   // the POST key / hidden input name
            'label'       => __( 'Occupation', 'my-layer' ),
            'type'        => 'flat_taxonomy',           // tells ChroniclePress to use the picker
            'taxonomy'    => 'cp_occupation',           // your taxonomy slug
            'placeholder' => __( 'Set occupation…', 'my-layer' ),
            'help'        => __( 'Primary occupation or trade of this person.', 'my-layer' ),
            'span'        => 2,                         // span both columns (optional)
        );
        break;
    }

    return $groups;

}, 10, 2 );

Option B β€” Add a new group of your own #

This creates a dedicated Occupation group on the People edit screen.

🐘
PHP
add_filter( 'cp_field_groups', function ( $groups, $post_type ) {

    if ( 'cp_person' !== $post_type ) {
        return $groups;
    }

    $groups[] = array(
        'id'      => 'occupation',
        'label'   => __( 'Occupation', 'my-layer' ),
        'icon'    => 'dashicons-hammer',
        'columns' => 1,
        'fields'  => array(
            array(
                'key'         => 'cp_person_occupation',
                'label'       => __( 'Occupation', 'my-layer' ),
                'type'        => 'flat_taxonomy',
                'taxonomy'    => 'cp_occupation',
                'placeholder' => __( 'Set occupation…', 'my-layer' ),
                'help'        => __( 'Primary occupation or trade of this person.', 'my-layer' ),
                'span'        => 1,
            ),
        ),
    );

    return $groups;

}, 10, 2 );

That’s everything required. The picker UI renders automatically, the REST endpoints (GET/POST to /wp-json/chroniclepress/v1/flat-terms) are already registered by CP_Flat_Tax_Picker, and saving is handled transparently on save_post.

Step 3 β€” Reading the Saved Value #

The picker saves the selected term ID via wp_set_post_terms(). Read it like any other taxonomy assignment:

🐘
PHP
// Get the selected term object for a person post.
$terms = wp_get_post_terms( $post_id, 'cp_occupation' );

if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
    $occupation = $terms[0]; // WP_Term object
    echo esc_html( $occupation->name ); // e.g. "Blacksmith"
}

Because it is a real WordPress taxonomy, all standard WP functions work:

🐘
PHP
// Query all people with a specific occupation.
$blacksmiths = get_posts( array(
    'post_type'  => 'cp_person',
    'tax_query'  => array(
        array(
            'taxonomy' => 'cp_occupation',
            'field'    => 'slug',
            'terms'    => 'blacksmith',
        ),
    ),
) );

// Get the term ID directly (e.g. for use in featured card fields).
$term_ids = wp_get_post_terms( $post_id, 'cp_occupation', array( 'fields' => 'ids' ) );
$term_id  = ! empty( $term_ids ) ? (int) $term_ids[0] : 0;

Step 4 β€” Exposing the Field to the Featured Card Block (Optional) #

If you want your new taxonomy field to appear as a displayable field in the Featured Card block, hook into cp_featured_card_fields:

🐘
PHP
add_filter( 'cp_featured_card_fields', function ( $map ) {

    $map['cp_person'][] = array(
        'key'        => 'cp_person_occupation_display', // matches the picker's 'key' in get_field_groups
        'label'      => __( 'Occupation', 'my-layer' ),
        'field_type' => 'taxonomy_term',               // passed through $def to your resolver
        'taxonomy'   => 'cp_occupation',
        'default'    => false,
    );

    return $map;
} );
🐘
filename.js
add_filter( 'cp_featured_card_resolve_field', function ( $value, $post_id, $key, $def ) {

    // Only handle our custom field type; pass everything else through unchanged.
    if ( 'taxonomy_term' !== ( $def['field_type'] ?? '' ) ) {
        return $value; // null β†’ let core handle it
    }

    $taxonomy = $def['taxonomy'] ?? '';
    if ( ! $taxonomy ) {
        return '';
    }

    $terms = wp_get_post_terms( $post_id, $taxonomy, array( 'fields' => 'names' ) );

    if ( is_wp_error( $terms ) || empty( $terms ) ) {
        return '';
    }

    return $terms[0]; // returns the term name as a string

}, 10, 4 );

Multiple Pickers on the Same Post Type #

You can add as many flat taxonomy fields as you need. Each must have a unique key (used as the HTML input name). The picker instances are completely independent.

🐘
PHP
add_filter( 'cp_field_groups', function ( $groups, $post_type ) {

    if ( 'cp_person' !== $post_type ) {
        return $groups;
    }

    $groups[] = array(
        'id'      => 'classification',
        'label'   => __( 'Classification', 'my-layer' ),
        'icon'    => 'dashicons-category',
        'columns' => 2,
        'fields'  => array(
            array(
                'key'         => 'cp_person_occupation',
                'label'       => __( 'Occupation', 'my-layer' ),
                'type'        => 'flat_taxonomy',
                'taxonomy'    => 'cp_occupation',
                'placeholder' => __( 'Set occupation…', 'my-layer' ),
            ),
            array(
                'key'         => 'cp_person_social_class',
                'label'       => __( 'Social Class', 'my-layer' ),
                'type'        => 'flat_taxonomy',
                'taxonomy'    => 'cp_social_class',
                'placeholder' => __( 'Set social class…', 'my-layer' ),
            ),
        ),
    );

    return $groups;

}, 10, 2 );

Field Definition Reference #

Every flat_taxonomy field definition supports the following keys:

KeyRequiredTypeDescription
keyβœ…stringUnique input name. Used as the $_POST key and the hidden input’s name attribute. Convention: cp_{posttype}_{fieldname}.
typeβœ…stringMust be 'flat_taxonomy'.
taxonomyβœ…stringThe taxonomy slug passed to register_taxonomy().
labelβ€”stringDisplayed above the picker.
placeholderβ€”stringShown in the trigger when nothing is selected. Defaults to "Select…".
helpβ€”stringSmall grey hint shown below the picker.
spanβ€”intColumn span within the group grid. Set to 2 (or the group’s column count) for full width. The picker auto-spans to full width regardless, but setting span makes the cell width explicit.

REST API Reference #

CP_Flat_Tax_Picker registers two endpoints that the JS picker uses internally. You can also call them directly if needed.

GET /wp-json/chroniclepress/v1/flat-terms #

Returns terms for any registered taxonomy.

ParameterRequiredDescription
taxonomyβœ…Taxonomy slug
qβ€”Search string. Omit to return all terms.
per_pageβ€”Max results (default 100, max 200)
πŸ“‹
JSON
GET /wp-json/chroniclepress/v1/flat-terms?taxonomy=cp_occupation&q=black

[
  { "id": 12, "name": "Blacksmith", "count": 4 },
  { "id": 19, "name": "Blackbird Farmer", "count": 1 }
]

Requires: edit_posts capability.

POST /wp-json/chroniclepress/v1/flat-terms #

Creates a new term. Returns the existing term if the name already exists (case-sensitive match), preventing duplicates.

πŸ“‹
JSON
// Request body
{ "taxonomy": "cp_occupation", "name": "Cooper" }

// Response
{ "id": 23, "name": "Cooper" }

Requires: manage_categories capability.


still looking for something?

REFERENCE
CATALOGUE NO.

L&L 001