WordPress Multisite allows a single WordPress installation to host a network of independent sites — each with its own domain, theme, and plugin configuration — sharing a single database, user table, and codebase. For businesses running country-specific subdomains, agency networks, or multi-brand publishing platforms, Multisite reduces server overhead, centralises WordPress core and plugin updates, and simplifies user management across all network sites. From an SEO perspective, Multisite presents both opportunities and pitfalls: each network site needs its own canonical domain, its own sitemap, and properly configured hreflang tags if serving the same content in multiple languages across subdomains. The network admin panel controls network-wide plugin and theme activation, while individual site admins retain control over content and settings that do not require network-level privileges. Subdomain networks (fr.example.com) require a wildcard DNS record and a wildcard SSL certificate; subdirectory networks (example.com/fr/) work on a standard single-domain certificate and are simpler to set up but harder to migrate to individual domains later. Domain mapping — assigning a custom top-level domain to a network subsite — requires either the DOMAIN_CURRENT_SITE constant in wp-config.php or the WordPress MU Domain Mapping plugin for sites on WordPress.com-style hosting. wp_get_sites() (deprecated) and get_sites() iterate over all network sites, enabling network-wide operations such as cross-site search or aggregated sitemap generation. Super Admins can switch between network sites using switch_to_blog() and restore_current_blog() — essential for network plugins that query data from multiple sites in a single request. The sitemap extension post shows the provider pattern that can be adapted to generate a network-wide sitemap index pointing to per-site sub-sitemaps. The canonical URL post is especially important in Multisite, where cross-posted content on multiple network sites must declare a single canonical origin to avoid duplicate-content penalties. Test Multisite locally with a wildcard entry in /etc/hosts before deploying to production — 127.0.0.1 *.local.test enables all subdomains to resolve to localhost during development.
Problem: Setting up WordPress Multisite for a multilingual or multi-brand network requires non-obvious wp-config.php constants, .htaccess changes, and per-site SEO configuration that are easy to misconfigure, causing broken rewrites, duplicate content, or missing sitemaps across network sites.
Solution: Enable Multisite by adding the required constants to wp-config.php, update .htaccess with the network rewrite rules, configure a wildcard subdomain or subdirectory structure, and add per-site canonical and sitemap hooks in a network-active must-use plugin.
// 1. Add to wp-config.php BEFORE the "That's all" line
define('WP_ALLOW_MULTISITE', true);
// After running Tools → Network Setup, WordPress adds these automatically:
define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', true); // false = subdirectory network
define('DOMAIN_CURRENT_SITE', 'helloadmin.com');
define('PATH_CURRENT_SITE', '/');
define('SITE_ID_CURRENT_SITE', 1);
define('BLOG_ID_CURRENT_SITE', 1);
// 2. wp-content/mu-plugins/network-seo.php
// Ensure each network site declares its own canonical and sitemap
// Add hreflang tags for a bilingual network (en + fr subsites)
add_action('wp_head', function() {
if (!is_multisite()) return;
$sites = get_sites(['number' => 50, 'public' => 1]);
foreach ($sites as $site) {
switch_to_blog($site->blog_id);
$lang = get_bloginfo('language'); // e.g. "en-US", "fr-FR"
$homeUrl = trailingslashit(home_url());
restore_current_blog();
printf(
'' . "
",
esc_attr(strtolower(str_replace('_', '-', $lang))),
esc_url($homeUrl)
);
}
// x-default points to the primary site
printf('' . "
",
esc_url(network_home_url('/')));
});
// Cross-site query: get recent posts from all network sites
function get_network_recent_posts(int $per_site = 3): array {
$posts = [];
foreach (get_sites(['number' => 20]) as $site) {
switch_to_blog($site->blog_id);
$recent = get_posts(['numberposts' => $per_site, 'post_status' => 'publish']);
foreach ($recent as $p) {
$posts[] = [
'title' => get_the_title($p),
'url' => get_permalink($p),
'blog_id' => $site->blog_id,
'date' => $p->post_date,
];
}
restore_current_blog();
}
usort($posts, fn($a, $b) => strcmp($b['date'], $a['date']));
return array_slice($posts, 0, 10);
}
NOTE: Always call restore_current_blog() after every switch_to_blog() — failing to restore context corrupts global WordPress state for the rest of the request, causing queries to run against the wrong site's tables and options.