Реалізація автоматичного перенаправлення по регіону на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація автоматичного перенаправлення по регіону на сайті
Проста
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація автоматичного перенаправлення по регіону на сайту

Автоматичний редирект по регіону — логічне продовження 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.