Site-Specific Settings in Multisite Sites > Info Page
When working with WordPress Multisite you have plenty of ways how you can add settings. You can add some custom options network-wide or you can still use classic options pages for each site of the network. For classic options pages I can recommend a superb plugin (it is me, who developed it).
But what if for your actual goal the best way to add settings is to Sites > Info pages of each site? Just like on the screenshot below:
In this tutorial I will show you how you can do that:

1. Add a Custom Tab
First things first, you have to add a custom tab into each site settings page. It quite easy to do, I mean you just have to use network_edit_site_nav_links
for that purpose.
add_filter( 'network_edit_site_nav_links', 'rudr_new_siteinfo_tab' );
function rudr_new_siteinfo_tab( $tabs ){
$tabs[ 'site-misha' ] = array(
'label' => 'Misha',
// 'url' => 'sites.php?page=mishapage',
'url' => add_query_arg( 'page', 'mishapage', 'sites.php' ),
'cap' => 'manage_sites'
);
return $tabs;
}
Let’s figure it out step by step how is this code meant to work:
- The first question is – where to insert this code? Because you can just insert it a usual way. I think the best idea would be to create a simple plugin and activate it network-wide.
- Now please take a look at line
7
, at query string?page=mishapage
specifically.mishapage
here is a custom page slug which will be part of the URL and we are also going to use it in the following steps of this tutorial.
Removing or renaming the default tabs
Action hook network_edit_site_nav_links
can be used not only for creating a custom tab but also in order to edit the default ones. It is as simple as working with arrays in PHP. Let’s do some stuff now.
add_filter( 'network_edit_site_nav_links', function( $tabs ) {
unset( $tabs[ 'site-themes' ] ); // site-users, site-info, site-settings
$tabs[ 'site-users' ][ 'label' ] = 'Humans';
return $tabs;
} );
And the result of this step:

2. Add a Page with Settings
The interesting part here is that network_edit_site_nav_links
hook is not supposed to be used for creating new tabs with some custom content. Even if you look at the URLs of the other tabs, like Info, Users etc, you will see that the tabs are linked directly to php files.
Create a hidden admin submenu page
The trick here is to create a regular network admin submenu page but without connection to any parent menu. So, in order to do that, we just have to pass null
as a first argument of add_submenu_page()
function.
Everything else is almost as usual, but remember that mishapage
slug should match to the slug you used in network_edit_site_nav_links
before.
<?php
add_action( 'network_admin_menu', 'rudr_new_page' );
function rudr_new_page(){
add_submenu_page( '', 'Edit site', 'Edit site', 'manage_network_options', 'mishapage', 'rudr_page_callback' );
}
function rudr_page_callback(){
// do not worry about that, we will check it too
$id = absint( $_REQUEST[ 'id' ] );
$site = get_site( $id );
?>
<div class="wrap">
<h1 id="edit-site">Edit Site: <?php echo $site->blogname ?></h1>
<p class="edit-site-actions">
<a href="<?php echo esc_url( get_home_url( $id, '/' ) ) ?>">Visit</a> | <a href="<?php echo esc_url( get_admin_url( $id ) ) ?>">Dashboard</a>
</p>
<?php
// navigation tabs
network_edit_site_nav(
array(
'blog_id' => $id,
'selected' => 'site-misha' // current tab
)
);
?>
<form method="post" action="edit.php?action=mishaupdate">
<?php wp_nonce_field( 'misha-check' . $id ); ?>
<input type="hidden" name="id" value="<?php echo $id ?>" />
<table class="form-table">
<tr>
<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="<?php echo esc_attr( get_blog_option( $id, 'some_field') ) ?>" /></td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
Save page settings
add_action( 'network_admin_edit_mishaupdate', 'rudr_save' );
function rudr_save() {
$id = absint( $_POST[ 'id' ] );
check_admin_referer( 'misha-check' . $id ); // nonce check
update_blog_option( $id, 'some_field', sanitize_text_field( $_POST[ 'some_field' ] ) );
// redirect to /wp-admin/sites.php?page=mishapage&blog_id=ID&updated=true
wp_safe_redirect(
add_query_arg(
array(
'page' => 'mishapage',
'id' => $id,
'updated' => 'true'
),
network_admin_url( 'sites.php' )
)
);
exit;
}
Just a reminder – never forget about proper data sanitization and escaping.
Notices
<?php
add_action( 'network_admin_notices', 'rudr_notice' );
function rudr_notice() {
if( isset( $_GET[ 'updated' ] ) && isset( $_GET[ 'page' ] ) && 'mishapage' === $_GET[ 'page' ] ) {
?>
<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>
</div>
<?php
}
}
Finally:

Page title
Almost forgot about it. In case you don’t want a PHP notice “Deprecated: strip_tags(): Passing null to parameter #1 ($string) of type string is deprecated” to appear at the top of your newly created page, you will have to think about its title as well (I mean what is inside <title>
tag).
I found the only way to set it – with current_screen
hook. Which is actually not that bad for this purpose.
add_action( 'current_screen', 'rudr_page_title' );
function rudr_page_title( $current_screen ) {
global $title;
if( 'sites_page_mishapage-network' === $current_screen->id && isset( $_GET[ 'id' ] ) && $_GET[ 'id' ] ) {
$blog_details = get_blog_details( array( 'blog_id' => $_GET[ 'id' ] ) );
$title = __( 'Edit Site:' ) . ' ' . $blog_details->blogname;
}
}
Site ID validation
This part of the tutorial is not 100% necessary, but it allows to avoid some errors when someone may try to access our options page directly without providing site ID to it as a URL parameter.
add_action( 'current_screen', 'rudr_double_check' );
function rudr_double_check(){
// do nothing if we are on another page
$screen = get_current_screen();
if( 'sites_page_mishapage-network' !== $screen->id ) {
return;
}
// $id is a blog ID
$id = isset( $_REQUEST[ 'id' ] ) ? absint( $_REQUEST[ 'id' ] ) : 0;
if ( ! $id ) {
wp_die( __( 'Incorrect site ID.' ) );
}
if ( ! get_site( $id ) ) {
wp_die( __( 'The requested site does not exist.' ) );
}
//if ( ! can_edit_network( $id ) ) {
// wp_die( __( 'Sorry, you are not allowed to access this page.' ), 403 );
//}
}
So, instead of receiving PHP warnings and fatal errors your lost users will get this notice:


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
This works great! I needed to add a sub-site expiration date field to my multisite installation, and your code is a great starting point.
Your code above works well with WP 6.1. Now I just need to modify it for my needs. Maybe some extra textarea fields for site notes. And a date field with a calendar popup.
Thanks!