> For the complete Lynkow documentation index in agent-friendly format, see [llms.txt](/llms.txt).

# Product Catalog

**Publié le** : 2026-08-14
**Catégorie** : Features

> Browse a storefront catalog read-only: list products, fetch one by id or slug, and read collections and the category tree. Previous: [Guide 17](/docs/sdk/advanced/visual-editor) | Next: [Guide 19](/docs/sdk/features/commerce-reservations)

> **Reference** - This guide is a tutorial. For exhaustive, always-current signatures and field lists, see the auto-generated reference (it regenerates on every `docs:sync`, so it never goes stale):
> 
> - SDK: [`ProductsService`](/docs/sdk/services/sdk-products-service), [`ProductCollectionsService`](/docs/sdk/services/sdk-product-collections-service), [`ProductCategoriesService`](/docs/sdk/services/sdk-product-categories-service)
> - Types: [SDK Types](/docs/sdk/types)
> - API: [Storefront: Commerce](/docs/storefront-api/storefront-commerce)

## Prerequisites

- Lynkow SDK installed ([Quick Start](/docs/sdk/getting-started/quick-start))
- A **publishable key**. The commerce endpoints reject any call that does not carry one, so they throw a `LynkowError` with code `'UNAUTHORIZED'` if it is missing.

The publishable key is in the admin dashboard under **Site Settings**. It has the form `lkw_pk_` followed by 24 characters. Pass it once to `createClient(...)` and every commerce request the client makes will carry it:

```typescript
import { createClient } from 'lynkow'

const lynkow = createClient({
  siteId: '550e8400-e29b-41d4-a716-446655440000',
  publishableKey: 'lkw_pk_9f8g7h6j5k4w3m2n1p0qrstv',
})
```

The catalog is read-only: `list()`, `getBySlug()`, and the collection and category readers below only ever return **published**, merchant-visible entries. There is no create or update surface here.

## List products

`lynkow.products.list(filters?)` returns a paginated page of published products, newest first by default. Both `data` and `meta` come back, so you can render the page and drive pagination controls.

```typescript
// Each product (partial - see SDK Types for the full shape):
// { id, slug, title, priceCents, currency, thumbnail, kind }
const { data, meta } = await lynkow.products.list({ page: 1, limit: 12 })

for (const product of data) {
  const price = (product.priceCents / 100).toFixed(2)
  console.log(`${product.title} - ${price} ${product.currency}`)
}

console.log(`Page ${meta.currentPage} of ${meta.lastPage} (${meta.total} products)`)
```

`priceCents` is the price in the smallest currency unit (cents), so divide by 100 to display. `thumbnail` is the primary image URL, or `null` when the product has no images.

### Filter, search, and sort

Every filter is optional. `kind` narrows to `'standard'` products or `'bookable'` reservation products, `collection` and `category` accept a slug or a wire id, and `sortBy` / `sortOrder` control ordering:

```typescript
// Bookable products only, cheapest first, searching "cruise".
const { data } = await lynkow.products.list({
  kind: 'bookable',
  search: 'cruise',
  sortBy: 'price_cents',
  sortOrder: 'asc',
  limit: 20,
})
```

`category` matches the target category **and** its sub-categories. `sortBy` accepts `'title'`, `'price_cents'`, or `'created_at'` (the default).

> A `kind: 'bookable'` product is a reservation product. Route it to the availability and booking flow in [Guide 19](/docs/sdk/features/commerce-reservations) rather than a buy button.

## Get one product

`lynkow.products.getBySlug(idOrSlug)` reads a single published product by its URL slug or its `prod_...` wire id. It is named `getBySlug` (not `get`) but accepts either identifier, which makes it the natural resolver for a product page route.

