Тема
Blueprint: Мультигород (Multicity)
Когда применять
Один сайт обслуживает несколько городов: «сайт для Краснодара и Сочи», региональные цены/телефоны, SEO-страницы под города. Фирменный типовой модуль студии.
Не применять при: единственном городе «на вырост» (добавить потом дешевле, чем таскать сложность), полностью разных сайтах по городам (это мультисайт).
Ключевое решение №1: схема URL
Выбрать с клиентом до реализации, менять потом дорого:
| Схема | Плюсы | Минусы |
|---|---|---|
Путь /msk/uslugi (по умолчанию) | Один домен, простой деплой и SSL | Придётся аккуратно с корнем без города |
Поддомены msk.site.ru | Классика локального SEO | wildcard-DNS/SSL, куки между поддоменами |
Blueprint описывает вариант «путь»; для поддоменов меняется только резолвер (Route::domain) — остальное идентично.
Состав решения
- Модель
City+ миграция + сидер (города — из сидера, не руками) - Trait
BelongsToCity(FK + scopeforCity) - Middleware
ResolveCity: город из первого сегмента URL → container/context - Trait
HasDeclensionsлибо json-поле склонений города - Filament:
CityResource, у городозависимых ресурсов — фильтр/колонка города - Blade: компонент выбора города (модалка со списком)
- Тесты городов и подмены контента
Схема данных
cities
| Столбец | Тип | Индекс | Cast / примечание |
|---|---|---|---|
| id | id | PK | |
| name | string | «Краснодар» | |
| slug | string | unique | krasnodar, сегмент URL |
| declensions | json | cast array: gen/dat/prep — «в Краснодаре», «из Краснодара» | |
| phone | string nullable | городской телефон (fallback на общий из settings) | |
| address | string nullable | ||
| is_default | boolean default false | город по умолчанию | |
| is_active | boolean default true | index | |
| sort_order | smallint default 0 |
Городозависимые сущности: city_id FK (BelongsToCity) — когда контент свой на город; если различаются только подстановки в текстах (город, телефон) — FK не нужен, достаточно переменных в шаблонах (specs/seo).
Правила реализации
ResolveCitymiddleware: сегмент →City(кеш списка городов тегомcities), неизвестный слаг → 404, без сегмента → default-город (301 на канонический URL с городом — решить по SEO-стратегии и зафиксировать).- Текущий город — через container binding (
app(CurrentCity::class)) или контекст; не черезsessionдля SEO-страниц: поисковик должен получать контент по URL, не по куке. Кука — только для запоминания выбора и предложения переключиться. - Роуты: группа с префиксом
{city:slug}+->name()как обычно; хелпер/расширениеroute()не городить — передавать город параметром, Blade-компонент ссылок. - Склонения:
{city},{city_gen},{city_prep}в SEO-шаблонах и текстах — изdeclensions; заполняются в сидере/админке (Filament: collapsed-секция из трёх TextInput). - hreflang не нужен (язык один), а вот canonical на каждый город — свой URL; сквозные страницы без городской специфики (политика конфиденциальности) — вне городской группы.
- Sitemap: генерировать на все активные города × городозависимые страницы.
- Кеш публичных страниц — ключ/тег обязательно включает город.
Пакеты
Без специальных: middleware + trait + json-склонения. Morpher/petrovich — только если города добавляются динамически и склонять руками некому.
Шаги
- Миграция + модель City + сидер городов со склонениями.
- Middleware ResolveCity + binding текущего города + 404/redirect-логика.
- Городская группа роутов; вынести вне её общие страницы.
- Подстановки в SEO-шаблоны и Blade (телефон/адрес города с fallback).
BelongsToCityдля городозависимых моделей (если есть).- Компонент выбора города + кука предпочтения.
- Filament: CityResource; фильтр города в зависимых ресурсах.
- Тесты: каждый активный город → 200 и правильные подстановки; неизвестный город → 404; redirect-логика корня; sitemap множится на города; кеш не мешает городам.
Чеклист приёмки
- [ ] Один и тот же URL всегда отдаёт один и тот же город (никакой session-магии)
- [ ] Склонения корректны во всех SEO-шаблонах (проверить каждую форму)
- [ ] Телефон/адрес города с fallback на общие настройки
- [ ] Кеш страниц городозависимый (нет «Сочи показал телефон Краснодара»)
- [ ] Sitemap и canonical по городам корректны
- [ ] Добавление нового города = запись в админке + сидер склонений, без правки кода