Add Custom Tabs with Options on “Edit Site” Multisite Settings page

Before we begin, I will show you the desired result:

Creating custom tabs with options in site settings in WordPress Multisite

Let’s do it πŸš€

Step 1. Hook the tabs. Create a new one.

The first and the most simplest step begins with network_edit_site_nav_links. If you ever added custom table columns or hook bulk actions, you are very familiar with the look of this code.

add_filter( 'network_edit_site_nav_links', 'misha_new_siteinfo_tab' );

function misha_new_siteinfo_tab( $tabs ){

	$tabs['site-misha'] = array(
		'label' => 'Misha',
		'url' => 'sites.php?page=mishapage',
		'cap' => 'manage_sites'
	return $tabs;


Where to insert the code?

Okay, usually I do not describe this part, because for most of my tutorials by default – current theme (preferably child theme) or a custom plugin.

But not now! You code must be in a custom plugin which is active for the whole network.

Step 2. Trick with option pages

Ok, the interesting part is that the hook from the previous step (network_edit_site_nav_links) wasn’t supposed to be used for creating new tabs. Just for changing the default ones.

If you look at the URLs of the other tabs, like Info, Users etc, you will see that the tabs linked directly to php files.

So, what to do? πŸ€”

Just to make it simpler for you, I decided not to deal with the Settings API this time. Maybe I will publish the next tutorial about it.

 * Add submenu page under Sites
add_action( 'network_admin_menu', 'misha_new_page' );
function misha_new_page(){
		'Edit website', // will be displayed in <title>
		'Edit website', // doesn't matter
		'manage_network_options', // capabilities
		'misha_handle_admin_page' // the name of the function which displays the page

 * Some CSS tricks to hide the link to our custom submenu page
function misha_trick(){
	echo '<style>
	#menu-site .wp-submenu li:last-child{

 * Display the page and settings fields
function misha_handle_admin_page(){

	// do not worry about that, we will check it too
	$id = $_REQUEST['id'];

	// you can use $details = get_site( $id ) to add website specific detailes to the title
	$title = 'Misha\'s settings';

	echo '<div class="wrap"><h1 id="edit-site">' . $title . '</h1>
	<p class="edit-site-actions"><a href="' . esc_url( get_home_url( $id, '/' ) ) . '">Visit</a> | <a href="' . esc_url( get_admin_url( $id ) ) . '">Dashboard</a></p>';

		// navigation tabs
		network_edit_site_nav( array(
			'blog_id'  => $id,
			'selected' => 'site-misha' // current tab
		) );

		// more CSS tricks :)
		echo '
		#menu-site .wp-submenu li.wp-first-item{
		#menu-site .wp-submenu li.wp-first-item a{
		<form method="post" action="edit.php?action=mishaupdate">';
			wp_nonce_field( 'misha-check' . $id );
			echo '<input type="hidden" name="id" value="' . $id . '" />
			<table class="form-table">
					<th scope="row"><label for="some_field">Some option</label></th>
					<td><input name="some_field" class="regular-text" type="text" id="some_field" value="' . esc_attr( get_blog_option( $id, 'some_field') ) . '" /></td>
		echo '</form></div>';


 * Save settings
add_action('network_admin_edit_mishaupdate',  'misha_save_options');
function misha_save_options() {

	$blog_id = $_POST['id'];

	check_admin_referer('misha-check'.$blog_id); // security check

	update_blog_option( $blog_id, 'some_field', $_POST['some_field'] );

	wp_redirect( add_query_arg( array(
		'page' => 'mishapage',
		'id' => $blog_id,
		'updated' => 'true'), network_admin_url('sites.php')
	// redirect to /wp-admin/sites.php?page=mishapage&blog_id=ID&updated=true



add_action( 'network_admin_notices', 'misha_notice' );
function misha_notice() {

	if( isset( $_GET['updated'] ) && isset( $_GET['page'] ) && $_GET['page'] == 'mishapage' ) {

		echo '<div id="message" class="updated notice is-dismissible">
			<p>Congratulations!</p><button type="button" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>



Some comments about the code.

Step 3. $_GET validation

The last but not the least, we have to validate the some stuff. Consider that once you have implemented the first two steps, everything should work.

add_action( 'current_screen', 'misha_redirects' );
function misha_redirects(){

	// do nothing if we are on another page
	$screen = get_current_screen();
	if( $screen->id !== 'sites_page_mishapage-network' ) {

	// $id is a blog ID
	$id = isset( $_REQUEST['id'] ) ? intval( $_REQUEST['id'] ) : 0;

	if ( ! $id ) {
		wp_die( __('Invalid site ID.') );

	$details = get_site( $id );
	if ( ! $details ) {
		wp_die( __( 'The requested site does not exist.' ) );

	//if ( ! can_edit_network( $details->site_id ) ) {
	//	wp_die( __( 'Sorry, you are not allowed to access this page.' ), 403 );


This code through the error like below in case the $_GET variable doesn’t exist or doesn’t contain a correct blog ID. I commented the capability validation, because it has been also performed in Step 2, line 10.

Validation in WordPress Multisite settings page – website doesn't exist

That’s it! Read more Multisite stuff here or by the links below. Do not forget to leave a commend down below if you have a question about this tut πŸ‘‡

Misha Rudrastyh

Misha Rudrastyh

Hey guys and welcome to my website. For more than 10 years I've been doing my best to share with you some superb WordPress guides and tips for free.

Need some developer help? Contact me

Follow me on Twitter