Skip to main content
The product catalog allows customers to browse, search, and view detailed product information with support for variants, regional pricing, and rich media.

Product Pages

Product detail pages (/[countryCode]/products/[handle]) provide a complete product view:

Layout Structure

The product template uses a responsive 3-column layout:
// features/storefront/modules/products/templates/index.tsx
const ProductTemplate = ({ product, region, countryCode }) => (
  <div className="max-w-[1440px] mx-auto flex flex-col lg:flex-row">
    {/* Left: Product Info (sticky on desktop) */}
    <div className="lg:sticky lg:top-48 lg:max-w-[300px]">
      <ProductInfo product={product} />
      <ProductTabs product={product} />
    </div>
    
    {/* Center: Image Gallery */}
    <div className="w-full">
      <ImageGallery images={product.productImages} />
    </div>
    
    {/* Right: Actions (sticky on desktop) */}
    <div className="lg:sticky lg:top-48 lg:max-w-[300px]">
      <ProductActions product={product} region={region} />
    </div>
  </div>
)

Product Information

Key product details displayed to customers:
interface StoreProduct {
  id: string
  title: string
  handle: string              // URL-friendly slug
  description: Document       // Rich text content
  thumbnail: string          // Primary image
  status: 'draft' | 'published'
  productImages: ProductImage[]
  productVariants: ProductVariant[]
  productOptions: ProductOption[]
  productCollections: Collection[]
  metadata?: Record<string, any>
}

Product Variants

Products can have multiple variants based on options like size, color, or material.

Variant Selection

The variant selection component handles option selection:
// features/storefront/modules/products/components/product-actions/index.tsx
export default function ProductActions({ product, region }) {
  const [options, setOptions] = useState({})
  const variants = product.productVariants
  
  // Build variant lookup by option values
  const variantRecord = useMemo(() => {
    const map = {}
    for (const variant of variants) {
      const temp = {}
      for (const optionValue of variant.productOptionValues) {
        temp[optionValue.productOption.id] = optionValue.value
      }
      map[variant.id] = temp
    }
    return map
  }, [variants])
  
  // Find matching variant based on selected options
  const variant = useMemo(() => {
    let variantId
    for (const key of Object.keys(variantRecord)) {
      if (isEqual(variantRecord[key], options)) {
        variantId = key
      }
    }
    return variants.find(v => v.id === variantId)
  }, [options, variantRecord, variants])
  
  return (
    <div>
      {/* Option selectors */}
      {product.productOptions.map(option => (
        <OptionSelect
          key={option.id}
          option={option}
          current={options[option.id]}
          updateOption={setOptions}
        />
      ))}
      
      {/* Price and add to cart */}
      <ProductPrice product={product} variant={variant} region={region} />
      <Button onClick={() => addToCart({ variantId: variant.id })}>
        Add to cart
      </Button>
    </div>
  )
}

Inventory Management

Each variant tracks inventory and backorder settings:
const inStock = useMemo(() => {
  if (!variant) return false
  if (variant.inventoryQuantity <= 0) return false
  if (variant.allowBackorder === false) return true
  return true
}, [variant])
Products support multiple images with an interactive gallery:
// features/storefront/modules/products/components/image-gallery/
const ImageGallery = ({ images, handle, region }) => {
  return (
    <div className="flex flex-col">
      {images.map((image, index) => (
        <div key={image.id} className="relative aspect-[29/34]">
          <Image
            src={image.image?.url || image.imagePath}
            alt={`${handle} image ${index + 1}`}
            fill
            sizes="(max-width: 768px) 100vw, 50vw"
            className="object-cover"
            priority={index === 0}
          />
        </div>
      ))}
    </div>
  )
}

Regional Pricing

