Skip to main content

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’s Url model provides slug-based lookups for products, removing the need to expose database IDs in URLs.

Define the Route

Resolve the Product

Eager loading relationships in a single query avoids N+1 performance issues. The with() call above loads everything needed to render the full PDP.
The channel() and customerGroup() scopes ensure that a product not published to 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

Product names and descriptions are stored as attribute data. Use the attr() method to retrieve translated values.
In a Blade template:

Custom Attributes

Any custom attributes defined on the product type are accessible in the same way. For example, if the product type includes a “Material” attribute:

Product Images

Lunar uses Spatie MediaLibrary for image management. Products store images in the images media collection.
The available conversion sizes (large, medium, zoom, etc.) depend on the media definitions configured in 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 more ProductOptionValue 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

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.
Pass this as JSON to JavaScript for client-side variant resolution:

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 the Pricing facade, which accounts for the current currency, customer group, and quantity.

Displaying the Price

The Pricing facade accepts an optional currency and customer group. Pass these from StorefrontSession so the returned price matches the browsing context.
The get() method returns a PricingResponse with the following properties:

Formatting Prices in Blade

Tax-Inclusive and Tax-Exclusive Prices

If prices are stored inclusive of tax, use the helper methods on the Price model to show both:
See the Pricing reference for more on configuring whether prices are stored inclusive or exclusive of tax.

Price Breaks

If the product offers quantity-based pricing, display the available tiers:

Stock and Availability

Each variant tracks its own inventory. Display stock status to help customers make purchasing decisions.
The purchasable field on a variant controls its availability: Use canBeFulfilledAtQuantity() to verify a specific quantity can be fulfilled before adding to cart:

Adding to Cart

Lunar provides a CartSession facade for managing the active cart. Use it to add variants to the cart from the PDP.

The Add-to-Cart Form

The Controller

The Route

Lunar supports product associations for cross-sells, upsells, and alternate products. Display these on the PDP to encourage additional purchases.
See the Products reference for more on association types.

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.
Pass this map to JavaScript alongside the variant option map:

Putting It All Together

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

Next Steps