A Gory Hack For Removing Tags And Categories From Multiple Posts

A Gory Hack For Removing Tags And Categories From Multiple Posts

The WordPress admin interface provides the functionality to add tags and categories to multiple posts but not to remove them.

To do that you’ve still got to edit each post which is a bit of a chore if you’ve got plenty of tags or it’s something you do on a regular basis.

In this post, I’ll show you how to add the ability to remove tags or a category from multiple posts. But be warned, it’s a hack that’s not for the squeamish.

Photo of Boris Karloff as Frankenstein's Monster
This hack even scares Frankenstein’s monster

The solution to this problem is, on the surface pretty simple. The admin user goes to the ‘All Posts’ screen and:

  1. Selects the posts to be actioned
  2. Selects the appropriate action (e.g. remove tags) from the Bulk Actions menu
  3. Selects a category from a select control, or enters a tag in a text input control
  4. Clicks on Apply
  5. Magic happens and tags or a category are removed from the selected posts

Virtually every time you want to add functionality to WordPress you can usually find a filter or an action to help out, often in the most obscure of use cases.

But not in this case.

Screenshot of the menus at the top of All Posts screen
Customizing this set of menus is alarmingly difficult

Whilst the load-edit.php action allows custom functionality to be executed on selected posts, there are no filters or actions that provide the ability to customize the action bar at the top of the All Posts screen.

After a session on Google, I did find help (thanks to Justin Stern) for a workaround but, be warned, if you like your solutions to be clean then perhaps you should look away now.

We can hack the Bulk Actions on the client-side using javascript. Told you it was ugly.

Customizing the Action Bar With a Client-side Hack

We are going to use the admin_footer-edit.php action to get our Frankenstein javascript on the All Posts page but only if the $post_type is post:

if ( is_admin() ) add_action('admin_footer-edit.php' , 'custom_bulk_admin_footer' );
global $post_type;

