): string $url * @param ?int $collectionId scope to a collection, or null for every product * @return array */ public function build(ProductListing $listing, Closure $url, ?int $collectionId = null): array { $filters = $listing->filters($collectionId); // Listing reads from the Meilisearch index via ProductService, not // Eloquent. list() returns a ProductListingResult — the product page // plus the price-slider bounds from one call; the controller no longer // stitches list() + priceRange() together itself. Sort/filter/page all // come from $listing (the query string). $result = $this->products->list( filters: $filters, perPage: self::PER_PAGE, page: $listing->page, sort: $listing->sort, ); $products = $result->products ->through(ProductCard::fromIndexed(...)) ->appends($listing->query(['page' => null])); // Slider bounds — the price span of everything matching the *other* // filters, rounded to whole euros, plus whether the current price params // actually narrow that span. All computed in core now // (ProductService::priceSliderBounds()). $priceBounds = $result->priceBounds; return [ 'listing' => $listing, 'products' => $products, 'listingAction' => $url([]), 'priceFloor' => $priceBounds->floor, 'priceCeil' => $priceBounds->ceil, 'clearPriceUrl' => $priceBounds->filtered ? $url($listing->query(['price_min' => null, 'price_max' => null, 'page' => null])) : null, 'sortOptions' => ProductSortOptions::build( $listing->sort, fn (?ProductSort $sort) => $url($listing->query(['sort' => $sort?->value, 'page' => null])), ), ]; } }