Product detail page
Render a product detail page and product selection URLs in one SDK flow.
Product detail page
Render a full product detail page from commerce.product.detail, including product selection URLs. The helper returns a server-shaped payload with variants, options, option value slugs/media, brand, categories, tags, images, videos, and the listing rollup. A null return covers all "not available" reasons; render the same fallback UI for missing, unpublished, wrong tenant, and feature-disabled cases.
Next.js Server Component (SSG / ISR)
// app/products/[slug]/page.tsx
import { createClient, resolveProductSelection } from '@01.software/sdk'
import { notFound } from 'next/navigation'
export const revalidate = 60
export default async function ProductPage({
params,
}: {
params: { slug: string }
}) {
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
})
const detail = await client.commerce.product.detail({ slug: params.slug })
if (!detail) return notFound()
const selection = resolveProductSelection(detail, {})
return <ProductView detail={detail} selection={selection} />
}React Client Component
'use client'
import {
buildProductHref,
createProductSelectionCodec,
getProductSelectionImages,
resolveProductSelection,
} from '@01.software/sdk'
import { createQueryHooks } from '@01.software/sdk/query'
import { useClient } from '@/lib/sdk'
export function ProductDetail({
slug,
search,
}: {
slug: string
search: string
}) {
const client = useClient()
const query = createQueryHooks(client)
const { data: detail, isLoading } = query.useProductDetailBySlug(slug)
if (isLoading) return <Skeleton />
if (!detail) return <NotAvailable />
const codec = createProductSelectionCodec(detail)
const normalizedSelection = codec.parse(search)
const selection = resolveProductSelection(detail, normalizedSelection)
const selectionQuery = codec.stringify(normalizedSelection)
const images = getProductSelectionImages(selection)
return (
<ProductView
detail={detail}
selection={selection}
images={images}
selectionQuery={selectionQuery}
/>
)
}Selection URL contract
Use createProductSelectionCodec(detail) for round-tripping selection state. Complete selections use variant=<variantId>; partial selections use opt.<optionId>=<valueId>. Older opt.<optionSlug>=<valueSlug> URLs still parse during Stage 1, but option and value slugs are compatibility metadata rather than canonical identity.
Use IDs from detail.options[].id and detail.options[].values[].id when building new selection links. Slugs are display and compatibility metadata, not the source of truth for new outbound URLs.
const codec = createProductSelectionCodec(detail)
const normalized = codec.parse('?opt.option-color=color-black')
const query = codec.stringify(normalized)Slug-only URLs such as ?black or ?color=black are rejected because option titles are not stable identifiers.
For listing cards, use buildProductHref(product, group, options) to compose
detail links with the same ID-based query contract.
On the detail page, parse that query with createProductSelectionCodec(detail)
or pass it to resolveProductSelection(detail, { search }). Selection media is
resolved in the same order as the selection itself: selected variant, selected
option value, partial matching variant, then listing/product fallback.
const href = buildProductHref(product, group, {
basePath: '/products',
detail,
})Option values in selection.availableValuesByOptionSlug include stock fields
for UI state: available is combinability, while availableForSale,
isUnlimited, and availableStock describe saleability.
Why null and not an error?
The endpoint returns 404 with a code field for not_found, not_published, tenant_mismatch, or feature_disabled. The SDK collapses all 404s to null so consumer UIs render one "not available" path. To recover the exact reason for backend triage, log client.lastRequestId against backend logs — the endpoint records the code.
Cache invalidation
useProductDetail* cache invalidates automatically when any of 10 detail-relevant collections is mutated through SDK mutation hooks: products, product-variants, product-options, product-option-values, product-categories, product-tags, product-collections, brands, brand-logos, images.
When to skip the helper
Use client.collections.from('products').find(...) (advanced escape hatch) when:
- You need bulk reads or custom filter combinations the helper does not expose.
- You need fields not in the helper response shape.
- You are reading on a write path under a transaction (use the server query builder).