@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) --}}
Главная
/
Каталог
@foreach ($ancestors as $ancestor)
/
{{ $ancestor->name }}
@endforeach
/
{{ $category->name }}
{{ 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
{{-- SEO-текст — только на первой странице пагинации (SEO §3, §9): на page=2+
он превращался в дубль без нового смысла. --}}
@if ($category->seo_text && $page === 1)
{!! nl2br(e($category->seo_text)) !!}
@endif
@endsection