How to Create an Unlisted Tag in WordPress

At my job, I work a on a membership-based site where staff has access to the WordPress admin, but users of the site only have access to the front end of the site. I have two shortcode functions I wrote for a staff-facing “Changelog” page of recent edits and a user-facing “Recent Updates” page.

However, there are some pages that are accessible for users that we don’t want to advertise. Even though we don’t give the link out to a page and it’s not in any menus, I was inadvertently about to create an area where users could discover these pages. Not to mention that users could also find the pages with a quick search on the site.

My solution to both of these issues was to create a layer between “Published” and “Private” – by using a tag I named “Unlisted.” With this sweet little function, your pages, posts, and custom post types with a particular tag (change “1” to the correct ID) will be excluded from the front-end search. The second part of the conditional, !is_admin() is what stops you from going crazy when searching for pages on the admin side. 🙂

 * Exclude "Unlisted" tag from search.
function rose_exclude_unlisted($query) {
	if ( ($query->is_search) && (!is_admin()) ) {
		$query->set('tag__not_in', 1); // Change 1 to the ID of your "Unlisted" tag.
	return $query;
add_filter('pre_get_posts', 'rose_exclude_unlisted');

Similarly, in my [public-updates] shortcode function, I added the argument tag__not_in with the Unlisted tag’s ID.

function rose_public_updates() {
	// Get the 20 most recent edited public posts/pages/custom post type.
	$recent_updates = '';
	$args = array(
		'post_type' => 'any',
		'post_status' => array(
		'tag__not_in' => 1, // Change 1 to the ID of your "Unlisted" tag.
		'orderby' => 'modified',
		'posts_per_page' => 20,
		'ignore_sticky_posts' => '1'

	$recent_loop = new WP_Query( $args );
	if ( $recent_loop->have_posts() ) {
		$recent_updates = '<table><tr><th width="42%">Last 20 Updates</th><th width="30%">Last Modified</th></tr>';
		while( $recent_loop->have_posts() ) : $recent_loop->the_post();
			$recent_updates .= '<tr><td><a href="' . get_permalink( $recent_loop->post->ID ) . '">' . get_the_title( $recent_loop->post->ID ) . '</a></td><td>'. get_the_modified_date( 'D, M d, Y \a\t g:iA' ) .'</td></tr>';
		$recent_updates .= '</table>';

	return $recent_updates;
add_shortcode('public-updates', 'rose_public_updates');

Add that bad boy to your functions file (if it’s your own theme or child theme), or throw it in a plugin and fire it up!