Skip to main content

Overview

A Product Listing Page (PLP) displays a grid or list of products within a collection (category). Customers browse products, filter by attributes, sort results, and click through to individual product pages. This guide walks through building a PLP using Lunar’s models and facades. The examples below use standard Laravel controllers and Blade templates. The same concepts apply whether the storefront is built with Livewire, Inertia, or a headless API.

Resolving a Collection from a URL

Like products, collections use Lunar’s Url model for slug-based lookups.

Define the Route

Resolve the Collection

Displaying Collection Information

A collection’s name, description, and short_description are real, translatable database columns. Read them with the translate() method.
Any custom attributes defined for collections (via the attribute system) are still accessed with attr(), for example $collection->attr('featured_note').

Collection Image

Collections support media through Spatie MediaLibrary, just like products. The default media collection is images.
Collections form a nested hierarchy. The breadcrumb attribute returns the translated names of all ancestor collections, which is useful for building breadcrumb navigation.
The breadcrumb attribute uses the ancestors relationship from the nested set. If displaying breadcrumbs on every page load, consider eager loading ancestors to avoid extra queries: $collection->load('ancestors').

Querying Products

The products() relationship on a Collection returns products ordered by the collection’s configured position pivot column.

Basic Query

whereVisible() restricts the query to products in the published state.

Filtering by Channel

Products can be scheduled against channels. Use the channel scope to return only products that are active on the current channel.

Filtering by Customer Group

Similarly, use the customerGroup scope to return only products visible to the current customer group.

Sorting

Products in a collection have a default sort order determined by the position pivot column, which the products() relationship already orders by. Merchants control this order (drag-and-drop, or bulk actions) in the admin panel.

Custom Sort on the Query

To let customers choose a sort order at browse time, override the default pivot ordering on the query.
For simpler sorting needs, the default position ordering is often sufficient — reorder products for a collection from the admin panel rather than building custom query logic.

Filtering

Product filtering depends on the storefront’s requirements. Below are some common filtering approaches.

Filtering by Brand

To build a brand filter list from the products in the collection:

Filtering by Price Range

Prices are stored as integers in the lowest denomination (e.g., cents). Multiply the customer-facing price by 100 (or the currency’s factor) before comparing.

Filtering by Product Availability

A variant’s availability is governed by its enabled flag and its selling_policy (an Lunar\Core\Enums\SellingPolicy case: Always, InStock, or InStockOrOnBackorder), checked against the stock_available rollup.

Displaying Products

Product Grid

Fetch a display price for each product through the Pricing facade. Passing the resolved StorefrontContext keeps the currency and customer groups consistent with the rest of the storefront.
Price::format() formats a stored money column (price or list_price) for the price row’s own currency. list_price is the list price (RRP) shown for comparison — display it struck through when it is set and higher than price.

Pagination

Laravel’s built-in pagination works directly with the collection product query.
Using withQueryString() preserves any active filter and sort parameters in the pagination links.

Child Collections

Collections can contain child collections (subcategories). Display them to help customers navigate deeper into the catalog.

Putting It All Together

Here is a complete controller that prepares all the data a PLP needs:

Next Steps