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.

Live output

Enable JavaScript to customise; default output below.

The hook names carry this, so it has to match the registered taxonomy exactly.

Live preview term-meta.php
<?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.

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

  1. 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.
  2. Prefix the meta keys. Term meta is a shared table, and subtitle is a name several plugins reach for.
  3. Keep the meta registration on. It types the value and checks permission wherever it is written from, which the forms alone do not.
  4. Leave the column on. The terms list is the only place an editor can see these values without opening each term.
  5. 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-field on add, a tr.form-field on edit. There is no shared template.
  • Both created_<taxonomy> and edited_<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_id to 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.
  • $_POST values are slashed. Without wp_unslash() you store literal backslashes that grow on every save.
  • The column filters are manage_edit-<taxonomy>_columns for the header and manage_<taxonomy>_custom_column for 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_categories is 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?
Only edited_<taxonomy> was hooked. New terms fire created_<taxonomy>.
Why does the field look broken on the edit screen?
That form is a table. The field needs a tr with a th and a td, not a div.
Do I need register_term_meta()?
Not for the forms to work, but it gives the key a type and an auth callback, and it is what exposes the value to REST.
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?
Yes. The taxonomy is product_cat, and the capability is usually manage_product_terms.

From the people who built this tool

WP Adminify

The WordPress admin, rebuilt: a dashboard worth looking at, menu and column control, a real file manager and the login page your client sees.

See WP Adminify Free version on WordPress.org

Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.