As I found out, WordPress does not allow the creation of taxonomies independently, without binding to any post type. I mean, we can register a taxonomy without binding it to any post type, but when we go to the page for creating new terms (elements) of this taxonomy, we will be in the "Posts" tab of the Admin Panel. But we want to have our own, separated taxonomy page in the Admin Panel. I will explain that further by steps...
Task
I need to keep some data (strings) with a possibility to add more data. Further, that data (strings) will be used for WordPress users (users will have skills; for example, a user is able to cook, wash, clean).
In order to avoid writing a bunch of code and to be able to create, modify and delete those skills (the number of which may exceed 2000), I made a decision to create a custom WordPress taxonomy for this purpose.
Advantages: it is very easy to register a taxonomy, so we immediately get a table with pagination and search; the ability to add, change or delete data, as well as the ability to expand the data with terms metadata. Moreover, we get a whole package of WP functions to display and manage our taxonomy elements If, for example, we need to keep the data in a separate table or in WP Options. Managing all of them requires writing additional code for everything: from the creation of the page in the Admin Panel to the functions for outputting our data — utilizing the taxonomy registering we get all this at once.
Disadvantages: unused fields in the taxonomy table, but this is a trifle, so actually there are no disadvantages!
So, the task is to create a taxonomy that is not tied to any post type and has its own menu item in the Admin Panel.
Solution
Register a taxonomy. We need it only for data storage, so it is not public (it's invisible on the frontend of the website), and it doesn't have any typical taxonomy parameters:
## create taxonomy skills
add_action( 'init', function (){
register_taxonomy( 'skills', null, array(
'labels' => array(
'name' => 'Skills',
'singular_name' => 'Skill',
'add_new_item' => 'Add new Skill',
),
'public' => false,
'show_ui' => true, // equal to the argument public
'show_in_rest' => false, // add to REST API
'hierarchical' => false,
'update_count_callback' => '__return_null',
) );
}, 20 );
## add a taxonomy menu item to the admin menu
add_action( 'admin_menu', 'add_skills_menu_item' );
function add_skills_menu_item(){
add_menu_page( 'Skills', 'Skills', 'manage_options', "edit-tags.php?taxonomy=skills", null, 'dashicons-awards', 9 );
}
As a result, we get the following taxonomy page in the Admin Panel:
But when we go to the skills taxonomy page, we are in the "Posts" menu tab.
Now the second moment is to make an active menu item to be the taxonomy, not the "Posts" tab. There are no built-in hooks in WordPress to do it simply so we will use a workaround for that.
Let's do it by replacing our previous code around adding menu item:
/**
* Add taxonomy menu item to the admin menu.
*/
add_action( 'admin_menu', 'add_skills_menu_item' );
function add_skills_menu_item() {
global $menu;
$tax_name = 'skills';
$menu_title = 'Skills';
$capability = 'manage_options';
$is_skills = ( ( $_GET['taxonomy'] ?? '' ) === $tax_name );
// Remove 'current' for posts (by default, the taxonomy is attached there, even if the post type is not specified when registering the taxonomy)
if( $is_skills ){
add_filter( 'parent_file', '__return_false' );
}
// Add menu item
add_menu_page( $menu_title, $menu_title, $capability, "edit-tags.php?taxonomy=$tax_name", null, 'dashicons-awards', 9 );
// Adjust some parameters of the added menu item
$menu_item_key = key( wp_list_filter( $menu, [ $menu_title ] ) );
$menu_item = & $menu[ $menu_item_key ];
foreach( $menu_item as & $val ){
// Add the 'current' class where necessary
if( false !== strpos( $val, 'menu-top' ) ){
$val = 'menu-top' . ( $is_skills ? ' current' : '' );
}
$val = preg_replace( '~toplevel_page[^ ]+~', "toplevel_page_$tax_name", $val );
}
}
Result:
That's it!
-
My task required me to hide unnecessary fields and to add a field to bulk add skills.
Whole previous code, including the code for my further tasks:
<?php
Kama_Register_Single_Taxonomy::init();
/**
* Registers a taxonomy without binding to a post type with its own separate menu item in the admin menu.
*/
class Kama_Register_Single_Taxonomy {
public static $tax_name = 'skills';
public static $menu_title = 'Skills';
private static $capability = 'manage_options';
private static $request_key = 'bulk_add_skills';
private static $menu_labels = [
'name' => 'Skills',
'singular_name' => 'Skill',
'add_new_item' => 'Add New Skill',
];
public static function init(){
add_action( 'init', static function(){
self::register_taxonomy();
self::bulk_add_terms_handler();
} );
## Add taxonomy menu item to the admin menu.
add_action( 'admin_menu', [ __CLASS__, 'add_tax_menu_item' ] );
## bulk add skills form
add_action( self::$tax_name . '_add_form', [ __CLASS__, 'bulk_add_terms_form' ] );
## custom styles on the taxonomy page and on the edit item page.
add_action( 'admin_head', [ __CLASS__, 'styles_on_edit_term_page' ] );
## Remove unnecessary columns
$edit_page_key = 'edit-' . self::$tax_name;
add_filter( "manage_{$edit_page_key}_columns", [ __CLASS__, 'remove_unused_columns' ] );
}
private static function register_taxonomy(): void {
register_taxonomy(
self::$tax_name,
null,
[
'labels' => self::$menu_labels,
'public' => false,
'show_ui' => true, // equal to the public argument
'show_in_rest' => false, // add to the REST API
'hierarchical' => false,
'update_count_callback' => '__return_null',
]
);
}
public static function add_tax_menu_item(): void {
global $menu;
$tax_name = self::$tax_name;
$is_the_tax = ( ( $_GET['taxonomy'] ?? '' ) === self::$tax_name );
// remove 'current' for entries (by default, the taxonomy is linked there, even if no post type is specified when registering the taxonomy)
if( $is_the_tax ){
add_filter( 'parent_file', '__return_false' );
}
// add menu item
add_menu_page(
self::$menu_title,
self::$menu_title,
self::$capability,
'edit-tags.php?taxonomy=' . self::$tax_name,
null,
'dashicons-awards',
9
);
// adjust some parameters of the added menu item
$menu_item_key = key( wp_list_filter( $menu, [ self::$menu_title ] ) );
$menu_item = & $menu[ $menu_item_key ];
foreach( $menu_item as & $val ){
// add the 'current' class where needed
if( false !== strpos( $val, 'menu-top' ) ){
$val = 'menu-top' . ( $is_the_tax ? ' current' : '' );
}
$val = preg_replace( '~toplevel_page[^ ]+~', "toplevel_page_$tax_name", $val );
}
}
/**
* Handles the request for bulk adding skills.
*/
public static function bulk_add_terms_handler(): void {
$new_terms = trim( $_POST[ self::$request_key ] ?? '' );
if( ! $new_terms || ! current_user_can( self::$capability ) ){
return;
}
$new_terms = wp_unslash( $new_terms );
$new_terms = array_filter( array_map( 'trim', explode( "\n", $new_terms ) ) );
$err_names = [];
foreach( $new_terms as $new_term_name ){
$data = wp_insert_term( $new_term_name, self::$tax_name );
if( is_wp_error( $data ) ){
$err_names[ $new_term_name ] = $data->get_error_message();
}
}
// message about the request processing result
add_action( 'admin_notices', function() use ( $err_names, $new_terms ) {
$added_count = count( $new_terms ) - count( $err_names );
$message = "<p>Added terms: $added_count</p>";
if( $err_names ){
$message .= '<p style="color:red;">';
$message .= 'Failed to add: <br>';
foreach( $err_names as $skill_name => $err_msg ){
$message .= '<b>' . esc_html( $skill_name ) . "</b>: $err_msg <br>";
}
$message .= "</p>";
}
echo '<div class="notice notice-success is-dismissible"><div>' . $message . '</div></div>';
} );
}
public static function bulk_add_terms_form(): void {
if( ! current_user_can( self::$capability ) ){
return;
}
// the code is output inside the existing form, so we will close it and open our own
?>
</form>
<form method="POST" action="" class="bulk-add-terms-form">
<div class="form-field">
<h2>Bulk Add Skills</h2>
<p>List skills one per line.</p>
<textarea name="<?= self::$request_key ?>" rows="5" style="width:95%"></textarea>
</div>
<?php
submit_button( 'Add Skills in Bulk' );
}
public static function styles_on_edit_term_page() {
$edit_page_key = 'edit-' . self::$tax_name;
if( get_current_screen()->id !== $edit_page_key ){
return;
}
// hide unnecessary fields
?>
<style>
.form-field.term-slug-wrap{ display:none; }
.form-field.term-description-wrap{ display:none; }
</style>
<?php
}
public static function remove_unused_columns( $columns ) {
unset( $columns['description'], $columns['posts'] );
return $columns;
}
}
Result: