Налаштування мультидоменного сайту (різні домени по країнах)
Коли бізнес працює в кількох країнах, кожна нерідко отримує власний домен: company.ru, company.kz, company.by, company.ua. Один кодовий движок обслуговує всі домени — різні мови, ціни, юридичні тексти, телефони. Складніше за поддиректорії, але дає максимальний локальний SEO-сигнал та повну ізоляцію контенту.
Конфігурація доменів
Центральна таблиця доменів зв'язує хост із налаштуваннями:
CREATE TABLE site_domains (
id SERIAL PRIMARY KEY,
host VARCHAR(253) UNIQUE NOT NULL, -- 'company.ru'
country_code CHAR(2) NOT NULL, -- 'RU', 'KZ', 'BY'
locale VARCHAR(10) NOT NULL, -- 'ru', 'kk', 'be'
currency CHAR(3) NOT NULL, -- 'RUB', 'KZT', 'BYN'
timezone VARCHAR(64) NOT NULL,
is_primary BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
meta JSONB DEFAULT '{}'
);
INSERT INTO site_domains VALUES
(DEFAULT, 'company.ru', 'RU', 'ru', 'RUB', 'Europe/Moscow', true, true, '{}'),
(DEFAULT, 'company.kz', 'KZ', 'kk', 'KZT', 'Asia/Almaty', false, true, '{}'),
(DEFAULT, 'company.by', 'BY', 'ru', 'BYN', 'Europe/Minsk', false, true, '{}');
Middleware визначення домену
// app/Http/Middleware/ResolveSiteDomain.php
class ResolveSiteDomain
{
public function handle(Request $request, Closure $next): Response
{
$host = $request->getHost(); // 'company.kz'
$domain = SiteDomain::where('host', $host)
->where('is_active', true)
->first();
if (!$domain) {
// Невідомий домен — редирект на основний
$primary = SiteDomain::where('is_primary', true)->firstOrFail();
return redirect("https://{$primary->host}" . $request->getRequestUri(), 301);
}
app()->instance('site.domain', $domain);
// Встановлюємо локаль та часовий пояс
App::setLocale($domain->locale);
Carbon::setlocale($domain->locale);
date_default_timezone_set($domain->timezone);
return $next($request);
}
}
Мультитенантна конфігурація Laravel
// app/Providers/DomainServiceProvider.php
public function boot(): void
{
$this->app->resolving('current.domain', function () {
return app('site.domain');
});
// Переопределяємо mail from для кожного домену
$this->app['events']->listen(MessageSending::class, function ($event) {
$domain = app('site.domain');
config([
'mail.from.address' => "noreply@{$domain->host}",
'mail.from.name' => config('app.name') . ' ' . strtoupper($domain->country_code),
]);
});
}
Зберігання контенту по доменах
-- Переводимі тексти привязані до домену
CREATE TABLE pages (
id SERIAL PRIMARY KEY,
slug VARCHAR(255) NOT NULL,
domain_id INTEGER REFERENCES site_domains(id),
-- NULL в domain_id = загальний контент для всіх доменів
UNIQUE (slug, domain_id)
);
CREATE TABLE page_translations (
page_id INTEGER REFERENCES pages(id),
locale VARCHAR(10) NOT NULL,
title TEXT,
body TEXT,
meta_title TEXT,
meta_desc TEXT,
PRIMARY KEY (page_id, locale)
);
Запит контенту з fallback на загальний:
public function getPage(string $slug): Page
{
$domain = app('site.domain');
// Спочатку ищемо сторінку, специфічну для домену
$page = Page::where('slug', $slug)
->where('domain_id', $domain->id)
->first();
// Fallback на загальну сторінку
$page ??= Page::where('slug', $slug)
->whereNull('domain_id')
->firstOrFail();
return $page;
}
Nginx: віртуальні хости для PHP-FPM
# /etc/nginx/sites-available/multisite.conf
server {
listen 443 ssl http2;
server_name company.ru company.kz company.by;
ssl_certificate /etc/letsencrypt/live/company.ru/fullchain.pem;
# Wildcard сертифіката або multi-domain SAN
root /var/www/company/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param HTTP_HOST $host; # Передаємо реальний хост у PHP
include fastcgi_params;
}
}
Якщо домени на різних серверах — додається балансувальник з заголовком X-Forwarded-Host, який Laravel читає через TrustProxies middleware.
SSL-сертифікати
Certbot із мультидоменним SAN:
certbot certonly --nginx \
-d company.ru -d www.company.ru \
-d company.kz -d www.company.kz \
-d company.by -d www.company.by \
--email [email protected] \
--agree-tos
Альтернатива — wildcard *.company.ru + DNS challenge.
Crossdomain SEO
Кожен домен — самостійний сайт в очах Google. Для зв'язку між ними використовують hreflang в <head>:
<!-- На company.ru -->
<link rel="alternate" hreflang="ru-RU" href="https://company.ru/products/item-1" />
<link rel="alternate" hreflang="ru-KZ" href="https://company.kz/products/item-1" />
<link rel="alternate" hreflang="ru-BY" href="https://company.by/products/item-1" />
Генерація через хелпер:
function hreflangTags(string $path): string
{
$domains = SiteDomain::where('is_active', true)->get();
return $domains->map(fn($d) =>
"<link rel=\"alternate\" hreflang=\"{$d->locale}-{$d->country_code}\" href=\"https://{$d->host}{$path}\" />"
)->join("\n");
}
Проблема з куками та сесіями
При переході користувача між доменами сесія теряется — куки не передаються між різними доменами. Варіанти:
SSO через shared token: при кліку «Перейти на сайт для Казахстану» генерується одноразовий токен, користувач переадресується з ним, другий домен обмінює токен на сесію.
Загальне сховище сесій: Redis з одним і тим же SESSION_DOMAIN — але браузер все одно не передасть куку з .ru на .kz. Цей підхід працює лише для поддоменів.
Миграція при додаванні домену
Додавання нового домену займає менше години:
- Реєстрація домену, налаштування DNS A-записів — від хвилин до доби (propagation)
- Додання в
site_domains— 2 хвилини - Додання в SAN сертифіката (
certbot --expand) — 5 хвилин - Додання
server_nameв Nginx — 1 хвилина - Імпорт або створення контенту — залежить від обсягу
Мониторинг
Окрема health check сторінка /health на кожному домені. Uptime Robot або Checkly пінгує всі домени щохвилини, алерт при недоступності. Перевірка SSL expiry окремо — через ssl_certificate_expire метрику в Prometheus або сторонній сервіс.







