Тема
Модули услуг (разбор artel-23ru)
Статус: проектирование по донору. Разбор рабочего кода
artel-23ru(сайт электромонтажной артели) — самая зрелая SEO-модель услуг из проектов студии. Здесь услуги проработаны как SEO-комбинаторная машина: 3-уровневая иерархия × 4 SEO-оси = тысячи ЧПУ-страниц, полные русские падежи, двойная микроразметка. Для CMS это перенос и обобщение, а не разработка с нуля.⚠️ Обобщение уже частично сделано (сверено по коду, июль 2026): весь этот стек перенесён в
universalв обобщённом виде — те же модели/сервисы/трейты, ноsetting()из БД вместоconfig('artel.*')иSeoBlockс entity-scope. Версии в universal свежее. При портировании в CMS брать universal-версию за основу, этот разбор — как описание предметной модели.
Что уникального в artel-23ru
Проверено по коду (app/Models/, routes/web.php, Services/SeoService.php):
- 3-уровневая иерархия
ServiceDirection → ServiceCategory → Service(чистый FK parent→child,cascadeOnDelete,unique(category_id, slug)); - 4 SEO-оси кросс-продукта: услуга × район × бренд × тип объекта — одни и те же категории/услуги под разными URL-префиксами дают тысячи посадочных;
- движок падежей
HasDeclensions— 6 русских падежей на каждую сущность (город/категория/бренд/локация), хранятся в JSONdeclensions,{city_gen},{category_prep}, «в Краснодаре»; - двойная микроразметка — JSON-LD (
Service+provider: LocalBusiness+Offer) + дублирующая inlineitemprop(редкость),ItemList/ListItemдля прайс-листов; - промышленный слой 301 — ~80 хардкод-редиректов старых slug после переименований (признак реального прода);
- E-E-A-T сигнал —
wiki_urlна направление/категорию/услугу (ссылка на источник).
Это глубже, чем услуги в gulaev-dev/er. В universal с июня 2026 живёт обобщённый порт этого же стека (см. врезку выше) — сравнение «глубже, чем в universal» устарело.
Структура сущностей
| Модель | Таблица | Ключевые связи |
|---|---|---|
ServiceDirection | service_directions | hasMany(ServiceCategory) |
ServiceCategory | service_categories | belongsTo(Direction), hasMany(Service), belongsToMany(Brand) |
Service | services | belongsTo(Category); цена inline (price/price_type/unit) |
BrandService | brand_services | оверрайд услуги под бренд (unique(brand_id, service_id)) |
CaseStudy | ↔ case_service | кейсы/портфолио, pivot к услугам |
4-го уровня нет. «Работы» вынесены в параллельную подсистему сметы (estimate_*), не связанную с услугами FK. Оси «районы» и «типы объектов» — чистая URL-комбинаторика (без pivot): все категории показываются на каждой локации, уникальность даёт SEO-текст.
SEO-вложенность URL
/uslugi/ все направления+категории
/uslugi/{direction}/ посадочная направления (whitelist из 7 slug)
/uslugi/{category}/ прайс-лист категории
/uslugi/{category}/{service}/ карточка услуги
/elektrik/{location}/{category}/ район × категория
/brendy/{brand}/{category}/ бренд × категория
/brendy/{brand}/{category}/{service}/ бренд × услуга (3 уровня)
/dlya-biznesa/{objectType}/{category}/ тип объекта × категорияКросс-оси — withoutScopedBindings() (осознанный декартов продукт, category не дочерняя к location/brand). Мультигород заложен (поддомены, ResolveCityMiddleware), но в URL услуг пока через middleware, не в пути.
Рекомендуемая разбивка на 5 модулей
Три первых — обязательны и связаны иерархией; два последних — опциональны и независимы.
Модуль 1. «Услуги (иерархия)» ★★★
Границы: Direction → Category → Service, цены (price/price_type/unit, «по договору»), sort_order + drag&drop, RelationManager'ы для вложенного редактирования, unique(category_id, slug), wiki_url. Зависит от: ядра; suggests City/мультигород (city-падежи, middleware, гео-оси) — на сайте одного города модуль работает без него. Не включать: SEO-тексты, бренды, калькулятор.
Модуль 2. «SEO услуг с вложенностью» ★★★ — главный переносимый актив
Границы: SeoBlock (page_type/position/format/template/{переменные}/per-city), SeoMeta (ручной оверрайд), SeoService::render(), движок падежей HasDeclensions, сборка ЧПУ и 301, генерация breadcrumbs + Schema.org (Service/Offer/ItemList/ BreadcrumbList) + og_image-каскад, Sitemap кросс-продуктов. Зависит от: модуля 1 (сущности как контекст) и City. Ключевое: проектировать generic — контекст-массив сущностей + реестр переменных, чтобы движок работал не только с услугами, а с любой сущностью. Это то, что делает услуги artel глубже прочих проектов. Смыкается с SEO-гибкостью и умным фильтром.
Модуль 3. «Кросс-оси услуг» (районы/бренды/типы объектов) ★★
Границы: Location (self-ref: район→микрорайон→улица→ЖК), Brand + pivot brand_category + оверрайд brand_services, ObjectType; контроллеры кросс-продукта {ось}/{category} и {brand}/{category}/{service}. Зависит от: модулей 1 и 2. Решить на старте: реальные pivot (Location↔Category) или URL-комбинаторика как в artel. Pivot = чище фильтрация, дороже контент-менеджмент.
Модуль 4. «Портфолио/кейсы услуг» ★★ (слабо связан)
Границы: CaseStudy + pivot case_service, метрики (JSON), типы (b2b/private), город, медиа. Зависит от: модуля 1 опционально (кейс может жить без услуг).
Модуль 5. «Калькулятор/Смета услуг» ★★ (независимый)
Границы: простой calculator_sections/items + wizard-смета estimate_projects/rooms/points/lines + estimate_works/materials (3 tier цен) + estimate_formulas (конфигурируемые коэффициенты). Зависит от: только City. Связь с услугами — опциональная строковая (calc_section). Портировать самостоятельным модулем, не встраивать в ядро услуг.
Граф зависимостей модулей услуг
City ──► Услуги(1) ──► SEO-услуг(2) ──► Кросс-оси(3)
│
Кейсы(4) ────────┘ (опц.)
Калькулятор(5) ── (независим, только City)Это конкретный пример иерархии зависимостей модулей: жёсткая цепочка 1→2→3 + опциональные 4, 5.
Уроки для контракта (чего избегать)
Из «нечистот» artel (не критично, но учесть при портировании):
$guarded=['id']вместо$fillable— в CMS вернуть whitelist;- нет
afterSave/сброса кеша в Filament-ресурсах — в CMS обязательна инвалидация тегов; - хардкод whitelist slug направлений в роуте, список «электрических» категорий в контроллере — в CMS вынести в конфиг/БД (см. anti-hardcode).
Итог
Услуги artel-23ru — эталон SEO-глубины для CMS. Переносим как 5 модулей с чёткими границами, а движок SEO услуг (модуль 2) обобщаем до generic-движка, применимого к любой иерархической сущности. Добавлено в реестр модулей.