Prices are calculated based on the customer’s region:
// features/storefront/lib/util/get-product-price.ts
export function getProductPrice({ product, variantId, region }) {
  const variant = product.productVariants.find(v => v.id === variantId)
  
  // Find price for current region
  const price = variant?.prices?.find(p => 
    p.region?.id === region.id
  )
  
  return {
    calculatedPrice: price?.calculatedPrice?.calculatedAmount,
    originalPrice: price?.calculatedPrice?.originalAmount,
    currencyCode: price?.currency?.code || region.currency.code
  }
}

Price Display

const ProductPrice = ({ product, variant, region }) => {
  const price = getProductPrice({ product, variantId: variant?.id, region })
  
  return (
    <div className="flex items-baseline gap-2">
      {price.originalPrice !== price.calculatedPrice && (
        <span className="line-through text-muted-foreground">
          {formatAmount(price.originalPrice, region.currency.code)}
        </span>
      )}
      <span className="text-2xl font-semibold">
        {formatAmount(price.calculatedPrice, region.currency.code)}
      </span>
    </div>
  )
}

Product Collections

Products can be organized into collections for easier browsing:

Collection Pages

Collection pages (/[countryCode]/collections/[handle]) display filtered products:
const CollectionPage = async ({ params }) => {
  const { handle, countryCode } = params
  const region = await getRegion(countryCode)
  
  // Get products in this collection
  const { products } = await getProductsList({
    pageParam: 0,
    queryParams: { collectionId: collection.id },
    countryCode,
    sortBy: { createdAt: 'desc' }
  })
  
  return (
    <div>
      <h1>{collection.title}</h1>
      <ProductGrid products={products} region={region} />
    </div>
  )
}

Product Browsing

Filtering & Sorting

Products can be filtered and sorted:
// features/storefront/lib/data/products.ts
export async function getProductsList({
  pageParam = 0,
  queryParams,
  countryCode,
  sortBy = { createdAt: 'desc' }
}) {
  const whereClause = {
    productCollections: queryParams?.collectionId ? {
      some: { id: { equals: queryParams.collectionId } }
    } : undefined,
    productCategories: queryParams?.categoryId ? {
      some: { id: { equals: queryParams.categoryId } }
    } : undefined,
    productVariants: {
      some: {
        prices: {
          some: {
            region: {
              countries: { some: { iso2: { equals: countryCode } } }
            }
          }
        }
      }
    }
  }
  
  const { products, productsCount } = await openfrontClient.request(
    GET_PRODUCTS_QUERY,
    { where: whereClause, orderBy: [sortBy] }
  )
  
  return { products, count: productsCount }
}

Pagination

Products are paginated for performance:
const limit = 12
const offset = pageParam * limit

const products = await getProductsList({
  pageParam,
  queryParams: { limit },
  countryCode
})

const hasNextPage = products.count > offset + limit
Product pages show related products from the same collection:
// features/storefront/modules/products/components/related-products/
const RelatedProducts = async ({ product, countryCode }) => {
  const collection = product.productCollections?.[0]
  
  if (!collection) return null
  
  const { products } = await getProductsList({
    queryParams: { 
      collectionId: collection.id,
      limit: 4 
    },
    countryCode
  })
  
  return (
    <div>
      <h3>You might also like</h3>
      <ProductGrid products={products} />
    </div>
  )
}

Product Tabs

Additional product information in expandable tabs:
const ProductTabs = ({ product }) => (
  <div>
    <Accordion type="single" collapsible>
      <AccordionItem value="description">
        <AccordionTrigger>Product Information</AccordionTrigger>
        <AccordionContent>
          {renderRichText(product.description)}
        </AccordionContent>
      </AccordionItem>
      
      <AccordionItem value="shipping">
        <AccordionTrigger>Shipping & Returns</AccordionTrigger>
        <AccordionContent>
          {/* Shipping information */}
        </AccordionContent>
      </AccordionItem>
    </Accordion>
  </div>
)

Next Steps

Shopping Cart

Learn how products are added to cart

Collections API

Manage product collections

Build docs developers (and LLMs) love