Term Meta Generator
Add fields to a taxonomy term: the add and edit forms, the nonce, the save with its capability check, and register_term_meta with a sanitize callback.
<?php
/**
* Extra fields on category terms.
*/
/**
* Registers the meta keys.
*
* This is what types the value and checks permission wherever it is written
* from, not only through the forms below.
*/
function my_plugin_register_term_meta() {
register_term_meta(
'category',
'my_plugin_subtitle',
array(
'type' => 'string',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => 'my_plugin_can_edit_term_meta',
)
);
register_term_meta(
'category',
'my_plugin_featured',
array(
'type' => 'string',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => 'my_plugin_can_edit_term_meta',
)
);
}
add_action( 'init', 'my_plugin_register_term_meta' );
/**
* Decides who may write these keys outside the forms.
*
* @param bool $allowed Current decision.
* @param string $key Meta key.
* @param int $term_id Term ID.
* @return bool
*/
function my_plugin_can_edit_term_meta( $allowed, $key, $term_id ) {
return current_user_can( 'manage_categories' );
}
/**
* The fields on the Add Term form.
*
* This form has no term yet, so there is nothing to read: only defaults.
*/
function my_plugin_term_add_fields() {
wp_nonce_field( 'my_plugin_term_meta', 'my_plugin_nonce' );
echo '<div class="form-field">';
printf(
'<label for="%1$s">%2$s</label><input type="text" id="%1$s" name="%1$s" value="" />',
esc_attr( 'my_plugin_subtitle' ),
esc_html__( 'Subtitle', 'my-plugin' )
);
echo '<p>' . esc_html__( 'Shown under the term name on the archive.', 'my-plugin' ) . '</p>';
echo '</div>';
echo '<div class="form-field">';
printf(
'<label for="%1$s"><input type="checkbox" id="%1$s" name="%1$s" value="1" /> %2$s</label>',
esc_attr( 'my_plugin_featured' ),
esc_html__( 'Feature this term', 'my-plugin' )
);
echo '</div>';
}
add_action( 'category_add_form_fields', 'my_plugin_term_add_fields' );
/**
* The fields on the Edit Term form.
*
* A different markup shape from the add form: this one is a table.
*
* @param WP_Term $term The term being edited.
*/
function my_plugin_term_edit_fields( $term ) {
wp_nonce_field( 'my_plugin_term_meta', 'my_plugin_nonce' );
$value = get_term_meta( $term->term_id, 'my_plugin_subtitle', true );
echo '<tr class="form-field">';
printf(
'<th scope="row"><label for="%1$s">%2$s</label></th>',
esc_attr( 'my_plugin_subtitle' ),
esc_html__( 'Subtitle', 'my-plugin' )
);
echo '<td>';
printf(
'<input type="text" id="%1$s" name="%1$s" value="%2$s" class="regular-text" />',
esc_attr( 'my_plugin_subtitle' ),
esc_attr( $value )
);
echo '<p class="description">' . esc_html__( 'Shown under the term name on the archive.', 'my-plugin' ) . '</p>';
echo '</td></tr>';
$value = get_term_meta( $term->term_id, 'my_plugin_featured', true );
echo '<tr class="form-field">';
printf(
'<th scope="row"><label for="%1$s">%2$s</label></th>',
esc_attr( 'my_plugin_featured' ),
esc_html__( 'Feature this term', 'my-plugin' )
);
echo '<td>';
printf(
'<input type="checkbox" id="%1$s" name="%1$s" value="1"%2$s />',
esc_attr( 'my_plugin_featured' ),
checked( $value, '1', false )
);
echo '</td></tr>';
}
add_action( 'category_edit_form_fields', 'my_plugin_term_edit_fields' );
/**
* Saves the fields.
*
* Both hooks call this: created_ for a new term, edited_ for an existing one.
*
* @param int $term_id The term.
*/
function my_plugin_save_term_meta( $term_id ) {
if ( ! isset( $_POST['my_plugin_nonce'] ) ) {
return;
}
$nonce = sanitize_text_field( wp_unslash( $_POST['my_plugin_nonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'my_plugin_term_meta' ) ) {
return;
}
if ( ! current_user_can( 'manage_categories' ) ) {
return;
}
if ( empty( $_POST['my_plugin_subtitle'] ) ) {
delete_term_meta( $term_id, 'my_plugin_subtitle' );
} else {
update_term_meta(
$term_id,
'my_plugin_subtitle',
sanitize_text_field( wp_unslash( $_POST['my_plugin_subtitle'] ) )
);
}
if ( empty( $_POST['my_plugin_featured'] ) ) {
delete_term_meta( $term_id, 'my_plugin_featured' );
} else {
update_term_meta( $term_id, 'my_plugin_featured', '1' );
}
}
add_action( 'created_category', 'my_plugin_save_term_meta' );
add_action( 'edited_category', 'my_plugin_save_term_meta' );
/**
* Adds a column for subtitle to the terms list.
*
* @param array $columns Existing columns.
* @return array
*/
function my_plugin_term_columns( $columns ) {
$columns['my_plugin_subtitle'] = esc_html__( 'Subtitle', 'my-plugin' );
return $columns;
}
add_filter( 'manage_edit-category_columns', 'my_plugin_term_columns' );
/**
* Fills the column.
*
* @param string $content Current content.
* @param string $column Column name.
* @param int $term_id The term.
* @return string
*/
function my_plugin_term_column_content( $content, $column, $term_id ) {
if ( 'my_plugin_subtitle' !== $column ) {
return $content;
}
return esc_html( (string) get_term_meta( $term_id, 'my_plugin_subtitle', true ) );
}
add_filter( 'manage_category_custom_column', 'my_plugin_term_column_content', 10, 3 );
Output is valid and updates as you type.
Fix the highlighted fields to update the output.
Add fields to a taxonomy term. The generator writes both forms, because WordPress uses different markup for Add Term and Edit Term, plus the nonce, the save, and the meta registration.
How to use
- Name the taxonomy exactly. Every hook here carries it:
category_add_form_fields,edited_category,manage_edit-category_columns. A typo is a hook that never fires. - Prefix the meta keys. Term meta is a shared table, and
subtitleis a name several plugins reach for. - Keep the meta registration on. It types the value and checks permission wherever it is written from, which the forms alone do not.
- Leave the column on. The terms list is the only place an editor can see these values without opening each term.
- Paste the file into your plugin. Everything registers itself.
Example
The two forms take different markup, which is the part people get wrong:
// Add Term: a div, and nothing to read yet.
add_action( 'category_add_form_fields', 'acme_term_add_fields' );
// Edit Term: a table row, with the stored value.
add_action( 'category_edit_form_fields', 'acme_term_edit_fields' );
// Both save through the same callback.
add_action( 'created_category', 'acme_save_term_meta' );
add_action( 'edited_category', 'acme_save_term_meta' );
Reusing the add form’s markup on the edit screen produces a field that renders outside the table and looks broken in every browser.
Pitfalls
- The add and edit forms need different markup: a
div.form-fieldon add, atr.form-fieldon edit. There is no shared template. - Both
created_<taxonomy>andedited_<taxonomy>have to be hooked, or the value saves on one path and silently not on the other. - The add form runs before the term exists, so there is no
term_idto read from and no default to show except the one you hardcode. - Term meta arrived in WordPress 4.4. On older installs
get_term_meta()does not exist at all. $_POSTvalues are slashed. Withoutwp_unslash()you store literal backslashes that grow on every save.- The column filters are
manage_edit-<taxonomy>_columnsfor the header andmanage_<taxonomy>_custom_columnfor the cell, and the second one returns a string rather than echoing. - Deleting a term does not delete its meta automatically in every case. Clean up on
delete_<taxonomy>if the data matters. - A capability check on
manage_categoriesis right for categories; a custom taxonomy may use its own capability names.
Compatibility
Term meta, register_term_meta(), get_term_meta() and the manage_<taxonomy>_custom_column filter all need WordPress 4.4 or later; register_term_meta() gained its full argument array in 4.9.8. The form hooks have been stable since 3.0. The generated code targets PHP 7.0 and up, and the tool runs entirely in your browser.
Frequently asked questions
Why does my field not save on a new term?
edited_<taxonomy> was hooked. New terms fire created_<taxonomy>.Why does the field look broken on the edit screen?
tr with a th and a td, not a div.Do I need register_term_meta()?
How do I show the value on the front end?
get_term_meta( $term_id, 'prefix_key', true ), usually inside a taxonomy archive template.Can I use this for product categories?
product_cat, and the capability is usually manage_product_terms.