Настройка мультирегионального сайта (разный контент по регионам)
Мультирегиональный сайт — это один домен с разным контентом в зависимости от региона пользователя. Московский пользователь видит актуальные цены для Москвы, краснодарский — свои акции и местные контакты, казахстанский — цены в тенге. Технически задача сложнее, чем кажется: нужно согласовать определение региона, хранение контента, маршрутизацию, SEO и кэширование.
Архитектурные варианты
Вариант 1: Поддиректории — site.ru/msk/, site.ru/spb/, site.ru/krd/
Проще всего для SEO, понятно для пользователя, легко реализуется на любом фреймворке. Google индексирует каждый регион отдельно.
Вариант 2: Поддомены — msk.site.ru, spb.site.ru
Требует wildcard-сертификат и DNS-запись *.site.ru. Проще разделить кэш по регионам на уровне CDN. Технически чище, но Google может воспринять поддомены как разные сайты — хуже для общего авторитета домена.
Вариант 3: Автоопределение без изменения URL
Пользователь всегда на site.ru, регион определяется по IP. Плохо для SEO — Googlebot не обходит контент разных регионов, всегда видит один. Подходит только если региональный контент не нужно индексировать.
Для большинства проектов оптимален вариант 1.
Модель данных
CREATE TABLE regions (
id SERIAL PRIMARY KEY,
slug VARCHAR(16) UNIQUE NOT NULL, -- 'msk', 'spb', 'krd'
name VARCHAR(128) NOT NULL,
is_default BOOLEAN DEFAULT false,
currency VARCHAR(3) DEFAULT 'RUB',
phone VARCHAR(32),
address TEXT
);
-- Региональные переопределения контента
CREATE TABLE content_region_overrides (
content_id INTEGER NOT NULL,
region_id INTEGER NOT NULL REFERENCES regions(id),
field VARCHAR(64) NOT NULL, -- 'price', 'title', 'body'
value TEXT,
PRIMARY KEY (content_id, region_id, field)
);
Базовый контент хранится в основной таблице. Региональные переопределения — только то, что отличается. Это экономит место и упрощает синхронизацию: при изменении базового контента регионы, у которых нет override, автоматически получают новую версию.
Маршрутизация (Laravel)
// routes/web.php
Route::prefix('{region}')
->where(['region' => '[a-z]{2,8}'])
->middleware('region.resolve')
->group(function () {
Route::get('/', [HomeController::class, 'index']);
Route::get('/catalog/{slug}', [CatalogController::class, 'show']);
Route::get('/contacts', [ContactsController::class, 'index']);
});
// Корень без региона — редирект на определённый регион
Route::get('/', RegionDetectController::class);
// app/Http/Middleware/ResolveRegion.php
public function handle(Request $request, Closure $next): Response
{
$slug = $request->route('region');
$region = Region::where('slug', $slug)->firstOrFail();
// Доступен во всём приложении через singleton
app()->instance('current.region', $region);
View::share('currentRegion', $region);
return $next($request);
}
Определение региона при первом заходе
// app/Http/Controllers/RegionDetectController.php
public function __invoke(Request $request): RedirectResponse
{
// 1. Сохранённый регион в куке
if ($saved = $request->cookie('preferred_region')) {
if (Region::where('slug', $saved)->exists()) {
return redirect("/{$saved}/");
}
}
// 2. Определение по IP через MaxMind GeoIP2
$reader = new \GeoIp2\Database\Reader(storage_path('geoip/GeoLite2-City.mmdb'));
try {
$record = $reader->city($request->ip());
$citySlug = $this->mapCityToRegion($record->city->name);
} catch (\Exception) {
$citySlug = null;
}
$slug = $citySlug ?? Region::where('is_default', true)->value('slug');
return redirect("/{$slug}/")->withCookie(
cookie('preferred_region', $slug, 60 * 24 * 365)
);
}
Получение регионального контента
trait HasRegionalContent
{
public function getRegionalField(string $field, ?Region $region = null): mixed
{
$region ??= app('current.region');
$override = ContentRegionOverride::where('content_id', $this->id)
->where('region_id', $region->id)
->where('field', $field)
->value('value');
return $override ?? $this->$field;
}
}
Использование: $product->getRegionalField('price') — вернёт региональную цену или базовую, если override не задан.
SEO: hreflang и sitemap
Для корректной региональной индексации каждая страница должна содержать hreflang-теги:
<link rel="alternate" hreflang="ru-RU" href="https://site.ru/msk/catalog/product-1" />
<link rel="alternate" hreflang="ru-KZ" href="https://site.ru/kz/catalog/product-1" />
<link rel="alternate" hreflang="x-default" href="https://site.ru/msk/catalog/product-1" />
Региональный sitemap генерируется отдельно для каждого региона и включается в sitemap_index.xml.
Кэширование
При использовании Nginx или Varnish ключ кэша должен включать регион:
# nginx fastcgi_cache
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# URI уже содержит /msk/ — кэш автоматически разделяется по регионам
Для Redis-кэша Laravel:
$cacheKey = "catalog.{$region->slug}.{$slug}";
Cache::remember($cacheKey, 3600, fn() => $this->buildPage($slug, $region));
Административная часть
В CMS нужно предусмотреть:
- Переключатель региона в интерфейсе редактирования
- Визуальное выделение полей с региональным override
- Массовое применение override на группу товаров
- Отчёт: какие страницы имеют региональные версии, а какие нет
Сроки и этапы
| Этап | Содержание | Срок |
|---|---|---|
| 1 | Модель данных, миграции, CRUD регионов | 2 дня |
| 2 | Маршрутизация, middleware, GeoIP | 2 дня |
| 3 | Региональный контент в шаблонах | 3 дня |
| 4 | SEO: hreflang, sitemap | 1 день |
| 5 | Административный интерфейс | 3 дня |
| 6 | Кэширование, нагрузочное тестирование | 2 дня |
Итого: 2–3 недели в зависимости от количества регионов и объёма контента.







