Skip to content

Instantly share code, notes, and snippets.

@justintadlock
Created July 30, 2026 14:13
Show Gist options
  • Select an option

  • Save justintadlock/b9d84f97d8e961a5ac555234ab7e124c to your computer and use it in GitHub Desktop.

Select an option

Save justintadlock/b9d84f97d8e961a5ac555234ab7e124c to your computer and use it in GitHub Desktop.
Term Author for WordPress
<?php
/**
* Plugin Name: Term Author
* Plugin URI: https://example.com/term-author
* Description: Records the user who created a taxonomy term in the `term_author` term meta key, and lets editors view or reassign it.
* Version: 1.0.0
* Requires at least: 5.5
* Requires PHP: 7.2
* Author: You
* License: GPL-2.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: term-author
*
* @package TermAuthor
*/
defined( 'ABSPATH' ) || exit;
/**
* The meta key used to store the author's user ID.
*/
define( 'TERM_AUTHOR_META_KEY', 'term_author' );
/* -------------------------------------------------------------------------
* Meta registration
* ---------------------------------------------------------------------- */
/**
* Register the meta key for every taxonomy so it is typed, sanitized and
* available over the REST API.
*
* Passing an empty taxonomy registers the key for all taxonomies.
*
* @return void
*/
function term_author_register_meta() {
register_term_meta(
'',
TERM_AUTHOR_META_KEY,
array(
'type' => 'integer',
'description' => __( 'ID of the user who created the term.', 'term-author' ),
'single' => true,
'default' => 0,
'show_in_rest' => true,
'sanitize_callback' => 'absint',
'auth_callback' => 'term_author_meta_auth_callback',
)
);
}
add_action( 'init', 'term_author_register_meta' );
/**
* Only users who can edit the term may write the meta value.
*
* @param bool $allowed Whether the user can add/edit the meta.
* @param string $meta_key The meta key.
* @param int $term_id The term ID.
* @param int $user_id The user ID being checked.
* @return bool
*/
function term_author_meta_auth_callback( $allowed, $meta_key, $term_id, $user_id ) {
return user_can( $user_id, 'edit_term', $term_id );
}
/* -------------------------------------------------------------------------
* Capture the author on creation
* ---------------------------------------------------------------------- */
/**
* Store the current user as the author when a term is created.
*
* Nothing is stored for terms created outside a user session (WP-CLI, cron,
* unauthenticated imports) so the absence of the key stays meaningful.
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
* @param string $taxonomy Taxonomy slug.
* @return void
*/
function term_author_set_on_create( $term_id, $tt_id, $taxonomy ) {
/**
* Filters the user ID recorded as the term author.
*
* Return 0 to skip storing an author entirely — useful during imports
* where you want to assign authorship yourself.
*
* @param int $user_id Defaults to the current user.
* @param int $term_id Term ID.
* @param string $taxonomy Taxonomy slug.
*/
$user_id = (int) apply_filters( 'term_author_user_id', get_current_user_id(), $term_id, $taxonomy );
if ( $user_id > 0 ) {
add_term_meta( $term_id, TERM_AUTHOR_META_KEY, $user_id, true );
}
}
add_action( 'created_term', 'term_author_set_on_create', 10, 3 );
/* -------------------------------------------------------------------------
* Public helpers
* ---------------------------------------------------------------------- */
/**
* Get the author ID for a term.
*
* @param int|WP_Term $term Term ID or object.
* @param string $taxonomy Optional taxonomy, when passing an ID.
* @return int User ID, or 0 if none is recorded.
*/
function term_author_get_id( $term, $taxonomy = '' ) {
$term = get_term( $term, $taxonomy );
if ( ! $term instanceof WP_Term ) {
return 0;
}
return (int) get_term_meta( $term->term_id, TERM_AUTHOR_META_KEY, true );
}
/**
* Get the author as a WP_User object.
*
* @param int|WP_Term $term Term ID or object.
* @param string $taxonomy Optional taxonomy, when passing an ID.
* @return WP_User|null
*/
function term_author_get_user( $term, $taxonomy = '' ) {
$user_id = term_author_get_id( $term, $taxonomy );
$user = $user_id ? get_userdata( $user_id ) : false;
return $user ? $user : null;
}
/**
* Set (or clear) the author of a term.
*
* @param int $term_id Term ID.
* @param int $user_id User ID. Pass 0 to remove the stored author.
* @return bool True on success.
*/
function term_author_set_id( $term_id, $user_id ) {
$term_id = absint( $term_id );
$user_id = absint( $user_id );
if ( ! $term_id ) {
return false;
}
if ( ! $user_id ) {
return (bool) delete_term_meta( $term_id, TERM_AUTHOR_META_KEY );
}
return false !== update_term_meta( $term_id, TERM_AUTHOR_META_KEY, $user_id );
}
/* -------------------------------------------------------------------------
* Admin UI
* ---------------------------------------------------------------------- */
/**
* Wire up the column and edit field for every taxonomy with a UI.
*
* @return void
*/
function term_author_admin_hooks() {
$taxonomies = get_taxonomies( array( 'show_ui' => true ), 'names' );
foreach ( $taxonomies as $taxonomy ) {
add_filter( "manage_edit-{$taxonomy}_columns", 'term_author_add_column' );
add_filter( "manage_{$taxonomy}_custom_column", 'term_author_render_column', 10, 3 );
add_action( "{$taxonomy}_edit_form_fields", 'term_author_render_edit_field', 10, 2 );
}
}
add_action( 'admin_init', 'term_author_admin_hooks' );
/**
* Add an "Author" column to the term list table.
*
* @param array $columns Existing columns.
* @return array
*/
function term_author_add_column( $columns ) {
$columns[ TERM_AUTHOR_META_KEY ] = __( 'Author', 'term-author' );
return $columns;
}
/**
* Render the author column.
*
* @param string $content Column content.
* @param string $column_name Column being rendered.
* @param int $term_id Term ID.
* @return string
*/
function term_author_render_column( $content, $column_name, $term_id ) {
if ( TERM_AUTHOR_META_KEY !== $column_name ) {
return $content;
}
return esc_html( term_author_get_display_name( $term_id ) );
}
/**
* Human-readable author name, with sensible fallbacks.
*
* @param int $term_id Term ID.
* @return string
*/
function term_author_get_display_name( $term_id ) {
$user_id = term_author_get_id( $term_id );
if ( ! $user_id ) {
return __( '—', 'term-author' );
}
$user = get_userdata( $user_id );
if ( ! $user ) {
/* translators: %d: user ID that no longer exists. */
return sprintf( __( 'Deleted user (#%d)', 'term-author' ), $user_id );
}
return $user->display_name;
}
/**
* Render the author field on the term edit screen.
*
* @param WP_Term $term Term being edited.
* @param string $taxonomy Taxonomy slug.
* @return void
*/
function term_author_render_edit_field( $term, $taxonomy ) {
$user_id = term_author_get_id( $term );
?>
<tr class="form-field">
<th scope="row">
<label for="term_author"><?php esc_html_e( 'Author', 'term-author' ); ?></label>
</th>
<td>
<?php if ( current_user_can( 'list_users' ) && current_user_can( 'edit_term', $term->term_id ) ) : ?>
<?php
wp_nonce_field( 'term_author_' . $term->term_id, 'term_author_nonce' );
wp_dropdown_users(
array(
'name' => 'term_author',
'id' => 'term_author',
'selected' => $user_id,
'include_selected' => true,
'show_option_none' => __( '— No author —', 'term-author' ),
'option_none_value' => 0,
)
);
?>
<p class="description">
<?php esc_html_e( 'The user credited with creating this term.', 'term-author' ); ?>
</p>
<?php else : ?>
<p><?php echo esc_html( term_author_get_display_name( $term->term_id ) ); ?></p>
<?php endif; ?>
</td>
</tr>
<?php
}
/**
* Persist the author field from the term edit screen.
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
* @param string $taxonomy Taxonomy slug.
* @return void
*/
function term_author_save_edit_field( $term_id, $tt_id, $taxonomy ) {
if ( ! isset( $_POST['term_author_nonce'], $_POST['term_author'] ) ) {
return;
}
$nonce = sanitize_key( wp_unslash( $_POST['term_author_nonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'term_author_' . $term_id ) ) {
return;
}
if ( ! current_user_can( 'edit_term', $term_id ) ) {
return;
}
term_author_set_id( $term_id, absint( wp_unslash( $_POST['term_author'] ) ) );
}
add_action( 'edited_term', 'term_author_save_edit_field', 10, 3 );
@justintadlock

Copy link
Copy Markdown
Author

How it works:

  • created_term fires for every taxonomy, so the current user ID gets written once via add_term_meta( ..., true ) — later edits won't overwrite it.
  • register_term_meta with an empty taxonomy registers the key everywhere, gives it an integer type with absint sanitization, and exposes it at meta.term_author in the REST API. Writes are gated on the edit_term capability.
  • Helpers: term_author_get_id(), term_author_get_user(), term_author_set_id().
  • An "Author" column appears in term list tables, and the term edit screen gets a user dropdown for reassignment.

Two things worth knowing:

Terms created without a logged-in user (WP-CLI, cron, unauthenticated imports) get no meta at all rather than 0, so a missing key genuinely means "unknown." If you want to force an author in those contexts, filter it:

add_filter( 'term_author_user_id', fn( $id ) => $id ?: 1 );

Existing terms won't be backfilled — there's no record of who made them. If you want everything to have a value, a one-off WP-CLI loop assigning a default admin ID is the usual approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment