User Choice: Hiding Some Sites in My Sites in WordPress Multisite

Long have I struggled with WordPress Multisite and the way it makes things difficult for non-super admin users. While it is very easy to join additional sites, leaving them on your own is easier said than done. I hesitate to write ‘impossible’ even now as it seems insane that there isn’t a good and obvious way for non-site admins to leave a site. Maybe someone will point out something obvious I’ve missed but in case I am right here is a way to allow users to control which sites show up in their My Sites list.

After some thought, I went with the least dramatic path here. We are just removing the sites from the main views rather than deleting accounts on individual blogs. I started to go that route (remove_user_from_blog) but a conversation with Matt and the concerns about transferring ownership of the content or deleting the content and being able to un-do that if a mistake was made quickly made me think of some other options.

I opted to use add a filter to the get_blogs_of_user function which builds both the drop down menu of sites and the blogs listed on the My Sites page.

add_filter( 'get_blogs_of_user', 'remove_selected_blogs_from_get_blogs' );

With the function below I can pass in an array of blog IDs to ignore when building the ‘My Sites’ list. You’ll also note that the filter doesn’t run when the person is on the profile.php page ($pagenow != ‘profile.php’). That allows us to see all the sites so we can undo decisions to hide sites in the future . . . otherwise there’d be nowhere we could see the full list.1

function remove_selected_blogs_from_get_blogs($blogs) {
    global $pagenow; //mostly works to allow full list on profile page but filter elsewhere
    $newblogs = array();
    $user_id = wp_get_current_user()->ID;
    $hidden_blogs = explode(",",hidden_blogs($user_id));
    if ( !is_super_admin() &&  $pagenow != 'profile.php') {
        foreach ($blogs as $key => $value) {
            if (!in_array($value->userblog_id, $hidden_blogs) )
                $newblogs[$key] = $value;
        return $newblogs;
    } else {
        return $blogs;

I decided that saving the blogs that you want blocked in the User Profile would be at least semi-logical. The following code builds out a new user profile field called ‘my_hidden_sites’ and does all the stuff necessary for it to save and have a semi-decent layout.

/add sites interface to user profile field
add_action( 'show_user_profile', 'hidden_site_user_profile_fields' );
add_action( 'edit_user_profile', 'hidden_site_user_profile_fields' );

function hidden_site_user_profile_fields( $user ) { ?>
    <span class='dashicons dashicons-hidden big-eye'></span>
    <h3><?php _e("Hide a Site?", "blank"); ?></h3>
    <?php rampages_get_user_sites($user->ID);?>
    <table class="form-table">
        <th><label for="my_hidden_sites"><?php _e(""); ?></label></th>
            <input type="hidden" name="my_hidden_sites" id="my_hidden_sites" value="<?php echo esc_attr( get_the_author_meta( 'my_hidden_sites', $user->ID ) ); ?>" class="regular-text" /><br />
            <span class="description"><?php _e(""); ?></span>
<?php }

add_action( 'personal_options_update', 'save_hidden_site_user_profile_fields' );
add_action( 'edit_user_profile_update', 'save_hidden_site_user_profile_fields' );

function save_hidden_site_user_profile_fields( $user_id ) {
    if ( !current_user_can( 'edit_user', $user_id ) ) { 
        return false; 
    update_user_meta( $user_id, 'my_hidden_sites', $_POST['my_hidden_sites'] );

function rampages_get_user_sites($user_id){
    $user_blogs = get_blogs_of_user( $user_id );
    echo 'Check the sites you would like to hide. Do not forget to update your profile at the bottom of this page.<ul>';
    foreach ($user_blogs AS $user_blog) {
        echo '<li class="hidden-list"><input type="checkbox" name="blog-' . $user_blog->userblog_id .'" id="blog-' . $user_blog->userblog_id .'" value="' . $user_blog->userblog_id . '"/> <label for="blog-' . $user_blog->userblog_id .'">'.$user_blog->blogname.'</label></li>';
    echo '</ul>';

Finally, there’s some javascript and a bit of CSS to write the data to the user profile field and make the interface a little better. I don’t think this is the best way to do this but it does work.

jQuery( document ).ready(function() {	

function watchHiddenBoxes(theIds){
	jQuery('.hidden-list input[type="checkbox"]').change(function() {
    	var theId = jQuery(this).val();
    	if (theIds.includes(theId)) {
    	} else {

function onOpen(){
	var hiddenSites = document.getElementById('my_hidden_sites');
	var blogIds = jQuery(hiddenSites).val();
	var theIds = blogIds.split(',');
	for (let i=0; i<theIds.length; i++) {
	  jQuery( "#blog-"+theIds[i] ).prop( "checked", true );
	return theIds;

function onChange(){
	var checked = jQuery('.hidden-list input[type="checkbox"]:checked');
	var newIds = [];
	for (let i=0; i<checked.length; i++) {
	var hiddenSites = document.getElementById('my_hidden_sites');
	 hiddenSites.value = newIds.join();

1 Or I’d have to write a new function to duplicate the capability of the original function.