```typescript
// app/products/[slug]/page.tsx (Next.js App Router)
import { notFound } from 'next/navigation'
import { isLynkowError } from 'lynkow'
import { lynkow } from '@/lib/lynkow'

export default async function ProductPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params

  try {
    const product = await lynkow.products.getBySlug(slug)
    const price = (product.priceCents / 100).toFixed(2)

    return (
      <article>
        <h1>{product.title}</h1>
        <p>{price} {product.currency}</p>
        {product.description ? <p>{product.description}</p> : null}
      </article>
    )
  } catch (error) {
    if (isLynkowError(error) && error.code === 'NOT_FOUND') notFound()
    throw error
  }
}
```

A missing, draft, or archived product throws a `LynkowError` with code `'NOT_FOUND'`, which the example maps to a Next.js `notFound()`.

### Render an accessible gallery

Prefer `product.gallery` over `product.images` when you need alt text or captions: it carries the same images in the same order, each with its display metadata.

```typescript
// Each gallery entry (partial): { url, alt, caption }
const product = await lynkow.products.getBySlug('sunset-dinner-cruise')

for (const image of product.gallery) {
  render(
    `<figure>
       <img src="${image.url}" alt="${image.alt ?? product.title}" />
       ${image.caption ? `<figcaption>${image.caption}</figcaption>` : ''}
     </figure>`
  )
}
```

Fall back to the product `title` when `alt` is `null`, as shown. A product with no images returns an empty `gallery` array.

### Products with variants

When a product has variants (for example size or color), `product.variants` is populated and each variant carries its own selling price; otherwise it is an empty array and the product-level `priceCents` is the selling price.

```typescript
// Each variant (partial): { id, title, priceCents, optionValues }
const product = await lynkow.products.getBySlug('classic-tee')

if (product.variants.length > 0) {
  for (const variant of product.variants) {
    const label = variant.optionValues.map((ov) => ov.value).join(' / ')
    console.log(`${label}: ${(variant.priceCents / 100).toFixed(2)} ${product.currency}`)
  }
} else {
  console.log(`${(product.priceCents / 100).toFixed(2)} ${product.currency}`)
}
```

## Collections

`lynkow.productCollections.list(filters?)` returns the merchant-visible collections, paginated. A collection's `slug` is exactly what the `collection` filter on `products.list()` accepts, so the two compose into a collection landing page.

```typescript
// Each collection (partial): { id, slug, title, image }
const { data: collections } = await lynkow.productCollections.list({ limit: 20 })

for (const collection of collections) {
  console.log(collection.title, `-> /collections/${collection.slug}`)
}

// Products in one collection:
const { data: products } = await lynkow.products.list({
  collection: collections[0]?.slug,
  limit: 12,
})
```

## Category tree

`lynkow.productCategories.tree()` returns the visible category tree as an array of root nodes. Nesting is carried recursively by each node's `children`, so you can render a multi-level navigation menu in one call.

```typescript
// Each node (partial): { id, slug, name, children }
const roots = await lynkow.productCategories.tree()

function renderBranch(node) {
  console.log(node.name, `-> /categories/${node.slug}`)
  for (const child of node.children) renderBranch(child)
}

for (const root of roots) renderBranch(root)
```

A leaf category has an empty `children` array. A node's `slug` is the value the `category` filter on `products.list()` expects, and because that filter includes sub-categories, filtering by a parent slug returns everything below it.

## Reference

Do not treat this guide as the field list. For every method, signature, and property, link the source of truth:

- **SDK services:** [`ProductsService`](/docs/sdk/services/sdk-products-service), [`ProductCollectionsService`](/docs/sdk/services/sdk-product-collections-service), [`ProductCategoriesService`](/docs/sdk/services/sdk-product-categories-service) - every method and signature.
- **Types:** [SDK Types](/docs/sdk/types) - the full `Product`, `ProductCollection`, and `ProductCategoryTreeNode` shapes.
- **API:** [Storefront: Commerce](/docs/storefront-api/storefront-commerce) - request and response schemas.

## Next Steps

- [Guide 19: Reservations and Deposits](/docs/sdk/features/commerce-reservations) - turn a `bookable` product into availability, a booking, and an optional deposit.