if($post_type == 'post') {
<script type="text/javascript">
jQuery(document).ready(function() {
jQuery('<option>').val('remove').text('<?php _e('Remove Terms')?>').appendTo("select[name='action']");
jQuery('input#doaction').before('<input type="text" name="action_tags" value="Enter tags">');
jQuery('input#doaction').before('<select name="action_cats" id="action_cats"><option value="0">Select categories</option></select>');
jQuery('select#cat option[value!=0]').clone().appendTo('select#action_cats');

The javascript adds a new options to the Bulk Actions menu:Remove Terms. It also inserts the HTML for the category select control and the text input control before the Apply (#doaction) button.

The last part of the horror hack is to copy the options from the existing categories select (this is used to filter the display) to our new select control. We could have built the options server-side but seeing as they are already in the page it seems easier just to copy them.

Our action menu now looks something like this:

Screenshot of customized action bar on All Posts screen
It’s not a pretty solution but we get the outcome we want

Handling the New Bulk Actions With our new actions sledgehammered into the Bulk Actions menu, we now need to process them. This time we do have an action to help us, load-edit.php.

if ( is_admin() ) add_action('load-edit.php' , 'custom_bulk_action' );

function custom_bulk_action() {

global $typenow;
$post_type = $typenow;

// only interested if the type is post
if ( $post_type == 'post' ) {
// get the action
$wp_list_table = _get_list_table( 'WP_Posts_List_Table' );
$action = $wp_list_table->current_action();

// we have a rogue action
if ( $action != 'remove' ) return;

// security check
check_admin_referer( 'bulk-posts' );

// get the tags and categories
$tags = ( $_REQUEST['action_tags'] == 'Enter tags' ) ? '' : $_REQUEST['action_tags'];
$cats = ( $_REQUEST['action_cats'] == '0' ) ? '' : $_REQUEST['action_cats'];

if ( $tags == '' && $cats == '' ) return;

// make sure ids are submitted (posts have been selected)
if ( isset( $_REQUEST['post'] ) ) $post_ids = array_map( 'intval' , $_REQUEST['post'] );

// no ids selected
if ( empty( $post_ids ) ) return;

// this is based on wp-admin/edit.php
$sendback = remove_query_arg( array('tagged', 'untrashed', 'deleted', 'ids'), wp_get_referer() );
if ( ! $sendback ) $sendback = admin_url( 'edit.php?post_type=$post_type' );

$pagenum = $wp_list_table->get_pagenum();
$sendback = add_query_arg( 'paged', $pagenum , $sendback );

$tagged = 0;
foreach( $post_ids as $post_id ) {

if ( $cats != '' ) {
if ( remove_terms( $post_id , $cats , 'category' ) ) $tagged++;

if ( $tags != '' ) {
if ( remove_terms( $post_id , $tags , 'post_tag' ) ) $tagged++;


$sendback = add_query_arg( array('tagged' => $tagged, 'ids' => join(',', $post_ids) ), $sendback );

$sendback = remove_query_arg( array('action', 'action_tags', 'action_cats' , 'post_author', 'comment_status', 'ping_status', '_status', 'post', 'bulk_edit', 'post_view'), $sendback );

} //if

Most of the initial processing is concerned with validating the request: that this is the post editing page, that the action is remove and that a category or tags have been provided.

If these tests are passed then the category or tags are removed. You’ll notice that regardless of whether the action is for tags or a category that the same function is used. This is because the wp_set_post_terms and wp_get_post_terms functions provided by WordPress work the same with both tags and categories – all you have to do is tell the function which taxonomy you are using (category or post_tag).

Removing Tags Or A Category From A Post

There’s no simple function available to remove a category or tag from a post and so we have to get a little creative.

The remove_terms function creates an array of the terms to be removed and an array of the post’s existing terms and then uses the array_diff command to create a new list of post terms minus the terms to be removed..

The wp_set_post_terms function is called with the $append parameter set to false so that the new post terms replace any existing post terms, effectively removing the unwanted terms.

function remove_terms( $post_id , $the_terms , $taxonomy ) {

// as there is no function to remove terms, we need to make a list of current terms,
// remove the passed terms from the list and then reapply

// turn $terms into an array
$term_list = explode( ',', trim( $the_terms , " \n\t\r\0\x0B," ) );

$fields = ( $taxonomy == 'category' ) ? 'ids' : 'names';

// get current terms
$post_terms = wp_get_post_terms( $post_id , $taxonomy , array( 'fields' => $fields ) );

// remove passed terms from current terms
$post_terms = array_diff( $post_terms , $term_list );

// assign new list
$result = wp_set_post_terms( $post_id , $post_terms , $taxonomy , false );

if ( is_array( $result ) ) {
return true;
} else {
return false;

Letting The Admin User Know What’s Happening

The final piece of the solution is to provide some feedback to the admin user on what has happened. This achieved by using the admin_notices action to add our own custom notice.

if ( is_admin() ) add_action('admin_notices', 'custom_bulk_admin_notices' );

function custom_bulk_admin_notices() {

global $post_type, $pagenow;

if( $pagenow == 'edit.php' && $post_type == 'post' && isset($_REQUEST['tagged']) && (int) $_REQUEST['tagged'] ) {
$message = sprintf( _n( 'Post updated.', '%s posts updated.', (int) $_REQUEST['tagged'] ), number_format_i18n( $_REQUEST['tagged'] ) );
echo '<div class="updated"><p>{$message}</p></div>';

All this does is check to ensure we are on the All Posts page and that the tagged query argument has been passed (this was added in the custom_bulk_action function). If this is the case then a simple message is generated for the top of the screen.

With so many actions and filters, it’s always something of a surprise when you can’t find one to help you. Hacking the action bar with client-side scripting is obviously not the preferred course of action but there really isn’t much choice.

As always, with no alternative, it’s about weighing up whether the end justifies the means. In this case the functionality is useful, so I think it does.

What do you think? And are there any other dirty hacks that you’ve had to implement to provide some essential feature or functionality?

Code Credit: This solution is based on Justin Stern’s blog post on adding custom bulk actions.

Download plugin: bulk-term-remover.zip

Photo Credit: Insomnia Cured Here

Free Video Why 100 is NOT a Perfect Google PageSpeed Score (*5 Min Watch) Learn how to use Google PageSpeed Insights to set realistic goals, improve site speed, and why aiming for a perfect 100 is the WRONG goal.