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 ProductDetailResult: successful responses include a server-shaped payload with variants, options, option value slugs/media, brand, categories, tags, images, videos, and the listing rollup; product-detail 404 responses include found: false with a reason for missing, unpublished, or feature-disabled cases.

Successful product payloads expose inventory without sentinel values: product.totalInventory is the tracked stock sum across non-unlimited variants, null when no variants are tracked, and product.hasUnlimitedVariant signals whether any variant is unlimited.

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 result = await client.commerce.product.detail({ slug: params.slug })
  if (!result.found) return notFound()
  const detail = result.product
  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: result, isLoading } = query.useProductDetailBySlug(slug)
  if (isLoading) return <Skeleton />
  if (!result?.found) return <NotAvailable reason={result?.reason} />
  const detail = result.product
  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 { found: false, reason } and not an error?

The endpoint returns 404 with a code or reason field for not_found, not_published, or feature_disabled. The SDK maps those product-detail 404s to { found: false, reason } so consumer UIs can choose a standard 404, preview CTA, or feature-gating UI. Permission/auth errors, including 403 tenant mismatches, still throw typed SDK errors. To correlate backend triage, log client.lastRequestId against backend logs — the endpoint records the reason.

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.

For SSG/ISR and webhook-driven cache invalidation, tag product detail reads with storefrontCacheResources.productDetail(scope, { slug }) before a slug-based fetch, or { id } when the adapter already has the product ID. Use productListing(scope) on detail pages only as a coarse fallback when your invalidation pipeline cannot carry previous slugs yet; see Storefront Cache Resources.

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