WordPress Multisite
|

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-запись:

* A 203.0.113.10

Это обеспечивает резолвинг любого поддомена на тот же сервер.

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

Чек-лист деплоя

  1. Бэкап БД и файлов.
  2. Wildcard DNS-запись.
  3. Wildcard SSL (Let’s Encrypt + DNS-01).
  4. wp-config.php с директивами Multisite.
  5. Веб-сервер: server_name *.example.ru.
  6. Object Cache (Redis) включён.
  7. Сетевая страница настроек для общих контактов.
  8. Шорткод [shared_content] в mu-plugin (доступен всем сайтам сети).
  9. hreflang в <head>.
  10. Тест: создать тестовый поддомен, проверить HTTPS, контент, кэш.

Похожие записи