01.software Docs

Product detail page

Render a product detail page and product selection URLs in one SDK flow.

Product detail page

Render a public-safe product detail page from commerce.product.detail, including product selection URLs. The browser helper returns a ProductDetailCatalogResult: successful responses include variants, options, option value slugs/media, brand, categories, tags, images, videos, and Shopify-shaped Product fields such as priceRange, availableForSale, and selectedOrFirstAvailableVariant; product-detail 404 responses include found: false with a reason for missing, unpublished, or feature-disabled cases. Direct detail allows products with status: 'published' or status: 'unlisted'; unlisted products stay out of listing helpers. Catalog detail omits raw stock quantities. Use createServerClient().commerce.product.detail() only for trusted server reads that need operational inventory fields.

Successful catalog payloads expose availability fields for rendering but omit raw stock, reservedStock, and product.totalInventory. Use stockSnapshot() or cart/checkout stock checks for live inventory decisions.

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.detailCatalog({ 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. not_published covers draft, archived, scheduled, and malformed lifecycle state. 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