Products and collections
The catalogue has four read shapes:
| Read | Returns | Use it on |
|---|---|---|
products.list(filters) | ProductList[] | Grids, rails, search results |
products.page(filters) | { items, hasNext, total } | Paginated catalogue pages |
products.search(text, filters) | { items, hasNext, total } | Search results |
products.get(handle) | Product | null | A product page |
collections.get(handle) | StoreCollection | null | A collection's page |
ProductList is a compact card shape. Product is the full detail shape with
variants, images, offers and quantity pricing.
In the snippets, store is the request's StoreClient: the client the
quick start creates, or Astro.locals.store.client
behind resolveStoreRequest.
List products
Code
The filters are:
| Filter | What it does |
|---|---|
categoryId | Products in that category |
collection | Products in that collection, in the merchant's order |
onSale | Products priced below their compare-at price |
limit | Server-side cap; the server clamps a larger value |
offset | Skip that many products |
sort | The order: newest, price-ascending or price-descending (PRODUCT_SORTS); absent, the listing's own |
There is no featured filter: the merchant's featured products are the
featured collection, collection: "featured".
Use products.page when the page needs a next link:
Code
page asks the server for one row beyond the page and trims it, so hasNext is
exact without a count query. A failed read is an empty final page, the same way
list degrades.
An unknown collection or category lists nothing. Do not treat that as an error on a catalogue page; show the empty state.
A page, its size and its order
A listing reads one page at a time, in the order the shopper picked or its own
(a collection's, a search's best match, else newest). PRODUCT_SORTS is the
list a sort control offers: newest, price-ascending, price-descending.
Code
total is always a number. A failed read is { items: [], hasNext: false, total: 0 }, so "no products" and "the read failed" look the same on the page;
the client's onError tells them apart.
Search
products.search(text, filters) finds the products whose name — in the
client's language or the catalog's — holds every word, or whose tags do, best
match first unless a sort is given. It takes the listing filters and pages
like products.page:
Code
The words are trimmed and cut to 100 characters. Blank words find nothing
without asking the server, never the whole catalog. A failed read is an empty
page, like products.page.
Product cards
ProductList carries what a card needs:
Code
The list is already filtered to what a shopper may see: it carries no status
or visibility field to re-check. variantOptions and tags arrive parsed, as
on a product page.
A list has no variant data. If a card needs “from 2,500 DA” for a variable
product, ProductList.price is the representative price the server sends; the
exact price comes from the product detail.
Image
coverImage is often null: a merchant may upload a gallery without choosing a
cover. Use productImageSource, which falls back to the first usable gallery
image:
Code
Render a placeholder when cardUrl is null. Do not render a broken <img> with
an empty src. Resizing and config.media are covered in
Money, images, WhatsApp and Meta events.
Product detail
A product page reads by handle, the public URL segment:
Code
getProductView answers the first sellable variant and the product's stock
state:
Code
For a variable product, product.inventory is a product-level summary. Check
variant.inventory before letting a shopper order a specific variant.
Variants
A variable product carries:
Code
variants holds only the variants a shopper can pick, in the merchant's
order. Take them as they come: there is no active flag or position to
filter or sort on.
The helper functions keep the picker honest:
Code
initialVariant opens on the merchant's default when it can be sold, otherwise
the first sellable variant. pickOption always returns a variant that exists,
or leaves the selection unchanged. optionStates tells a value apart as:
available— a sellable variant has it with the other choices;sold_out— the variant exists but has no stock;unavailable— no variant combines that value with the other choices.
Render unavailable values as disabled, and sold_out as visibly unavailable.
The server still validates the submitted variant.
Prices and offers
Prices are amounts in the store's currency (config.currency, DZD for an
Algerian store), not cents. Format them with formatMoney and the store's
currencySymbol; never divide by 100.
A product can carry three kinds of promotion:
| Kind | Field | Meaning |
|---|---|---|
| Compare-at price | compareAtPrice | A crossed-out reference price |
| Quantity pricing | quantityPricing.tiers | “2 for 3,500 DA” tiers |
| Buy X, get Y | offers | A gift or free delivery |
Quantity tiers
Code
product.quantityPricing is null when the product has no quantity tiers.
Each tier gives quantity, price, perUnit, savingsAmount, savePct and
freeShipping. These are display values. The total the shopper pays is always
the server's cart quote.
Buy X, get Y
Code
An offer can be free (a gift) or free_shipping. The server decides whether
the quantity qualifies and appends the reward. If the shopper picks a specific
tier, put its offerId on the basket line; a shared tier is then priced as the
shopper chose.
Do not calculate the promotion
A page may show “Buy 2, get 1 free”. It must not add the gift itself, subtract a
discount, or waive delivery. Send the chosen offerId and let
cart.validate / cart.quote answer with the priced lines, rewards and total.
Categories
categories.list() returns a flat, ordered list. Build the tree from
parentId; the SDK does not return nested children.
Code
Category pages read by slug:
Code
categories.list() is cached for the client's lifetime after a successful read.
A category rename is visible on the next server render, not in an old client.
Collections
A collection is a merchant-curated list with its own page:
Code
collections.get returns the handle, the title, the description, how many
products a shopper sees in it (productCount) and the photo of its first
listed product (image, a ProductImage or null). The products come from
products.page({ collection }) in the merchant's order. All of it is
translated when the client's language is published.
Descriptions and SEO
product.descriptionFormat says how to render the description:
"text"— render it as text, with line breaks preserved."html"— render it withset:html.
descriptionPlain is the text-only version for meta descriptions and anywhere
markup would be wrong. Use it for <meta name="description"> and JSON-LD.
Category and product rows carry metaTitle and metaDescription where present;
the store-level fallbacks are config.metaTitle and config.metaDescription.
Titles, structured data and canonical addresses
A product carries the title and description its merchant wrote for search
engines (metaTitle, metaDescription). productMeta(product) gives what a page
puts in its <title> and description: what the merchant wrote, else the name
and the start of the plain description — never markup — within
SEO_TEXT_LIMITS. productStructuredData gives the schema.org Product, written
with jsonLd, which escapes what could end the script. canonicalUrl keeps
only the page's path in its language and its listing page, never a sort, a
search, a coupon or an ad's click id:
Code
What this page does not cover
Reviews live on Reviews. The basket and server quote
live on Basket and quotes. Images and money helpers
are on Money, images, WhatsApp and Meta events.
Search is products.search, under "List products" above.