Product detail page
Render product detail and product selection with async route params.
Product detail page
Prepare
Use the same key, market and content-locale context as Product listing. This complete page uses Next.js 16 Promise params and searchParams; it has no undefined ProductView dependency. Add the listing recipe’s read error boundary at app/products/error.tsx.
These examples use the published SDK 0.65.1, Next.js 16.3.8 App Router, React 19.2.4, and Node 24.15.0. Install the SDK in your own app with npm install @01.software/sdk@0.65.1. The examples use force-dynamic; SDK calls run on requests, not at build time. This assumes no cacheComponents or static export. Compile and controlled local transport checks do not certify hosted availability or remote workspace admission.
Render product detail and selection
// app/products/[slug]/page.tsx
import {
createClient,
createProductSelectionCodec,
resolveProductSelection,
ProductSelectionCodecError,
} from '@01.software/sdk'
import { notFound } from 'next/navigation'
export const dynamic = 'force-dynamic'
export default async function ProductPage({
params,
searchParams,
}: {
params: Promise<{ slug: string }>
searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
const { slug } = await params
const query = new URLSearchParams()
for (const [key, value] of Object.entries(await searchParams)) {
if (typeof value === 'string') query.set(key, value)
}
const client = createClient({
publishableKey: process.env.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY!,
apiUrl: process.env.SOFTWARE_API_URL,
})
const result = await client.commerce.product.detailCatalog({ slug })
if (!result.found) return notFound()
const detail = result.product
const codec = createProductSelectionCodec(detail, { emit: 'canonical-id' })
let normalized: ReturnType<typeof codec.parse>
try {
normalized = codec.parse(query.toString())
} catch (error) {
if (!(error instanceof ProductSelectionCodecError)) throw error
// Stale/ambiguous URL input is not a catalog transport failure.
normalized = codec.parse('')
}
const selection = resolveProductSelection(detail, normalized)
const selectionQuery = codec.stringify(normalized)
const href = `/products/${encodeURIComponent(slug)}${selectionQuery ? `?${selectionQuery}` : ''}`
return (
<main>
<h1>
{typeof detail.product.title === 'string'
? detail.product.title
: (detail.product.title.en ?? detail.product.title.ko ?? 'Untitled')}
</h1>
<a href={href}>Selection link</a>
<p>
{selection.selectedVariant ? 'Variant selected' : 'Choose an option'}
</p>
</main>
)
}Normal, empty, and failure results
A found product displays its title and normalized product selection link. Follow a listing swatch URL; the detail codec parses the query and resolves a variant or partial selection. codec.stringify() returns the query without ?, so the example inserts the separator only when needed. Missing and non-public detail results call notFound(); the API’s recognized detail 404 reasons are not_found and not_published. Feature denial (feature_disabled) is a thrown 403, as is workspace mismatch. An unrelated deployment 404 still throws and reaches the read error boundary.
Selection URL and public stock boundary
New outbound links use stable IDs: variant=<variantId> for a full selection or opt.<optionId>=<valueId> for partial choices. Old slug-based option URLs are compatibility input. Labels and slugs are not canonical identity. Reuse IDs from the actual detail response rather than inventing option keys; repeated query values are ignored by this page. Both recipes explicitly opt into canonical-id; the SDK default is slug-compat. A stale, unknown or ambiguous selection query resets to an empty selection against fresh detail and produces a clean Selection link. It does not enter the network retry UI. Resolve the selection again before purchase.
Catalog detail permits published and unlisted products and omits raw stock quantities. Selection availability is a display hint. Do not render internal stock fields or treat an available variant as reserved. Quantity-sensitive decisions use stock checks and checkout.
Next Actions
Return to Ecommerce for follow-on cart/checkout work and SDK for media, cache resources and selection helpers. Privileged inventory reads belong on a trusted server with server credentials, outside this catalog recipe.