WordPress taxonomy terms — categories, tags, and custom taxonomy terms — can carry their own metadata in exactly the same way posts carry post meta. Since WordPress 4.4, add_term_meta(), get_term_meta(), update_term_meta(), and delete_term_meta() store arbitrary data in the wp_termmeta table. Before 4.4, developers stored term-level data in wp_options with a mangled key, which was fragile and hard to query. Term meta is the right place for information that belongs to a term rather than to posts that use the term: a category banner image, a product category sort order, a tag colour code, or a brand logo URL. Combined with register_term_meta(), term meta can also be exposed through the REST API and sanitized automatically.
Problem: A WooCommerce product category needs a custom banner image and a sort order integer that controls its position on the shop page. The data should appear as extra fields on the category edit screen and be available in templates.
Solution: Register the meta with register_term_meta(), render the fields using the product_cat_edit_form_fields action, save with the edited_product_cat hook, and read in templates with get_term_meta().
<?php
// ── Register term meta ────────────────────────────────────────────────
add_action( 'init', function () {
register_term_meta( 'product_cat', 'banner_image_id', [
'type' => 'integer',
'single' => true,
'sanitize_callback' => 'absint',
'show_in_rest' => true,
] );
register_term_meta( 'product_cat', 'sort_order', [
'type' => 'integer',
'single' => true,
'sanitize_callback' => 'absint',
'default' => 0,
'show_in_rest' => true,
] );
} );
// ── Add fields to the term edit form ─────────────────────────────────
// product_cat_edit_form_fields fires on the Edit Category screen
// For "Add new" form: product_cat_add_form_fields
add_action( 'product_cat_edit_form_fields', function ( WP_Term $term ) {
$image_id = (int) get_term_meta( $term->term_id, 'banner_image_id', true );
$sort_order = (int) get_term_meta( $term->term_id, 'sort_order', true );
$image_url = $image_id ? wp_get_attachment_image_url( $image_id, 'medium' ) : '';
wp_nonce_field( 'save_product_cat_meta_' . $term->term_id, '_cat_meta_nonce' );
?>
<tr class="form-field">
<th scope="row"><?php esc_html_e( 'Banner Image ID', 'textdomain' ); ?></th>
<td>
<input type="number" name="banner_image_id"
value="<?php echo esc_attr( $image_id ); ?>" min="0">
<?php if ( $image_url ) : ?>
<br><img src="<?php echo esc_url( $image_url ); ?>" style="max-width:120px;margin-top:6px;">
<?php endif; ?>
</td>
</tr>
<tr class="form-field">
<th scope="row"><?php esc_html_e( 'Sort Order', 'textdomain' ); ?></th>
<td>
<input type="number" name="cat_sort_order"
value="<?php echo esc_attr( $sort_order ); ?>" min="0">
</td>
</tr>
<?php
} );
// ── Save term meta ────────────────────────────────────────────────────
add_action( 'edited_product_cat', function ( int $term_id ) {
if ( ! isset( $_POST['_cat_meta_nonce'] )
|| ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_cat_meta_nonce'] ) ),
'save_product_cat_meta_' . $term_id ) ) {
return;
}
if ( ! current_user_can( 'manage_product_terms' ) ) {
return;
}
update_term_meta( $term_id, 'banner_image_id',
absint( $_POST['banner_image_id'] ?? 0 ) );
update_term_meta( $term_id, 'sort_order',
absint( $_POST['cat_sort_order'] ?? 0 ) );
} );
// ── Read in templates ─────────────────────────────────────────────────
$term = get_queried_object(); // on a taxonomy archive page
if ( $term instanceof WP_Term ) {
$image_id = (int) get_term_meta( $term->term_id, 'banner_image_id', true );
$sort_order = (int) get_term_meta( $term->term_id, 'sort_order', true );
if ( $image_id ) {
echo wp_get_attachment_image( $image_id, 'full', false, [ 'class' => 'cat-banner' ] );
}
}
NOTE: Term meta supports the same $unique flag as post meta — pass true as the fourth argument to add_term_meta() to prevent duplicate keys, or use update_term_meta() which always enforces uniqueness. For the "Add New" form use the {taxonomy}_add_form_fields action (no WP_Term argument) and the created_{taxonomy} save hook. When building admin UI for term meta, always verify a nonce; taxonomy forms do not include one by default, making them a CSRF target without it.