Налаштування мультирегіонального сайту (різний контент по регіонах)
Мультирегіональний сайт — це один домен з різним контентом в залежності від регіону користувача. Московський користувач бачит актуальні ціни для Москви, краснодарський — свої акції та місцеві контакти, казахстанський — ціни в тенге. Технічно завдання складніше, ніж здається: потрібно узгодити визначення регіону, зберігання контенту, маршрутизацію, 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 тижні в залежності від кількості регіонів та обсягу контенту.







