Repository navigation
Products
window.$storefront.products gives you everything a catalog needs: paginated product and category listings with search, filters and sorting, full product details, customer ratings and the content of subscription products. This page also explains the built-in response cache (optimize, cache, force, cancellation), which every namespace shares.
| Method | Endpoint (XAPI) |
|---|---|
products.list(dir, offset, limit, options) |
GET /shops/@{shop}/products/all |
products.get(product_id, options) |
GET /shops/@{shop}/products/{product_id}/info |
products.rate.submitMyRate(product_id, user_rating) |
POST /shops/@{shop}/products/{product_id}/set-my-rating |
products.subscription.getContents(product_id, offset, limit, options) |
GET /shops/@{shop}/products/{product_id}/contents |
products.list(
dir: string | null = null, // category; null = top level, "*" = search everywhere
offset: number = 0, // items to skip (pagination)
limit: number = 10, // items to return
options?: XapiProductListTypes.IOptions,
): Promise<XapiProductListTypes.IResponse>dir is the category to list. The official Storefront passes the category name from its /:category_name-category route, null for the shop root, and "*" to search all products and categories.
const handleSuccessResponse = ({ products, folders, parent, total }) => {
this.products = products; // Product.IProduct[]
this.folders = folders; // sub-categories
this.parent = parent; // current category (with_parent: true)
this.total = total; // with_total: true
};
window.$storefront.products
.optimize(600) // Cache products for 600 seconds
.list(null, 0, 20, {
categories_count: 10,
with_parent: true,
with_page: true,
sort: "most_popular",
available: true,
search: null,
filter: JSON.stringify({ only_has_discount: true }),
with_total: true,
})
.cache(handleSuccessResponse)
.then(handleSuccessResponse)
.catch((error) => console.error(error));Note
products.fetchProducts(...) is a deprecated alias of products.list(...). It still works, but use list() in new code.
All options are optional and sent as query parameters (XapiProductListTypes.IOptions):
| Option | Type | Description |
|---|---|---|
sort |
"most_visited" | "most_popular" | "newest" | "bestselling" | "cheapest" | "most_expensive" | "random" | "related" |
Sort order. |
search |
string |
Search text. Use "*" to return all products. |
search_type |
"exact" | "quote" | "tax" | "valuation" | "new" |
Search mode. |
available |
boolean |
Only products that are in stock. Default false. |
types |
Product.ProductType[] |
Product types: PHYSICAL, VIRTUAL, SERVICE, FILE, SUBSCRIPTION. |
statuses |
Product.ProductStatus[] |
Product statuses. |
categories_count |
number |
Maximum number of sub-categories (folders) to return. |
with_parent |
boolean |
Return the current category as parent. |
with_page |
boolean |
With with_parent, also return the custom page linked to the category. |
with_total |
boolean |
Return the total count as total. |
products_only / categories_only
|
boolean |
Return only products or only categories (useful for "load more"). |
dirs |
string[] |
Several category IDs at once. |
surrounded |
boolean |
true: show only the selected categories; false: show items inside them. |
filter |
IFilter |
Advanced filters, see below. The official components send it as a JSON string (JSON.stringify(filter)). |
tags |
string[] |
Filter by product tags. |
vendor_id |
string |
Only products of one marketplace vendor. |
bounds |
[lng1, lat1, lng2, lat2] |
Only products located inside a map bounding box. |
map |
number | null |
Only products of a map tag. |
only_stared |
boolean |
Only products the customer starred (favorites). Note the spelling. |
only_in_wishlist |
boolean |
Only products in the customer's wishlist. |
The official related-products component also passes { product_id, sort: "related" } to list items related to a product.
| Field | Type | Description |
|---|---|---|
only_is_original |
boolean |
Only original products. |
only_has_discount |
boolean |
Only discounted products. |
prices |
[min, max][] |
Price ranges. |
brands |
string[] |
Brands. |
selected_variants |
any[][] |
Variant values (color, size…). |
selected_spec |
any[][] |
Specification values. |
limited_offer |
boolean | number |
Only limited-time offers; a number limits to offers with that many hours left. |
| Field | Type | Description |
|---|---|---|
products |
Product.IProduct[] |
Products on this page. |
folders |
Category.ICategory[] |
Categories. |
parent |
Category.ICategory |
Current category (with with_parent). |
total |
number |
Total products (with with_total). |
after, before
|
string | null |
Time-span search metadata. |
relation-mode |
string[] | "same-category" |
How related products were chosen. |
tax_profile, valuation, time_filter
|
object | null |
Extra search objects. |
async function loadMore() {
const { products } = await window.$storefront.products.list(
currentCategory,
this.products.length, // offset = what you already have
20,
{ sort: "newest", products_only: true },
);
this.products.push(...products);
}const imageUrl = window.CDN.GET_SHOP_IMAGE_PATH(product.icon, 300);To build product links that match the official Storefront, use ShopURLs.GetProductLink(shop, product.id) from Selldone Core, or the route name window.$storefront.routes.PRODUCT_PAGE if you use the same router names.
products.get(
product_id: string | number,
options?: {
"augmented-categories"?: number; // return n categories in the parent folder of the current category
"augmented-cross-sells"?: boolean; // return cross-selling items (max 3)
no_article?: boolean; // skip the article (long description)
},
): Promise<XapiProductGetTypes.IResponse>const handleSuccessResponse = ({ product }) => {
this.product = product;
};
window.$storefront.products
.optimize(60)
.get(12345, { "augmented-categories": 3, "augmented-cross-sells": true })
.cache(handleSuccessResponse) // may resolve instantly from a previously loaded list
.then(handleSuccessResponse)
.catch((error) => console.error(error));Response (XapiProductGetTypes.IResponse):
| Field | Description |
|---|---|
product |
The product, including article_pack (article, tags, can_edit, and the customer's liked / stared / follow / reported state), categories, and cross-sell data when requested. |
campaign_id, link_id
|
Campaign attribution of the visit. |
preferred-language |
Preferred language for the product content. |
vendors |
Marketplace only: vendor offers for this product (id, name, icon, variant_id, price, currency, discount, quantity, lead, …). |
categories |
Deprecated; use product.categories. |
The language of the returned article follows the X-Localization request header.
Smart cache: when you chain .cache(), get() searches products already returned by list() calls in the cache and calls your cache callback with { product } immediately, so the product page can render before the network request finishes.
A product exposes its rating criteria in product.ratings (id, name, value, count). When product.my_ratings is not null, the signed-in customer has bought the product and can rate it. Submit a map of rating criterion ID → value from 1 to 5:
// product.ratings = [{ id: 7, name: "Quality", ... }, { id: 8, name: "Value", ... }]
window.$storefront.products.rate
.submitMyRate(product.id, { 7: 5, 8: 4 })
.then(({ success, my_ratings, ratings, rate_count, rate }) => {
product.my_ratings = my_ratings;
product.ratings = ratings;
product.rate_count = rate_count;
product.rate = rate;
});Subscription products can have members-only content (posts and files). Fetch it page by page:
const showContents = ({ contents, total }) => {
// contents[i]: id, title, description, rate, rate_count, created_at, files…
};
window.$storefront.products.subscription
.optimize(60)
.getContents(product.id, 0, 10, { search: "", sortBy: "id", sortDesc: true })
.cache(showContents)
.then(showContents);This is paywalled content, intended for customers subscribed to the product.
Every API namespace extends APIAbstract from Selldone Core, so these helpers are available on products, shop, coupon, offer and the others. Caching and cancellation apply to promise-based GET calls such as list(), get(), fetchShop() or fetchOffers(); POST, PUT and DELETE calls are never cached.
| Helper | Effect |
|---|---|
.optimize(seconds = 60) |
If a stored response for the same request is newer than seconds, return it without calling the server. The setting stays on the namespace object until you call force(). |
.force() |
Clear the optimize duration so the next call always hits the server. |
.cancellation(true) |
Cancel an in-flight request to the same URL when a new one starts (great for fast category switching or search-as-you-type). A canceled request rejects with undefined. |
promise.cache(callback) |
Chain it before .then(). If a stored response exists, callback is called immediately with it. It also marks the request so the fresh response is stored. Skipped when optimize already served the stored value. |
Important
Responses are stored only for requests chained with .cache(). optimize() alone has nothing to reuse, so always combine the two: .optimize(600).list(...).cache(render).then(render).
window.$storefront.products
.cancellation() // drop older requests to the same URL
.optimize(300) // reuse data up to 5 minutes old
.list("shoes", 0, 20, { sort: "cheapest" })
.cache(render) // instant UI from cache
.then(render) // fresh data
.catch((error) => {
if (error === undefined) return; // canceled by a newer request
console.error(error);
});The cache is an in-memory LRU of 100 responses, shared by all namespaces and reset on page reload. CleanCache() from @selldone/core-js/server/APIAbstract clears it.
- Basket and Checkout: put the product in the cart.
- Coupons, Offers, Cashback and Lottery
- Page Builder Snippets: a ready-made products carousel.
- API Reference