Multisite Development
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages. |
Plugin code on a Multisite network runs for one site per request, yet may need to read other sites, keep network-wide settings and react when sites are created or deleted. This page covers the functions and hooks for that, what they cost, and the mistakes that appear once a second site exists. Setting up the network itself is in Multisite Networks.
This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the
version to run in production), PHP 7.4 minimum and 8.3+ recommended, and WP-CLI 2.12.0, and checked on 2026-10-28.
WordPress 7.2 is in beta and planned for 8 December 2026; it announces no change to
these APIs so far. Every output below comes from a 7.1.3 site (official wordpress:7.1.3-php8.4-apache and
wordpress:cli-2.12.0-php8.4 images, MariaDB 11.8, table prefix wp_) installed as a single site, checked, and then
converted to a subdirectory network at lumenpress.test with two imprint sites, Lumen Kids (/kids/, site 2)
and Lumen Noir (/noir/, site 3), created with wp site create. lumen-catalog is network-activated.
The plugin skeleton and its per-site activation loop are in Plugin Fundamentals, and the role on new sites in Roles and Capabilities API.
Detecting the network and the current site
The database vocabulary is inverted: a site (wp_site, get_current_network_id()) is a network and a blog
(wp_blogs, blog_id) is a site. Newer functions use the admin screens' words (get_sites(), get_site_meta(),
get_network_option()); older ones keep "blog" and "site" in the database sense (switch_to_blog(),
get_site_option(), which reads a network option).
| Function or property | Returns |
|---|---|
|
|
|
The ID of the site whose tables are in use, which changes with |
|
The network ID ( |
|
The network’s main site; avoid assuming it is |
|
A |
|
The current site’s table prefix ( |
wp-settings.php loads ms-blogs.php (which includes ms-site.php), ms-functions.php and WP_Site_Query only on
Multisite. Before the conversion the test site reported function_exists() false for get_sites, switch_to_blog and get_site_meta,
while the network option functions and is_super_admin() existed. lumen-catalog therefore calls Network::boot()
from Plugin::boot() only if ( is_multisite() ).
Switching sites
switch_to_blog( $site_id ) makes another site of the network current for the rest of the code until
restore_current_blog() is called. It does less than its name suggests.
| Changes | Does not change |
|---|---|
|
The plugins and must-use plugins loaded for the request, and the post types, taxonomies, blocks, shortcodes, hooks and REST routes they registered |
The object cache’s per-site key prefix ( |
The active theme’s PHP code ( |
|
|
The previous IDs are pushed on the $GLOBALS['_wp_switched_stack'] array. restore_current_blog() pops one entry
and returns false when the stack is empty; ms_is_switched() (3.5) is true while the stack is not empty. Every
switch fires the switch_blog action with $new_blog_id, $prev_blog_id and, since 5.4, a $context of switch or
restore, even when the target is already the current site. A short script, run with wp eval-file:
<?php
/**
* What switch_to_blog() changes, and what it does not. Run with: wp eval-file switch-demo.php
*/
global $wpdb;
$report = static function ( string $label ) use ( $wpdb ): void {
printf( "%-9s blog=%d prefix=%s base_prefix=%s switched=%s\n", $label, get_current_blog_id(), $wpdb->prefix, $wpdb->base_prefix, var_export( ms_is_switched(), true ) );
};
$report( 'start' );
switch_to_blog( 2 ); // Lumen Kids.
$report( 'kids' );
echo 'home_url(): ', home_url( '/' ), "\n";
echo 'get_permalink: ', get_permalink( get_page_by_path( 'the-paper-kite', OBJECT, 'lp_book' ) ), "\n";
switch_to_blog( 3 ); // Lumen Noir, nested.
echo 'stack: ', implode( ',', $GLOBALS['_wp_switched_stack'] ), "\n";
restore_current_blog();
restore_current_blog();
$report( 'restored' );
echo 'extra restore: ', var_export( restore_current_blog(), true ), "\n";
$ wp eval-file switch-demo.php
start blog=1 prefix=wp_ base_prefix=wp_ switched=false
kids blog=2 prefix=wp_2_ base_prefix=wp_ switched=true
home_url(): http://lumenpress.test/kids/
get_permalink: http://lumenpress.test/kids/blog/books/the-paper-kite/
stack: 1,2
restored blog=1 prefix=wp_ base_prefix=wp_ switched=false
extra restore: false
$ wp post list --post_type=lp_book --name=the-paper-kite --url=lumenpress.test/kids/ --fields=ID,url
ID url
3 http://lumenpress.test/kids/books/the-paper-kite/
Source: Code Reference: ms_is_switched()
The permalink built while switched is wrong. The main site of a subdirectory network has the forced /blog/ front in
its permalink structure, and the lp_book permastruct was registered with that front in the main site’s request, so
get_permalink() added /blog/ to a Lumen Kids URL, which answered only with a 301 redirect to the right address. Build
URLs of another site’s custom content in that site’s own context (its REST API, or a URL stored on save).
Capabilities do switch: current_user_can() answers for the target site. With maria.editor (an Editor of the
main site only) as current user, a wp eval loop printed site 1 roles=[editor] edit_posts=true and
site 3 roles=[] edit_posts=false. current_user_can_for_site( $site_id, $capability ) (6.7, replacing
current_user_can_for_blog()) does the switch and restore itself.
The object cache while switched
The default WP_Object_Cache keeps one array per request and prefixes the keys of non-global groups with the site
ID, so switching is cheap and the cache stays correct. Global groups (users, userlogins, site-options,
site-transient, networks, sites, blog-details and others declared with wp_cache_add_global_groups()) are
shared by every site, which is why a plugin that caches network-wide data adds its group there (see
Options, Transients and Object Cache).
A persistent drop-in should implement switch_to_blog() on its cache object. Since 7.0, for a drop-in that lacks it,
wp_cache_switch_to_blog() (in cache-compat.php) calls wp_cache_switch_to_blog_fallback(), which reinitialises the
cache and re-adds the global and non-persistent groups: correct, but it discards the in-memory cache on every switch.
The Redis Object Cache drop-in of Performance and Caching has the
method.
What a switch costs
On the test network with the default cache, 1,000 switch_to_blog()/restore_current_blog() pairs between the two
imprints took about 20 µs per pair, and the same loop with get_option( 'blogname' ) and wp_count_posts( 'lp_book' )
inside ran two queries in total. The switch is cheap; the first visit to each site is not, because it loads that
site’s autoloaded options and counts: about two queries per site (figures in A network-wide loop: books per imprint).
On a large network, page through sites, cache the result network-wide, run the work from WP-Cron or WP-CLI rather than
on a page view, or query the tables directly.
Querying sites with get_sites() and WP_Site_Query
get_sites( $args ) (4.6) wraps WP_Site_Query and returns WP_Site objects, IDs or a count. It replaces
wp_get_sites(), deprecated in 4.6. The results are cached in the global site-queries group and invalidated when a
site changes.
| Argument | Effect |
|---|---|
|
Maximum sites returned. Defaults to 100; |
|
|
|
Filter by network, address, search string or language. |
|
Status flags ( |
|
Site meta conditions, joined on |
|
Prime the site and site meta caches in one query each (both default |
|
Order by |
Source: Code Reference: WP_Site_Query
A WP_Site (4.5) mirrors a wp_blogs row, and its columns are strings: blog_id is "3", public is "1".
The magic property id returns an integer, and blogname, siteurl, post_count and home are loaded on demand from
the site’s options (a switch behind the scenes, then cached in site-details). Compare with === only after casting:
$ wp eval '$s = get_site( 3 ); var_dump( $s->blog_id, $s->id, $s->path, $s->public, $s->blogname );'
string(1) "3"
int(3)
string(6) "/noir/"
string(1) "1"
string(10) "Lumen Noir"
$ wp eval 'echo implode( ",", get_sites( array( "fields" => "ids", "path" => "/kids/" ) ) ), "\n"; $q = new WP_Site_Query( array( "number" => 2, "fields" => "ids", "no_found_rows" => false ) ); echo implode( ",", $q->sites ), " found_sites=", $q->found_sites, " max_num_pages=", $q->max_num_pages, "\n";'
2
1,2 found_sites=3 max_num_pages=2
Source: Code Reference: WP_Site
Network options
Network options live in wp_sitemeta, one row per network (site_id) and key, and hold settings that belong to the
network rather than to one site: core’s site_admins, active_sitewide_plugins and upload limits, and plugin settings
set by a super admin.
| Function | Notes |
|---|---|
|
Any network; |
|
Wrappers for the current network; the names predate the site/network renaming. |
|
Network-wide transients, stored as |
|
Short-circuit, filter, default and change hooks. |
Each value is cached in the global site-options group under "$network_id:$option", and missing keys in a
notoptions entry, so a missing option costs one query per request, not one per call. There is no autoload for network
options: only core’s own keys are preloaded (wp_load_core_site_options()), and each plugin key costs one query per
request without a persistent object cache. Group a plugin’s network settings into one array option, as lumen-catalog
does with lp_catalog_network_settings.
On a single site the same functions read and write wp_options. Before the conversion,
update_site_option( 'lp_catalog_network_settings', ... ) created a wp_options row with autoload off, so code
written with get_site_option() works unchanged on both kinds of installation. Network options need no
register_setting(): the Settings API and options.php work per site only, which is why network screens save through
network/edit.php (see A network settings screen). On the network, wp site option get
lp_catalog_network_settings --format=json reads the value.
Site meta
Site meta (5.1) attaches data to a site from outside it, in the network table wp_blogmeta, without switching. It is
the place for facts about a site that network code queries: an imprint code, a plan, a billing ID. Options of the site
itself remain the place for that site’s own settings.
| Function | Notes |
|---|---|
|
Wrappers of the metadata API for the object type |
|
|
|
Finds sites by meta, as |
Source: Code Reference: get_site_meta()
Lumen Press marks its imprint sites with lp_imprint, so network code finds them without hard-coding IDs:
$ wp site meta update 2 lp_imprint kids
Success: Updated custom field 'lp_imprint'.
$ wp site meta update 3 lp_imprint noir
Success: Updated custom field 'lp_imprint'.
$ wp site meta list 3
blog_id meta_key meta_value
3 lp_imprint noir
$ wp eval 'echo implode( ",", get_sites( array( "fields" => "ids", "meta_key" => "lp_imprint" ) ) ), "\n";'
2,3
Source: WP-CLI Commands: wp site meta
The Network class of lumen-catalog
All network behaviour of lumen-catalog sits in one class, booted from Plugin::boot() only on Multisite. It
creates the plugin’s data on new sites, drops its table with a deleted site, counts the published books of every
imprint, and adds a network settings screen.
src/Network.php
<?php
/**
* Network-level features: site life cycle, network settings and network-wide statistics.
*
* @package Lumen\Catalog
*/
namespace Lumen\Catalog;
defined( 'ABSPATH' ) || exit;
/**
* Booted from Plugin::boot() only when is_multisite() is true.
*/
final class Network {
const SETTINGS = 'lp_catalog_network_settings'; // Network option (wp_sitemeta).
const COUNTS = 'lp_network_book_counts'; // Site transient, shared by the network.
const IMPRINT = 'lp_imprint'; // Site meta key (wp_blogmeta).
const PAGE = 'lumen-catalog-network'; // Admin page slug and network/edit.php action.
public static function boot(): void {
add_action( 'wp_initialize_site', array( self::class, 'initialize_site' ), 900 );
add_filter( 'wpmu_drop_tables', array( self::class, 'drop_tables' ), 10, 2 );
add_action( 'wp_delete_site', array( self::class, 'flush_counts' ) );
add_action( 'save_post_lp_book', array( self::class, 'flush_counts' ) );
add_action( 'deleted_post', array( self::class, 'flush_counts' ), 10, 2 );
add_action( 'network_admin_menu', array( self::class, 'add_menu' ) );
add_action( 'network_admin_edit_' . self::PAGE, array( self::class, 'save_settings' ) );
}
public static function is_network_active(): bool {
if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php'; // Admin-only file.
}
return is_plugin_active_for_network( plugin_basename( LUMEN_CATALOG_FILE ) );
}
// A new site gets its table and settings now; priority 900 runs after core (10) created its tables.
public static function initialize_site( \WP_Site $site ): void {
if ( ! self::is_network_active() ) {
return;
}
$network = (array) get_site_option( self::SETTINGS, array() );
switch_to_blog( $site->id );
Installer::upgrade();
delete_option( 'rewrite_rules' ); // Built while switched, with the main site's /blog/ front: rebuild on first request.
if ( isset( $network['isbn_prefix'] ) ) {
update_option( 'lp_catalog_settings', array( 'isbn_prefix' => $network['isbn_prefix'] ) );
}
restore_current_blog();
}
// Core drops only its own tables (DROP TABLE IF EXISTS); add every per-site table of the plugin.
public static function drop_tables( array $tables, int $site_id ): array {
global $wpdb;
$prefix = $wpdb->get_blog_prefix( $site_id );
$tables[] = $prefix . 'lp_stock';
$tables[] = $prefix . 'lp_loans'; // Schema version 3, added in REST API: Extending It.
return $tables;
}
// Published books per imprint site, cached for the whole network for one hour.
public static function book_counts(): array {
$counts = get_site_transient( self::COUNTS );
if ( is_array( $counts ) ) {
return $counts;
}
$counts = array();
$site_ids = get_sites(
array(
'fields' => 'ids',
'meta_key' => self::IMPRINT, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- wp_blogmeta is small.
'archived' => 0,
'spam' => 0,
'deleted' => 0,
'number' => 500,
)
);
foreach ( $site_ids as $site_id ) {
switch_to_blog( $site_id );
$by_status = wp_count_posts( 'lp_book' );
$counts[ $site_id ] = array(
'imprint' => (string) get_site_meta( $site_id, self::IMPRINT, true ),
'name' => (string) get_option( 'blogname' ),
'books' => (int) ( $by_status->publish ?? 0 ),
);
restore_current_blog();
}
set_site_transient( self::COUNTS, $counts, HOUR_IN_SECONDS );
return $counts;
}
// Drops the cached counts when a book or a site changes ($post is set on deleted_post).
public static function flush_counts( $item = null, $post = null ): void {
if ( $post instanceof \WP_Post && 'lp_book' !== $post->post_type ) {
return;
}
delete_site_transient( self::COUNTS );
}
public static function add_menu(): void {
add_submenu_page( 'settings.php', __( 'Lumen Catalog network settings', 'lumen-catalog' ), __( 'Lumen Catalog', 'lumen-catalog' ), 'manage_network_options', self::PAGE, array( self::class, 'render_page' ) );
}
public static function render_page(): void {
$settings = (array) get_site_option( self::SETTINGS, array( 'isbn_prefix' => '978' ) );
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php if ( isset( $_GET['updated'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Settings saved.', 'lumen-catalog' ); ?></p></div>
<?php endif; ?>
<form method="post" action="<?php echo esc_url( network_admin_url( 'edit.php?action=' . self::PAGE ) ); ?>">
<?php wp_nonce_field( self::PAGE ); ?>
<p><label for="lp-isbn-prefix"><?php esc_html_e( 'ISBN prefix for new imprints', 'lumen-catalog' ); ?></label>
<input name="isbn_prefix" id="lp-isbn-prefix" type="text" class="small-text" value="<?php echo esc_attr( $settings['isbn_prefix'] ?? '978' ); ?>"></p>
<?php submit_button(); ?>
</form>
<h2><?php esc_html_e( 'Published books per imprint', 'lumen-catalog' ); ?></h2>
<table class="widefat striped">
<?php foreach ( self::book_counts() as $row ) : ?>
<tr><td><?php echo esc_html( $row['name'] ); ?></td><td><?php echo esc_html( number_format_i18n( $row['books'] ) ); ?></td></tr>
<?php endforeach; ?>
</table>
</div>
<?php
}
// Saves the form. network/edit.php checks only that the user is logged in.
public static function save_settings(): void {
if ( ! current_user_can( 'manage_network_options' ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to manage options for this network.', 'lumen-catalog' ), 403 );
}
check_admin_referer( self::PAGE );
$prefix = isset( $_POST['isbn_prefix'] ) ? sanitize_text_field( wp_unslash( $_POST['isbn_prefix'] ) ) : '978';
update_site_option( self::SETTINGS, array( 'isbn_prefix' => in_array( $prefix, array( '978', '979' ), true ) ? $prefix : '978' ) );
wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE, 'updated' => 'true' ), network_admin_url( 'settings.php' ) ) );
exit;
}
}
A network-wide loop: books per imprint
book_counts() finds the imprint sites by site meta, excludes archived, spam and deleted sites, switches once per site
and stores the result in a site transient, so the work runs once an hour for the whole network rather than on
every request of every site. Three books were published on the main site, two (later three) on Lumen Kids, and three
plus a draft on Lumen Noir:
$ wp eval 'foreach ( Lumen\Catalog\Network::book_counts() as $id => $row ) { printf( "%d %-5s %-11s %d\n", $id, $row["imprint"], $row["name"], $row["books"] ); }'
2 kids Lumen Kids 2
3 noir Lumen Noir 3
$ wp transient get lp_network_book_counts --network --format=json
{"2":{"imprint":"kids","name":"Lumen Kids","books":2},"3":{"imprint":"noir","name":"Lumen Noir","books":3}}
$ wp post create --post_type=lp_book --post_status=publish --post_title="Owl Post" --url=lumenpress.test/kids/ --porcelain
5
$ wp transient get lp_network_book_counts --network
Warning: Transient with key "lp_network_book_counts" is not set.
Source: Code Reference: get_sites()
The main site is not counted (it has no lp_imprint meta) and the draft is not published. Saving a book on Lumen Kids
fired save_post_lp_book there, and flush_counts() deleted the network transient: site transients are network-wide
from whichever site writes them. wp_count_posts() returns an empty object when the post type is not registered in
the current request, hence the ?? 0; a registered type stays registered after a switch even on a site where the
plugin is not active, so the count for such a site would still be read from its tables.
For hundreds of sites, read the tables directly instead of switching: $wpdb->get_blog_prefix( $id ) gives each
site’s prefix, so one UNION ALL of prepared per-site
SELECT %d AS site_id, COUNT(*) FROM {prefix}posts WHERE post_type = %s AND post_status = %s parts counts every
imprint in one query (2 queries and 1.4 ms for 42 sites, against 89 queries and 13 ms for the loop). Direct SQL skips every hook and cache that WP_Query and wp_count_posts() apply and depends
on the schema, so keep it for read-only statistics and keep writes on the APIs.
Site life cycle hooks
Since 5.1 a site’s life cycle has object-based hooks, and the old ones are deprecated:
| Hook | When |
|---|---|
|
After the |
|
Core’s |
|
Core’s callback at priority 10 drops the tables returned by |
|
The list of tables to drop; it contains only core’s per-site tables. |
|
After the row and its |
|
After a change of address or status flag. |
With the network setting at 979, a new site was ready at once: the table existed (checked from the main site, before
any request to the new one) and its settings carried the network default instead of the 978 that the upgrade routine
writes. Deleting it dropped wp_4_lp_stock with the core tables; without the wpmu_drop_tables filter, a deleted
site 5 left its wp_5_lp_stock behind. The filter also lists lp_loans, the second per-site table that schema
version 3 adds in REST API: Extending It; core drops tables with
DROP TABLE IF EXISTS, so the name is harmless on a site still at version 2, as here.
$ wp site create --slug=classics --title="Lumen Classics" --email=admin@lumenpress.test --porcelain
4
$ wp db query "SHOW TABLES LIKE 'wp\_4\_lp%'" --skip-column-names
wp_4_lp_stock
$ wp site delete 4 --yes
Success: The site at 'http://lumenpress.test/classics/' was deleted.
$ wp db query "SHOW TABLES LIKE 'wp\_4\_%'" --skip-column-names | wc -l
0
Source: Code Reference: wpmu_drop_tables
The delete_option( 'rewrite_rules' ) line fixes a problem found on the test network. Core’s site set-up rebuilds the
new site’s rewrite rules while switched from the main site, using the permastructs registered in that request, which
carry the main site’s /blog/ front: on Lumen Kids /kids/books/the-paper-kite/ returned 404 and the stored rules
started with blog/books/. Deleting the option makes WordPress rebuild the rules on the site’s first request, with its
own structure; with it, a new site’s /classics/books/old-harbour-tales/ returned 200. Never call
flush_rewrite_rules() inside switch_to_blog() for the same reason. Sites created earlier are repaired with
wp rewrite flush --url=lumenpress.test/kids/. The Catalogue Manager role on new sites follows the same pattern in
Roles and Capabilities API.
A network settings screen
Network Admin has its own menu hook, network_admin_menu; admin_menu does not fire there (see
Administration Menus and Settings API). Parent slugs are
the network screens: settings.php, sites.php, users.php, themes.php, plugins.php, index.php.
is_network_admin() tells code it is running there, and network_admin_url() builds its links.
Because the Settings API cannot save network options, the form posts to wp-admin/network/edit.php, which fires
network_admin_edit_{$action} for the action query argument and then redirects to the Network Admin dashboard.
That file checks only that the user is logged in, so the handler itself must check the capability and the nonce. With
cookies from wp-login.php for admin (super admin) and maria.editor (an Editor of the main site):
GET /wp-admin/network/settings.php?page=lumen-catalog-network (admin)
200, <h1>Lumen Catalog network settings</h1>, rows "Lumen Kids 3" and "Lumen Noir 3"
POST /wp-admin/network/edit.php?action=lumen-catalog-network (admin, with _wpnonce, isbn_prefix=978)
302 to /wp-admin/network/settings.php?page=lumen-catalog-network&updated=true
POST /wp-admin/network/edit.php?action=lumen-catalog-network (admin, no nonce)
403, "The link you followed has expired."
POST /wp-admin/network/edit.php?action=lumen-catalog-network (maria.editor)
403, "Sorry, you are not allowed to manage options for this network."
GET /wp-admin/network/settings.php?page=lumen-catalog-network (maria.editor)
403
Without the current_user_can() line, any logged-in user able to obtain a nonce for the action could change a network
setting. Admin notices on network screens use network_admin_notices, and plugin row links on Network Admin →
Plugins use network_admin_plugin_action_links.
Capabilities and network activation
Network screens check the network capabilities, which only super admins have: manage_network, manage_sites,
manage_network_users, manage_network_plugins, manage_network_themes and manage_network_options (the right one for
a network settings screen). Super admins, the site_admins network option and the capabilities a site Administrator
loses on a network are in Multisite Networks and
Roles and Capabilities API. Check
capabilities, never is_super_admin(), which on a single site is true for anyone with delete_users.
A network-activated plugin’s basename is a key of the active_sitewide_plugins network option (on the test network,
{"lumen-catalog/lumen-catalog.php":1793214653}, the value being the activation time); per-site activation stays in
each site’s active_plugins option. is_plugin_active_for_network( $basename ) reads that network option. It is
declared in wp-admin/includes/plugin.php: on a 7.1.3 front-end request it was already defined at plugins_loaded
but not at muplugins_loaded, so code keeps the require_once guard of is_network_active(). A Network: true
header forces network-only activation. Network activation runs the activation callback once, on the main site, with
$network_wide set to true; how lumen-catalog sets up the other sites is in
Plugin Fundamentals.
User names on a network
New user names on a network may contain only lowercase letters and digits ([a-z0-9], at least four characters), a
rule applied by wpmu_validate_user_signup(), which Network Admin, wp-signup.php and WP-CLI call. Lower-level
functions do not apply it: on the test network wp_insert_user() created nora.noir and wpmu_create_user()
created nora.x, both of which the screens would refuse. Plugins that create users on a network validate first:
$ wp user create nora.noir nora@lumenpress.test --role=editor --url=lumenpress.test/noir/
Error: Usernames can only contain lowercase letters (a-z) and numbers.
$ wp user create noranoir nora@lumenpress.test --role=editor --url=lumenpress.test/noir/ --porcelain
7
Common errors and pitfalls
| Symptom | Cause and fix |
|---|---|
|
Network-only functions on a single site. Load network code behind |
Later code reads the wrong site’s data |
A |
Wrong permalinks, or another site’s custom post type missing, after a switch |
Rewrite rules, post types and plugin code are those of the original request. Build URLs in the target site’s context. |
404 for custom post types on new sites |
Rewrite rules rebuilt while switched. Delete |
|
Plugin tables are not dropped by core. Add them with |
Any logged-in user can change a network setting |
The |
Where older books are outdated
| Older claim | Current behaviour (WordPress 7.1) |
|---|---|
Use |
Deprecated in 5.1; use |
List sites with |
|
Store per-site facts for network code in each site’s options and switch to read them |
Site meta (5.1) is read without switching and is queryable |
|
Only tables, the object cache prefix, roles and the current user switch |
|
Deprecated in 6.7 in favour of |