01.software Docs

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).

On this page