Browse a storefront catalog read-only: list products, fetch one by id or slug, and read collections and the category tree. Previous: Guide 17 | Next: Guide 19
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):
Prerequisites
Lynkow SDK installed (Quick Start)
A publishable key. The commerce endpoints reject any call that does not carry one, so they throw a
LynkowErrorwith 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:
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.
// 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:
// 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 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.
// 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.
// 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.
// 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.
// 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.
// 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,ProductCollectionsService,ProductCategoriesService- every method and signature.Types: SDK Types - the full
Product,ProductCollection, andProductCategoryTreeNodeshapes.API: Storefront: Commerce - request and response schemas.
Next Steps
Guide 19: Reservations and Deposits - turn a
bookableproduct into availability, a booking, and an optional deposit.