By default, when you delete any posts or pages in WordPress, they go to the trash bin first and can be restored from there, but when you delete a category or tag it is directly gone from the WP database. WordPress also changes its posts, child categories, and menu links. So what exactly changes and how can we rebuild it? How do we make it undoable?
How are categories and tags stored?
In WordPress, category and tag are not just simple names and slugs, they are terms in a taxonomy.
The tables for taxonomy are as follows:
wp_terms-> This holds the name and slug for different terms.wp_term_taxonomy-> This holds the information of which term belongs to which taxonomy, the parent of the term, the description of the term and the number of posts associated with it.wp_termmeta-> Used to store extra plugin fields or meta data for terms (colours, images, SEO data).wp_term_relationships-> Which post has which term assigned to it. Theterm_orderis used to define the order of the terms.
In menus, the links to terms are stored as nav_menu_item posts, and the object ID in the _menu_item_object_id meta is used to fetch the term object.
Term IDs are also saved in blocks -> In Query Loop -> "taxQuery":{"category":[4]}
This would only fetch posts from the category with ID 4.
Latest Posts -> categories, navigation link -> id attribute.
What happens to the posts?
The function wp_delete_term() does the delete of the term without putting it in the trash and without any revision.
When we delete a category, what happens to the posts which are using this category? They are not deleted.
If the category you delete is the only one assigned to posts, then the posts would be left without any category assigned to them. They would be assigned to the default category, which is Uncategorized.
We do not have anything like default tags, when we delete a tag the posts which were assigned that tag would just lose that tag and nothing would be added in their place.
Since 5.5.0 version of WordPress, when we create a custom taxonomy, we can add a default_term which would be the default term for any object which does not specify a term for this taxonomy, so this works just like the default category.
You can also add the force_default argument for the wp_delete_term() function which would add it to the default term even if the post has other terms assigned to it.
What happens to everything else?
pre_delete_term hook is fired first, before any change is made to the terms data.
When a term is deleted, all the child categories of the deleted term are moved up to the parent of the term which was deleted.
The term meta data is deleted from the database.
The row from the wp_term_taxonomy table is deleted.
The row from the wp_terms table is only deleted when there is no other taxonomy which is using this term.
delete_term -> This is handled by the function _wp_delete_tax_menu_item() -> all the menu items which are pointing at the deleted term are deleted from the menu (for good), they are not sent to the trash.
On deleting a term, all the saved term IDs point at nothing.
Why is there no undo?
The confirm box says
“You are about to permanently delete these items from your site. This action cannot be undone.”
The terms do not support trashing, if we try to trash a term via REST API without the force parameter being set to true, it would return with status 501 Not Implemented.
The posts in trash can be kept for EMPTY_TRASH_DAYS days. By default, this period is set to 30 days.
While in case of terms, they are gone the same second after you hit delete, they do not even stay in the trash-bin for a single day.
We cannot delete the default category, if we try to delete it via the function wp_delete_term() it would return 0 to denote failure.
How do terms get deleted?
There’s a Delete link on the Categories or Tags screen from which we can delete a term.
If we try deleting many terms at once, a confirmation box won’t even pop up, instead all of them would be deleted.
We can use the WP-CLI command wp term delete <taxonomy> <term-id>.
Or we can use the REST API to delete a term via the request DELETE /wp/v2/categories/<id>?force=true.
There are some plugins available which can help us in cleaning up the unused tags.
Every path eventually ends up in the function wp_delete_term().
Rebuilding it by hand
Adding new terms with the same name and slug would just create another new term object, it would give us the ID of the newly added term. The reference for the old ID would stay broken, everywhere where it was used (menus, Query Loop blocks). Also there’s no record of which posts had it attached.
We can use the Bulk Edit feature to add the category to many posts at once.
We can also re-parent the child categories to the new one.
We can re-add the menu items in the right position.
We can edit each Query Loop’s taxonomy filter to point to the new category.
We can remove the Uncategorized from the posts which only got it from the previous delete.
Restoring from a backup
Full restore means to restore the whole site back to a certain point in time, the problem here would be that every post, order, or comment added to the site after the backup was created would be lost.
Surgical restore creates a new separate database and loads the backup into that, for example if we only want to restore a single category we can copy the rows related to the category from the backup into the current site database.
- Copy that term’s rows from
wp_terms,wp_term_taxonomy,wp_termmetatables. - Plus copy all the
wp_term_relationshipsrows related to that term. - Plus copy all the
nav_menu_itemposts with their_menu_item_*meta which contains the term IDs. - Then we will run the command
wp term recount categoryto update the term count. - Also we will flush the object cache.
- We will take the
Uncategorizedcategory off from the posts which only have it from the delete.
Axle Category & Tag Trash
Category & Tag Trash
A trash for categories, tags and custom terms, with undo and full restore.
Use it when a category was deleted and its posts lost their place.
Learn more about Category & Tag TrashTry it live: Category & Tag TrashThere’s a free plugin for this, deleted terms go to a trash, and there they are kept for 30 days by default, can be changed to 7, 90, or 365 days.
There’s an Undo notice right after a delete, single or bulk.
We can view the deleted terms via the Tools > Axle Category & Tag Trash menu, here we are provided with information about the contents, who deleted them and when.
On the backend, it captures the pre_delete_term hook, before WordPress even tries to touch the posts.
On the delete_term action, once the delete has actually happened, the data is stored in the database.
This works for every path, whether it be a screen, a bulk action, a REST API or a WP-CLI command.
It supports categories, tags, product categories and attributes and custom taxonomies, just the menus (nav_menu) are not included.
What does a restore put back?
Restore puts the term back with its old ID if the ID is still free, else the term is put with a new ID and a note is added saying that this term ID has changed from the old one to the new one.
On restoring a term all the information about it is restored, name, slug, description, parent, all the term meta data is restored.
The term is also restored on every post it had, the same term_order is retained for all the posts.
Child terms are also moved back under it, unless they’ve been moved after the deletion.
All the menu links which were pointing to the current term are recreated, they are added to the same menu and in the same position with the same sub-items.
Uncategorized term is taken off from every post where it only exists because of the deleted term. (It still remains if another term is assigned to the post.)
If the slug of the deleted term is already being used by a new term meanwhile, the restored term would get a unique version of the slug, and a note saying so.
If the term has the same ID, then all the navigation blocks, widgets, shortcodes or any other thing which uses the ID of this term would work again.
What about big terms?
If we delete a tag which is assigned to thousands of posts, the trash would keep their IDs, 1,000 per meta row.
On the restore action for the trash, the first 5,000 posts would be restored in that particular request. The remaining would be restored through the WP-Cron in the same way, 5,000 through every execution of the cron in the background.
There’s also a command line way of restoring the posts, wp axle-category-tag-trash restore <id>, this would restore every post associated to the trash item with the ID in one go. Here, <id> is the ID of the trash item we want to restore, not the term ID, we can get the list of trash items by running the command wp axle-category-tag-trash list.
Restoring a post would fire the hooks added_term_relationship and set_object_terms, so ElasticPress and other search plugins would reindex those posts.
Deleted posts are skipped while restoring a term from trash.
Limits
This only catches the deletes which are done after the activation of the plugin, if a term is deleted before the plugin is installed, it wouldn’t be caught by this and would be gone from the database forever. In this case you can rebuild it as we did in the above example.
The plugin deletes all the expired items from the trash daily.
We can leave a taxonomy out of this by adding the filter axle_category_tag_trash_supports_taxonomy and return false for the taxonomies we want to exclude.
Our WP-CLI commands for this plugin are
wp axle-category-tag-trash list-> This will list the items inside the trash.wp axle-category-tag-trash restore <id>-> This will restore the item with the given ID.wp axle-category-tag-trash delete --all-> This will delete all the items in the trash.
This is a free plugin released under the GPL, there is no pro version. It does not come with any tracking or analytics.
You can try out the demo of this plugin on try.axlewp.com/axle-category-tag-trash.