Creating Multisite Settings Pages
This is all-in-one tutorial where I’m going to show you how to create two types of WordPress multisite settings pages.
Network options pages – they look just like regular WordPress options pages, but in the Network Admin dashboard.

Site-specific settings pages – they are displayed in tabs, so if you go to Sites > All Sites page and then click “Edit” link, you can find them there.

I’m going to dive deep into the explanation and the details, but if you want a quick solution, I’d recommend you to take a look at my Simple Fields plugin. I’m also going to show you examples how to create the same multisite settings pages with both custom coding and my plugin.
Network Options Pages
Let’s start with the network settings pages and create a settings page like this one:

Network admin menu
This is the first step we need to do and it is very similar to what we did when created regular options pages. The only change here is that we are going to use network_admin_menu hook instead of admin_menu.
But we are going to use add_menu_page() and add_submenu_page() functions in order to add new network settings pages. Let’s begin with a simple code like below:
add_action( 'network_admin_menu', 'rudr_network_settings_pages' );
function rudr_network_settings_pages() {
add_menu_page( 'Schedule', 'Schedule', 'manage_network_options', 'schedule-page', 'schedule_cb', 'dashicons-airplane' );
add_submenu_page( 'themes.php', 'More settings', 'More settings', 'manage_network_options', 'more-settings', 'more_settings_cb' );
}
function schedule_cb() {
}
function more_settings_cb() {
}Please keep in mind that the best place where you can insert this code is a network-wide activated custom plugin.
Here are the pages we added with the above code:

schedule_cb() and more_settings_cb() are empty. But we are going to add fields in just a little bit, in a next step.Also when using add_submenu_page() in the Network Dashboard you can use one of the slugs below:
| Slug | Parent page |
|---|---|
index.php | Dashboard |
sites.php | Sites |
users.php | Users |
themes.php | Themes |
plugins.php | Plugins |
settings.php | Settings |
Creating Network Settings Page without Settings API
Let’s begin with the moment that in order to save network settings we are going to use network_admin_edit_{ACTION} action hook. If you look through any of the available network options pages tutorials, you will see that this hook is used in every one of them. The thing is that when we create regular options pages in WordPress we do not have to do anything extra to save them into the database – Settings API is going to care about it. But now… we are using an action hook even if you’re going to use the method with the Settings API.
Adding fields:
<?php
function schedule_cb() {
$some_field = get_site_option( 'some_field' );
$some_checkbox = get_site_option( 'some_checkbox' );
?>
<div class="wrap">
<h1>Schedule</h1>
<form method="post" action="<?php echo add_query_arg( 'action', 'mishaaction', 'edit.php' ) ?>">
<?php wp_nonce_field( 'misha-validate' ); ?>
<h2>Section 1</h2>
<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( $some_field ) ?>" />
<p class="description">Field description can be added here.</p>
</td>
</tr>
</table>
<h2>Section 2</h2>
<table class="form-table">
<tr>
<th scope="row">Some checkbox</th>
<td>
<label>
<input name="some_checkbox" type="checkbox" value="yes" <?php checked( 'yes', $some_checkbox ) ?>> Yes, check this checkbox
</label>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}Now it is time to save the field values. Please keep in mind, that network_admin_edit_ hook contains an action parameter which is a part of our form’s action HTML attribute, in our example it will be <form action="edit.php?action=mishaaction">.
// add_action( 'network_admin_edit_{ACTION}', 'rudr_save_settings' );
add_action( 'network_admin_edit_mishaaction', 'rudr_save_settings' );
function rudr_save_settings(){
check_admin_referer( 'misha-validate' ); // Nonce security check
update_site_option( 'some_field', sanitize_text_field( $_POST[ 'some_field' ] ) );
$checkbox = isset( $_POST[ 'some_checkbox' ] ) && 'yes' === $_POST[ 'some_checkbox' ] ? 'yes' : 'no';
update_site_option( 'some_checkbox', $checkbox );
wp_safe_redirect(
add_query_arg(
array(
'page' => 'schedule-page',
'updated' => true
),
network_admin_url( 'admin.php' )
)
);
exit;
}Functions get_option() and update_option() should be familiar to you, they work with the current blog settings in the database. There are also similar functions get_blog_option() and update_blog_option() that allow to work with options of a specific blog of a network, example is below. But in this example we’re working with get_site_option() and update_site_option() because these functions manage the settings of the whole network.
When saving options do not forget about sanitization.
The last but not the least, let’s create notices. For regular option pages notices we used admin_notices hook, for network options pages all is the same, but just use a different hook – network_admin_notices.
<?php
add_action( 'network_admin_notices', 'rudr_notice' );
function rudr_notice(){
if( isset( $_GET[ 'page' ] ) && 'schedule-page' === $_GET[ 'page' ] && isset( $_GET[ 'updated' ] ) ) {
?><div id="message" class="updated notice"><p>Settings updated. You're the best!</p></div><?php
}
}The result:

