Product detail page
Render product detail and product selection with async route params.
Prepare
Use the same key, market, and content locale as Product listing. This page uses the Next.js 16 Promise params and searchParams and needs no external ProductView component. Add the listing recipe’s read error boundary at app/products/error.tsx.
The example runs on each request in a Next.js App Router app. Build Options lists the tested versions and setup.
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 shows its title and a normalized product selection link. Follow a listing swatch URL: the detail codec parses the query and resolves a variant or a partial selection. codec.stringify() returns the query without ?, so the example adds the separator only when needed.
| Result | Page behavior |
|---|---|
Missing (not_found) or not published (not_published) | Calls notFound(). |
Feature disabled (feature_disabled) or workspace mismatch | Throws a 403, shown by the read error boundary. |
| Any other 404, for example from a wrong deployment URL | Throws, shown by the read error boundary. |
Selection URL and public stock boundary
New links use stable IDs: variant=<variantId> for a full selection, or opt.<optionId>=<valueId> for a partial one. Old slug-based option URLs are still accepted. Labels and slugs are not canonical selection identifiers. Take IDs from the actual detail response instead of inventing option keys; this page ignores repeated query values. Both recipes opt into canonical-id, while the SDK default is slug-compat. A stale, unknown, or ambiguous selection resets to an empty selection against fresh detail and produces a clean selection link, without showing the network retry. Resolve the selection again before purchase.
Catalog detail returns published and unlisted products without raw stock quantities. Selection availability is a display hint: do not show internal stock fields or treat an available variant as reserved. Use stock checks and Checkout for decisions that depend on quantity.
Next Actions
Continue with Ecommerce: Complete a guest purchase for Cart, Checkout, PortOne Standard V2 payment, and order confirmation. See Gallery image and video items for media and Storefront cache resources for cache invalidation. Privileged inventory reads belong on a trusted server with server credentials, not in this catalog recipe.