WordPress Multisite: архитектура мультирегиональных сайтов на поддоменах
Постановка задачи
Мультирегиональный сайт на WordPress — это сеть из N поддоменов (msk.example.ru, spb.example.ru, kazan.example.ru), каждый из которых имеет уникальный контент (контакты филиала, товары в наличии, локальные новости), но при этом часть данных должна редактироваться централизованно из одной админ-панели. Классическое решение — WordPress Multisite в режиме subdomain_install.
Архитектура Multisite
Multisite — встроенный режим WordPress, активируемый через wp-config.php. После включения структура БД расширяется:
wp_blogs— реестр всех сайтов сети, каждый сайт получаетblog_id.wp_site— запись сети (в 99% случаев одна запись,site_id = 1).wp_sitemeta— сетевые настройки (аналогwp_options, но на уровне сети).wp_usermeta/wp_users— общие для всей сети.- Для каждого сайта создаются таблицы с префиксом
wp_{blog_id}_(wp_2_posts,wp_2_postmeta,wp_2_options,wp_2_termsи т.д.).
Суперадминистратор управляет сетью через wp-admin/network/. Темы и плагины устанавливаются на уровне сети; активация — на уровне отдельного сайта или принудительно для всей сети.
Пошаговая конфигурация
1. Активация Multisite
wp-config.php:
define('WP_ALLOW_MULTISITE', true);
После этого в «Инструменты» → «Установка сети» выбираем режим поддоменов. WordPress генерирует директивы для wp-config.php и .htaccess (или nginx конфиг).
define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', true);
define('DOMAIN_CURRENT_SITE', 'example.ru');
define('PATH_CURRENT_SITE', '/');
define('SITE_ID_CURRENT_SITE', 1);
define('BLOG_ID_CURRENT_SITE', 1);
2. Конфигурация веб-сервера
Apache (.htaccess):
RewriteEngine On
RewriteRule ^ - [E=HTTP_X_FORWARDED_PROTO:%{HTTP:X-Forwarded-Proto}]
RewriteCond %{HTTP_HOST} ^([^.]+)\.example\.ru$ [NC]
RewriteRule ^ - [L]
WordPress сам добавит нужные правила, но при нестандартной конфигурации (например, проксирование через Nginx) требуется ручная настройка.
Nginx (минимальная конфигурация):
server {
server_name example.ru *.example.ru;
root /var/www/example.ru;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Ключевой момент: server_name должен принимать wildcard-домен, чтобы все поддомены указывали на одну директорию.
3. DNS: wildcard-запись
В DNS-зоне домена добавляется:
* CNAME example.ru.
или A-запись:
Это обеспечивает резолвинг любого поддомена на тот же сервер.
4. SSL: wildcard-сертификат
Для HTTPS на всех поддоменах используется wildcard-сертификат *.example.ru. При Let’s Encrypt — через DNS-01 challenge:
certbot certonly --manual --preferred-challenges dns \
-d example.ru -d *.example.ru
Альтернатива — модуль mod_md для Apache или автоматизация через Certbot + плагин DNS-провайдера (Cloudflare, Yandex Cloud и т.д.).
5. sunrise.php для маппинга доменов
Если поддомены должны использовать разные доменные имена (не только *.example.ru, но и example-spb.ru), включается sunrise.php:
// wp-config.php
define('SUNRISE', true);
Файл wp-content/sunrise.php:
<?php
if (defined('SUNRISE') && SUNRISE) {
$host = $_SERVER['HTTP_HOST'];
if ($host === 'example-spb.ru') {
$_blog_id = get_blog_id_from_url('spb.example.ru');
if ($_blog_id) {
$blog_id = $_blog_id;
$current_blog = $blog_id;
}
}
}
Для стандартного сценария (все поддомены на одном домене) sunrise.php не требуется.
Организация общего контента
Главная задача мультирегионального проекта — часть данных редактируется один раз и отображается на всех сайтах сети. Рассмотрим три подхода.
Подход A: Сетевые опции (site options)
Используются функции update_site_option() / get_site_option(). Данные хранятся в таблице wp_sitemeta — доступны всем сайтам сети.
// Сохранение (выполняется в сетевой админке)
update_site_option('company_phone', '+7 (800) 555-35-35');
update_site_option('company_email', 'info@example.ru');
// Чтение (в теме любого сайта)
$phone = get_site_option('company_phone');
$email = get_site_option('company_email');
Для админ-интерфейса создаётся страница настроек сети:
add_action('network_admin_menu', function () {
add_submenu_page(
'settings.php',
'Общие контакты',
'Общие контакты',
'manage_network_options',
'network-contacts',
'render_network_contacts_page'
);
});
function render_network_contacts_page() {
if (isset($_POST['save_contacts'])) {
update_site_option('company_phone', sanitize_text_field($_POST['phone']));
update_site_option('company_email', sanitize_email($_POST['email']));
}
$phone = get_site_option('company_phone');
$email = get_site_option('company_email');
?>
<div class="wrap">
<h1>Общие контакты сети</h1>
<form method="post">
<table class="form-table">
<tr><th>Телефон</th>
<td><input type="text" name="phone" value="<?= esc_attr($phone) ?>"></td></tr>
<tr><th>Email</th>
<td><input type="email" name="email" value="<?= esc_attr($email) ?>"></td></tr>
</table>
<button type="submit" name="save_contacts" class="button-primary">Сохранить</button>
</form>
</div>
<?php
}
Плюсы: минимальная реализация, данные в одном месте, нет дублирования.
Минусы: подходит только для простых значений (строки, числа, массивы). Не годится для блоков контента с форматированием.
Подход B: Шорткоды, тянущие данные с главного сайта
«Эталонный» контент (например, страницу «О компании») создаём на главном сайте (blog_id = 1). На региональных сайтах выводим через шорткод, который подтягивает контент через switch_to_blog():
add_shortcode('shared_content', function ($atts) {
$post_id = $atts['id'] ?? 0;
if (!$post_id) return '';
$original_blog_id = get_current_blog_id();
switch_to_blog(1); // главный сайт
$post = get_post($post_id);
$content = $post ? apply_filters('the_content', $post->post_content) : '';
restore_current_blog();
return $content;
});
Использование на региональном сайте: [shared_content id="42"].
Плюсы: контент с форматированием, редактируется в визуальном редакторе. Минусы: switch_to_blog() — ресурсоёмкая операция; требуется агрессивное кэширование.
Подход C: REST API для распределённой архитектуры
Если региональные сайты физически разнесены по разным серверам, Multisite неприменим. Альтернатива — центральный WordPress-инстанс с REST API, отдающий общий контент:
// На центральном сервере: регистрация эндпоинта
add_action('rest_api_init', function () {
register_rest_route('shared/v1', '/content/(?P<slug>[a-z0-9-]+)', [
'methods' => 'GET',
'callback' => function ($request) {
$post = get_page_by_path($request['slug'], OBJECT, 'shared_block');
if (!$post) return new WP_REST_Response(null, 404);
return [
'title' => $post->post_title,
'content' => apply_filters('the_content', $post->post_content),
];
},
]);
});
На региональных сайтах — кэшированный запрос к API:
function fetch_shared_block($slug) {
$cache_key = 'shared_block_' . $slug;
$cached = get_transient($cache_key);
if ($cached !== false) return $cached;
$response = wp_remote_get("https://hub.example.ru/wp-json/shared/v1/content/{$slug}");
if (is_wp_error($response)) return '';
$data = json_decode(wp_remote_retrieve_body($response), true);
set_transient($cache_key, $data, HOUR_IN_SECONDS);
return $data['content'] ?? '';
}
SEO-настройка мультирегиональной сети
Каждый региональный сайт должен указывать альтернативные версии:
// В <head> темы
$regions = [
'example.ru' => 'ru',
'msk.example.ru' => 'ru-MOW',
'spb.example.ru' => 'ru-LED',
'kazan.example.ru'=> 'ru-KZN',
];
foreach ($regions as $domain => $locale) {
printf('<link rel="alternate" hreflang="%s" href="https://%s/" />%s',
esc_attr($locale), esc_attr($domain), "\n"
);
}
Гео-таргетинг
- Яндекс Вебмастер: каждый поддомен добавляется как отдельный сайт, регион указывается вручную.
- Google Search Console: поддомен добавляется как separate property, гео-таргетинг — через разметку или
hreflang. - Schema.org: на каждой странице —
LocalBusinessсaddressRegionиareaServed.
Каноникализация
При дублировании контента между регионами (например, общая страница «Услуги») используйте rel="canonical" на регион, который считается основным, либо наоборот — уникализируйте текст для каждого поддомена. Вариант с canonical:
if (get_current_blog_id() !== 1 && is_page('services')) {
remove_action('wp_head', 'rel_canonical');
add_action('wp_head', function () {
echo '<link rel="canonical" href="https://example.ru/services/" />' . "\n";
});
}
Кэширование и производительность
- Object Cache: Redis или Memcached — обязателен при количестве сайтов от 5+. Multisite активно использует
switch_to_blog(), каждый вызов — набор запросов кwp_optionsиwp_postsцелевого сайта. Object Cache снижает это до чтения из памяти. - Page Cache: Nginx FastCGI Cache, WP Rocket (с поддержкой Multisite) или Redis Cache. Ключ кэша должен учитывать
blog_id, иначе страницы поддоменов будут перепутаны. - CDN: при работе с одним доменом — без особенностей. При маппинге разных доменов — CDN должен поддерживать wildcard или multi-origin.
Пример ключа кэша для Nginx FastCGI:
fastcgi_cache_key "$scheme$request_method$host$request_uri";
$host здесь — критичен: он включает поддомен.
Типичные проблемы и решения
| Проблема | Причина | Решение |
|---|---|---|
| Поддомен отдаёт 404 | Нет wildcard DNS или сервер не принимает *.example.ru | Проверить DNS-запись и server_name |
| HTTPS на поддомене не работает | Сертификат без wildcard | Перевыпустить как *.example.ru |
| Контент «смешивается» между сайтами | Кэш не учитывает blog_id | Исправить ключ кэширования |
switch_to_blog() тормозит | Нет object cache | Включить Redis/Memcached |
| Плагин ломается на поддомене | Не поддерживает Multisite | Проверить через is_multisite() в коде плагина |
| Дубли контента в индексе | Нет hreflang или canonical | Добавить разметку, см. раздел SEO |
Чек-лист деплоя
- Бэкап БД и файлов.
- Wildcard DNS-запись.
- Wildcard SSL (Let’s Encrypt + DNS-01).
wp-config.phpс директивами Multisite.- Веб-сервер:
server_name *.example.ru. - Object Cache (Redis) включён.
- Сетевая страница настроек для общих контактов.
- Шорткод
[shared_content]вmu-plugin(доступен всем сайтам сети). hreflangв<head>.- Тест: создать тестовый поддомен, проверить HTTPS, контент, кэш.