Реалізація автоматичного перенаправлення по регіону на сайту
Автоматичний редирект по регіону — логічне продовження GeoIP-визначення. Користувач відкриває site.ru, система визначає його місто та перенаправляє на site.ru/spb/ або spb.site.ru. Завдання проста, але у неї есть кілька гострих кутів: петля редиректів, боти, користувачі з-за рубежу, кешування на CDN.
Базова логіка редиректа
// app/Http/Controllers/RegionRedirectController.php
class RegionRedirectController
{
public function __invoke(Request $request): RedirectResponse|Response
{
// Боти та пошукувачі не редиректим — вони обходять конкретні URL
if ($this->isCrawler($request->userAgent())) {
return app(HomeController::class)->index($request);
}
// Якщо користувач вже вибирав регіон — поважаємо його вибір
if ($preferred = $request->cookie('region')) {
return $this->redirectToRegion($preferred, $request);
}
// GeoIP визначення
$geo = app(GeoIpService::class)->lookupCached($request->ip());
$region = $this->matchRegion($geo['city'], $geo['region_name'], $geo['country_code']);
// Редирект 302 (не 301 — регіон може змінитися)
return $this->redirectToRegion($region->slug, $request)
->withCookie(cookie('region', $region->slug, 60 * 24 * 30)); // 30 днів
}
private function redirectToRegion(string $slug, Request $request): RedirectResponse
{
$path = $request->getPathInfo(); // '/' на головній
return redirect("/{$slug}{$path}", 302);
}
private function isCrawler(?string $ua): bool
{
if (!$ua) return false;
$bots = ['Googlebot', 'bingbot', 'YandexBot', 'Baiduspider',
'DuckDuckBot', 'Slurp', 'facebookexternalhit'];
foreach ($bots as $bot) {
if (str_contains($ua, $bot)) return true;
}
return false;
}
}
Захист від петлі редиректів
Найпоширеніша помилка — редирект запускається на вже регіональних URL. Middleware захищає:
// app/Http/Middleware/SkipRegionRedirect.php
public function handle(Request $request, Closure $next): Response
{
// URL уже містить slug регіону — пропускаємо
$regions = Region::pluck('slug')->toArray();
$firstSeg = explode('/', trim($request->getPathInfo(), '/'))[0] ?? '';
if (in_array($firstSeg, $regions, true)) {
return $next($request);
}
// Корневий URL — виконуємо редирект
return app(RegionRedirectController::class)($request);
}
Применяється лише до кореневого маршруту:
Route::get('/', RegionRedirectController::class)
->middleware(SkipRegionRedirect::class);
Сопоставлення міста з регіоном
GeoIP повертає назву міста англійською — потрібно сопоставити з внутрішнім slug:
private function matchRegion(
?string $city,
?string $region,
?string $country
): Region
{
// Спочатку по місту
if ($city) {
$match = RegionGeoMapping::where('city_en', $city)->first();
if ($match) return $match->region;
}
// По області/регіону
if ($region) {
$match = RegionGeoMapping::where('region_en', $region)->first();
if ($match) return $match->region;
}
// По країні — базовий регіон для країни
if ($country) {
$match = Region::where('country_code', $country)
->where('is_country_default', true)
->first();
if ($match) return $match;
}
// Глобальний дефолт
return Region::where('is_default', true)->firstOrFail();
}
Таблиця маппінгу:
CREATE TABLE region_geo_mappings (
id SERIAL PRIMARY KEY,
region_id INTEGER REFERENCES regions(id),
city_en VARCHAR(255), -- 'Saint Petersburg'
region_en VARCHAR(255), -- 'Saint Petersburg'
country CHAR(2) -- 'RU'
);
Кнопка смінирегіону
Користувач повинен мати можливість змінити автоопределений регіон. При зміні — обновляємо куку:
// AJAX endpoint
Route::post('/set-region/{slug}', function (string $slug, Request $request) {
abort_unless(Region::where('slug', $slug)->exists(), 404);
$redirectTo = $request->input('redirect_to', "/{$slug}/");
return response()->json(['redirect' => $redirectTo])
->withCookie(cookie('region', $slug, 60 * 24 * 365)); // 1 рік
});
На фронтенді — при кліку на регіон в шапці або попапі:
async function setRegion(slug) {
const res = await fetch(`/set-region/${slug}`, {
method: 'POST',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content },
body: JSON.stringify({ redirect_to: `/${slug}/` })
});
const { redirect } = await res.json();
window.location.href = redirect;
}
CDN та кешування
CDN (Cloudflare, Fastly) кешує сторінки. Якщо CDN кешує корень /, всі користувачи отримають один редирект — той, що був кеширований першим. Варіанти:
Варіант A: виключити / з кешу CDN (Cache-Control: no-store на кореневому URL).
Варіант B: використовувати Cloudflare Workers для редиректа на edge:
// Cloudflare Worker
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const cookie = request.headers.get('Cookie') || '';
const region = cookie.match(/region=([a-z]+)/)?.[1];
if (url.pathname === '/' && !region) {
const country = request.cf?.country || 'RU';
const slug = countryToRegion[country] || 'msk';
return Response.redirect(`${url.origin}/${slug}/`, 302);
}
return fetch(request);
}
Варіант C: редирект на клієнті через JS — найпростіший, але користувач бачить мигання сторінки.
Поведінка для іноземних користувачів
Якщо GeoIP вернув країну, якої немає у списку регіонів, — потрібне рішення:
- Показати попап «Виберіть регіон» без автоматичного редиректа
- Перенаправити на дефолтний регіон з попапом «Ви з іншої країни?»
- Перенаправити на окрему сторінку
site.ru/international/
Рішення залежить від частки іноземних користувачів в аудиторії.
Відладка
Для тестування редиректів з різних IP-адрес зручна query-параметр в dev-окружені:
if (app()->isLocal() && $testIp = $request->query('test_ip')) {
$geo = app(GeoIpService::class)->lookup($testIp);
}
https://site.dev/?test_ip=77.109.0.1 — симулює користувача з конкретним IP.







