Overview
A Product Display Page (PDP) is the page where customers view a single product, select options like size or color, see pricing and images, and add items to their cart. This guide walks through building a PDP 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 Product from a URL
Lunar’sUrl model provides slug-based lookups for products, removing the need to expose database IDs in URLs.
Define the Route
Resolve the Product
whereVisible() scope restricts the query to products in the published state, and the channel()/customerGroup() scopes ensure that a product not scheduled for the current channel or customer group returns a 404 rather than rendering. See the Storefront Session reference for how to manage these values.
Displaying Product Information
Product Name and Description
A product’sname, description, and short_description are real, translatable database columns. Use the translate() method to retrieve them for the current locale.
Custom Attributes
Any custom attributes defined on the product type are accessible with theattr() method. For example, if the product type includes a “Material” attribute:
Product Images
Lunar uses Spatie MediaLibrary for image management. Products store images in theimages media collection, with zoom, large, and medium conversions registered by default.
config/lunar/media.php. See the Media reference for details on customizing conversions.
Variant Selection
Most products have at least one variant. When a product offers multiple variants (different sizes, colors, etc.), the storefront needs to let customers select the variant they want.Understanding the Data Structure
Each variant is associated with one or moreProductOptionValue records. For example, a T-shirt might have:
- Product Options: Color, Size
- Product Option Values: Blue, Red (for Color); S, M, L (for Size)
- Variants: Blue/S, Blue/M, Blue/L, Red/S, Red/M, Red/L
Building an Option Selector
Option and value names are plain translatable fields (not attribute data), so they are read withtranslate().
Mapping Option Values to Variants
To determine which variant corresponds to a given set of option selections, build a lookup map and pass it to the frontend.Single-Variant Products
When a product has only one variant, option selectors are unnecessary. Handle this in the template:Pricing
Pricing in Lunar is resolved through thePricing facade, which accounts for the current currency, customer group, and quantity.
Displaying the Price
ThePricing facade accepts a resolved StorefrontContext, which carries the currency and customer groups for the current browsing session, so the returned price matches the browsing context. StorefrontSession::context() builds that context from the active session.
get() method returns a Lunar\Core\DataObjects\PricingResponse with the following properties:
Each of
matched/base/an entry in priceBreaks is a Price model. Its price and list_price columns are plain integers in the currency’s lowest denomination; format them with the model’s own format() method, or resolve tax-aware values with priceIncTax(), priceExTax(), and listPriceIncTax().
Formatting Prices in Blade
list_price is the list price (RRP) shown for comparison — show it struck through when it is set.
Tax-Inclusive and Tax-Exclusive Prices
If prices need to be shown both inclusive and exclusive of tax, use the helper methods on thePrice model, which each return a PriceValue:
Price Breaks
If the product offers quantity-based pricing, display the available tiers:Stock and Availability
Each variant tracks its own inventory through a set of stock rollup columns (stock_on_hand, stock_available, stock_reserved, and so on), and an enabled flag gates whether it can be sold at all.
selling_policy field on a variant is a Lunar\Core\Enums\SellingPolicy case that controls how it may be sold relative to its stock:
Use
canBeFulfilledAtQuantity() to verify a specific quantity can be fulfilled before adding to cart, and getTotalInventory() to read the effective sellable quantity under the current policy:
Adding to Cart
Lunar provides aCartSession facade for managing the active cart. The Cart model exposes an add() verb that the storefront calls directly.
The Add-to-Cart Form
The Controller
The Route
Related Products
Lunar supports product associations for cross-sells, upsells, and alternate products. Display these on the PDP to encourage additional purchases.Variant-Specific Images
When variants have their own images (for example, showing a different image per color), swap the displayed image based on the selected variant.Putting It All Together
Here is a complete controller that prepares all the data a PDP needs:Next Steps
- Review the Products reference for the full list of model fields, relationships, and scopes.
- Review the Pricing reference for custom price formatters and additional formatting options.
- Review the Media reference for image conversion configuration.
- Review the Storefront Session reference to manage channel, currency, and customer group context.