Documentation Index Fetch the complete documentation index at: https://mintlify.com/openshiporg/openfront/llms.txt
Use this file to discover all available pages before exploring further.
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 >
)
Key product details displayed to customers:
Product Fields
GraphQL Query
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 ])
Image Gallery
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 }
}
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