Тема
Blueprint: Каталог с категориями (Catalog)
Когда применять
«Каталог услуг», «наши работы», «товары без корзины» — категории + карточки + фильтры. Витрина без транзакций.
Не применять для: магазина с корзиной/оплатой (Lunar/Bagisto — см. opensource/ecommerce), каталогов на 100k+ позиций с геопоиском (отдельная архитектура: Scout + PostGIS, как в проекте catalog).
Состав решения
- Модели
Category,Item(именовать по домену: Service, Work, Product) + миграции + фабрики - Контроллеры
CategoryController@index/show,ItemController@show - Filament-ресурсы для обеих моделей
- Blade: список категорий, категория с карточками и фильтрами, карточка
- Breadcrumbs-компонент с Schema.org
- Тесты
Схема данных
categories
| Столбец | Тип | Индекс | Примечание |
|---|---|---|---|
| id, name, slug unique, sort_order, is_active | стандартный набор | ||
| parent_id | FK nullable self | index, nullOnDelete | только если ТЗ требует вложенность |
| description | text nullable | SEO-текст категории | |
| meta_title / meta_description | string nullable |
Вложенность: 2 уровня — обычный parent_id c рекурсией в Blade; 3+ уровня и деревья в запросах — staudenmeir/laravel-adjacency-list.
items
| Столбец | Тип | Индекс | Cast / примечание |
|---|---|---|---|
| id | id | PK | |
| category_id | FK constrained | index | restrictOnDelete — не терять карточки при удалении категории |
| name | string | ||
| slug | string | unique (или composite с category_id) | |
| price | decimal(10,2) nullable / integer копейки | «цена от»; nullable = «по запросу» | |
| short_description | text nullable | для карточек в списке | |
| content | longText nullable | детальная | |
| attributes | json nullable | cast array; характеристики без фильтрации по ним | |
| is_active | boolean default true | index | |
| is_featured | boolean default false | для главной | |
| sort_order | unsignedSmallInteger default 0 | index | |
| timestamps |
Изображения — medialibrary (gallery + конверсии). Фильтруемые характеристики — отдельные столбцы или таблица атрибутов, не JSON (specs/migrations).
Правила реализации
- Роуты:
/catalog,/catalog/{category:slug},/catalog/{category:slug}/{item:slug}соscopeBindings()— Laravel сам проверит принадлежность item категории (иначе дубли URL одной карточки под разными категориями — SEO-беда). - Фильтры в категории: whitelist параметров (сортировка, цена от/до, 1–3 ключевых признака) через query string; canonical — на чистый URL категории без параметров.
- Листинг:
->with('media'),withCountгде надо, пагинация из config; сортировка default —sort_order. - «Цена от» — вычислять при сохранении, не агрегировать на каждый рендер.
- SEO: шаблоны мета для категорий/карточек с переменными (
{category},{price}) по specs/seo; breadcrumbs со Schema.org BreadcrumbList; всё активное — в sitemap. - Кеш: теги
catalog(+category:{id}при больших объёмах), flush вafterSave. - Поиск по каталогу: до ~10k позиций —
where('name', 'ilike')c functional index; больше или «умный» поиск — Scout + Meilisearch (см. guide/recipes).
Пакеты
spatie/laravel-medialibrary, spatie/eloquent-sortable (drag&drop порядок), staudenmeir/laravel-adjacency-list (только при глубоком дереве).
Шаги
- Миграции + модели + scopes (
active,featured,ordered) + фабрики. - Filament: категории (reorder), карточки (галерея, цена, category select).
- Роуты со scopeBindings + контроллеры.
- Blade: категория с фильтрами, карточка с галереей, breadcrumbs.
- SEO-шаблоны мета + sitemap.
- Тесты: категория 200, карточка 200, карточка под чужой категорией 404, неактивное скрыто, фильтр не ломает пагинацию.
Чеклист приёмки
- [ ] scopeBindings: карточка открывается только под своей категорией
- [ ] Листинги без N+1; изображения — конверсии
- [ ] Фильтры — whitelist, canonical без query-параметров
- [ ] Неактивные категории/карточки скрыты везде включая sitemap
- [ ] Breadcrumbs с разметкой на всех уровнях
- [ ] Удаление категории с карточками запрещено (restrict), а не каскад