@extends('layouts.app') @php $page = $products->currentPage(); // Сортировка и фильтры плодят бесконечные комбинации одного и того же // ассортимента — такие URL не индексируем (SEO §4). $isFiltered = $sort !== 'default' || ! empty($selected) || ! empty($price); $categoryUrl = route('catalog.category', $category->path); $priceFrom = $stats['price_from'] !== null ? (float) $stats['price_from'] : null; // Пустая ветка (ни своих товаров, ни у потомков) индексации не заслуживает. // Страницу оставляем доступной: фиды обновляются, товары появятся — и раздел // вернётся в индекс сам, без нашего участия (план доработок, п. 1). $isEmpty = (int) $stats['count'] === 0; @endphp @section('title', $category->metaTitle().($page > 1 ? ' — страница '.$page : '').' — teeu') @section('meta_description', $category->metaDescription($stats['count'], $priceFrom)) @php $seoCrumbs = [ ['name' => 'Главная', 'url' => route('home')], ['name' => 'Каталог', 'url' => route('catalog.index')], ]; foreach ($ancestors as $ancestor) { $seoCrumbs[] = ['name' => $ancestor->name, 'url' => route('catalog.category', $ancestor->path)]; } $seoCrumbs[] = ['name' => $category->name, 'url' => route('catalog.category', $category->path)]; @endphp {{-- Canonical страницы пагинации ведёт на неё саму: товары там другие, склеивать с первой нельзя (SEO §3). Сортировка и фильтры, наоборот, канонизируются на чистую категорию и закрыты noindex (SEO §4). Пустая ветка — тоже noindex, но follow: ссылки из неё вниз по дереву остаются рабочими. --}} @section('seo') @include('partials.seo.meta', [ 'canonical' => \App\Support\Seo::listingCanonical($categoryUrl, $page, $isFiltered), 'robots' => $isFiltered || $isEmpty ? 'noindex, follow' : null, 'ogTitle' => $category->metaTitle(), 'ogDescription' => $category->metaDescription($stats['count'], $priceFrom), ]) @include('partials.seo.json-ld', ['data' => \App\Support\Seo::breadcrumbJsonLd($seoCrumbs)]) @if ($products->isNotEmpty()) @include('partials.seo.json-ld', [ 'data' => \App\Support\Seo::itemListJsonLd($products, $products->firstItem() ?? 1), ]) @endif @endsection @section('content') @section('content') {{-- Breadcrumbs (ТЗ §19.1) --}}

{{ $category->h1() }}

{{ number_format($products->total(), 0, '.', ' ') }} {{ \App\Support\Ru::plural($products->total(), ['товар', 'товара', 'товаров']) }}
{{-- Left column = category nav (Ozon-style) + filters; both collapse on mobile. Сортировка — чипами-ссылками, фильтры в адресе сохраняются. Выбранные фильтры — чипами над сеткой, каждый снимается отдельно. Всё в одном блоке: однострочные php-директивы перед блочной Blade склеивает. --}} @php $hasFacets = ! empty($facets) || (int) $stats['count'] > 0; $navItems = $children->isNotEmpty() ? $children : $siblings; $hasCatNav = $ancestors->isNotEmpty() || $navItems->isNotEmpty(); $hasSidebar = $hasFacets || $hasCatNav; $sorts = ['default' => 'По актуальности', 'newest' => 'Новинки', 'price_asc' => 'Дешевле', 'price_desc' => 'Дороже']; $query = request()->except('page'); $chips = []; if ($price !== []) { $rub = fn ($v) => number_format($v, 0, '.', ' '); $without = $query; unset($without['price']); $chips[] = [ 'label' => 'Цена:'.(isset($price['min']) ? ' от '.$rub($price['min']) : '').(isset($price['max']) ? ' до '.$rub($price['max']) : '').' ₽', 'url' => url()->current().(($qs = \Illuminate\Support\Arr::query($without)) ? '?'.$qs : ''), ]; } foreach ($facets as $facet) { $raw = $query['f'][$facet['id']] ?? null; if ($raw === null) { continue; } if ($facet['type'] === 'number') { if ($facet['selected_min'] === null && $facet['selected_max'] === null) { continue; } $num = fn ($v) => rtrim(rtrim(number_format((float) $v, 2, '.', ' '), '0'), '.'); $label = $facet['name'].':' .($facet['selected_min'] !== null ? ' от '.$num($facet['selected_min']) : '') .($facet['selected_max'] !== null ? ' до '.$num($facet['selected_max']) : ''); $without = $query; unset($without['f'][$facet['id']]); $chips[] = ['label' => $label, 'url' => url()->current().(($qs = \Illuminate\Support\Arr::query($without)) ? '?'.$qs : '')]; continue; } foreach ($facet['values'] as $value) { if (! $value['selected']) { continue; } $without = $query; $without['f'][$facet['id']] = array_values(array_filter( (array) $raw, fn ($k) => \App\Models\CategoryAlias::normalize((string) $k) !== $value['key'], )); if ($without['f'][$facet['id']] === []) { unset($without['f'][$facet['id']]); } $chips[] = ['label' => $value['label'], 'url' => url()->current().(($qs = \Illuminate\Support\Arr::query($without)) ? '?'.$qs : '')]; } } @endphp
@if ($hasSidebar) @endif
@foreach ($sorts as $val => $label) {{ $label }} @endforeach
@if (count($chips) > 0)
@foreach ($chips as $chip) {{ $chip['label'] }} @endforeach Сбросить все
@endif @if ($products->isEmpty())
@if (! empty($selected) || ! empty($price)) Ничего не найдено по выбранным фильтрам. Сбросить фильтры @else В этой категории пока нет товаров. @endif
@else
@foreach ($products as $product) @endforeach
{{ $products->links() }}
@endif
{{-- SEO-текст — только на первой странице пагинации (SEO §3, §9): на page=2+ он превращался в дубль без нового смысла. --}} @if ($category->seo_text && $page === 1)
{!! nl2br(e($category->seo_text)) !!}
@endif @endsection