Creating Network Settings Page with Settings API
Ok, but let’s say that you decided to use Settings API in order to create a network settings page. Is that possible?
The bad news is that Settings API in WordPress is not fully ready for multisite options pages and I am not sure it will ever be, but yes, you can still use it in some way.
Let me show you how. First of all let’s use either network_admin_menu or admin_init hook to register settings.
add_action( 'admin_init', 'rudr_register_network_settings' );
function rudr_register_network_settings() {
add_settings_section( 'section1', 'Section 1', false, 'misha_network_settings_123' );
register_setting(
'misha_network_settings_123', // settings name
'some_field', // option name
array( 'sanitize_callback'=> 'sanitize_text_field' ) // sanitization function
);
add_settings_field(
'some_field', // option name
'Some option', // field label
'field_1_callback', // callback function which will print the HTML of the field
'misha_network_settings_123', // settings name
'section1' // section name
);
add_settings_section( 'section2', 'Section 2', false, 'misha_network_settings_123' );
register_setting( 'misha_network_settings_123', 'some_field' );
add_settings_field( 'some_checkbox', 'Some checkbox', 'field_2_callback', 'misha_network_settings_123', 'section2' );
}Now we can do some changes in schedule_cb() function which we specified earlier.
<?php
function schedule_cb() {
?>
<div class="wrap">
<h1>Schedule</h1>
<form method="post" action="<?php echo add_query_arg( 'action', 'mishaaction', 'edit.php' ) ?>">
<?php
settings_fields( 'misha_network_settings_123' );
do_settings_sections( 'misha_network_settings_123' );
submit_button();
?>
</form>
</div>
<?php
}The last but not least – let’s provide functions field_1_callback() and field_2_callback() which are going to display the HTML of the settings fields.
<?php
function field_1_callback() {
$some_field = get_site_option( 'some_field' );
?>
<input name="some_field" class="regular-text" type="text" id="some_field" value="<?php echo esc_attr( $some_field ) ?>" />
<p class="description">Field description can be added here.</p>
<?php
}
function field_2_callback() {
$some_checkbox = get_site_option( 'some_checkbox' );
?>
<label>
<input name="some_checkbox" type="checkbox" value="yes" <?php checked( 'yes', $some_checkbox ) ?>> Yes, check this checkbox
</label>
<?php
}That’s pretty much it, don’t forget that you will also need rudr_save_settings() function we created in the previous chapter almost without changes – just remove check_admin_referer() from there.
Creating WordPress multisite settings pages with the plugin
In case you decided to save a little bit of your time when developing projects, you can easily do it with my Simple Fields plugin, because in that case the same settings page can be created with just a small code snippet below.
add_filter( 'simple_register_option_pages', function( $network_option_pages ) {
$network_option_pages[] = array(
'id' => 'schedule-page',
'title' => 'Schedule',
'menu_name' => 'Schedule',
'icon' => 'dashicons-airplane',
'network' => true,
'sections' => array(
array(
'id' => 'section1',
'name' => 'Section 1',
'fields' => array(
array(
'id' => 'some_field',
'label' => 'Some option',
'type' => 'text',
'description' => 'Field description can be added here.',
),
),
),
array(
'id' => 'section2',
'name' => 'Section 2',
'fields' => array(
array(
'id' => 'some_checkbox',
'label' => 'Some checkbox',
'short_description' => 'Yes, check this checkbox',
'type' => 'checkbox',
),
),
),
)
);
return $network_option_pages;
} );Can you believe it?
Site-specific Settings Pages
But what if in your case it would be better to add settings pages to All Sites > Sites page, when you edit a specific site of the network.
Something like this:

I’m also going to show you how to code a page like this and how to create it with my plugin.
Creating and Managing tabs
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;
}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.
Action hook network_edit_site_nav_links can be used not only for creating a custom tab, it also allows 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:

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.
Creating a settings page for our custom tab
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
}We also need to save the settings for sure:
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>
</div>
<?php
}
}Finally:

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;
}
}It is also a great idea to add a Site ID validation, it 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:

Creating a site settings tab with my plugin
The same site-specific settings tab and page can be created with this simple code snippet.
add_filter( 'simple_register_site_settings', 'misha_site_settings' );
function misha_site_settings( $settings ) {
$settings[] = array(
'id' => 'site-misha',
'tab_name' => 'Misha',
'fields' => array(
array(
'id' => 'some_field',
'label' => 'Some Field',
'type' => 'text',
),
)
);
return $settings;
}But of course you need to have my Simple Fields plugin installed on your multisite network.
Misha Rudrastyh
Hey guys and welcome to my website. For more than 15 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
Hello Misha,
There are multiple helping tutorial about Multisite.Good work and easy to understand.
I have a question, is it possible to host multiple WordPress website on One database? And every site have different theme, plugins and domain.
Thanks
Hey,
Yes, absolutely, just use different database prefixes.
If i change the prefix for every WordPress then how to share (category, tags, CPT, Meta_box, images, etc) between all WordPress?
Is there anything to change in wp core files?
Maybe in your situation it is better to use WordPress Multisite. Did you consider it?
Thanks for the article, very helpful. One thing you didn’t mention is that you have to use
add_site_option()before you callget_site_option().Hmmm, that would be strange
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!
not sure why but I’m getting “no access” error page while movig to this tab… Any suggestios?… Followed whole tutorial but renamed functions and labels to my own…