@wocart/storefront 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ follows [semantic versioning](https://semver.org/): breaking changes ship only
5
5
  in a new major version, and an export is marked `@deprecated` for at least one
6
6
  minor release before a major removes it.
7
7
 
8
+ ## 1.9.0 — 2026-10-17
9
+
10
+ ### Added
11
+
12
+ - Page type `collection` at `/collections/:handle`: a collection's page,
13
+ which a merchant's menu can now link to. `storefrontPath("collection", {
14
+ handle })` builds it, `matchStorefrontPath` recognises it, and the WhatsApp
15
+ widget counts it as the catalog surface.
16
+ - `store.collections.get(handle)` → `StoreCollection` (handle, title,
17
+ description, in the client's language), `null` for an unknown handle, which
18
+ is not reported as a failure. `StoreCollectionSchema`.
19
+
20
+ ## 1.8.0 — 2026-10-17
21
+
22
+ ### Added
23
+
24
+ - **Collections.** `products.list({ collection: "best-sellers" })` lists a
25
+ collection — a list of products the merchant curates by hand — in the
26
+ merchant's order; an unknown handle lists nothing. `products.list({ onSale:
27
+ true })` lists the products priced below their compare-at price. Both need
28
+ a server with collections.
29
+
30
+ ### Deprecated
31
+
32
+ - `products.list({ featured: true })` and `ProductList.storeFeatured`: they
33
+ mean the `featured` collection; removed in 2.0.
34
+ - `StoreConfig.primaryColor`, `accentColor`, `bgColor`, `fontFamily` and
35
+ `fontUrl`. A store's colors and font are its theme's settings: a theme
36
+ declares them (`defineTheme({ settings })` in `@wocart/theme`) and reads
37
+ `themeSettings`. The fields still arrive — the values saved before, no
38
+ longer editable (`fontUrl` is always null) — and are removed in 2.0.
39
+ - `StoreConfig.announcementBar`: always null. An announcement is a theme's
40
+ own content, declared as a theme setting; removed in 2.0.
41
+
8
42
  ## 1.7.0 — 2026-10-17
9
43
 
10
44
  ### Added
package/USAGE.md CHANGED
@@ -105,6 +105,7 @@ break those links.
105
105
  | `home` | `/` | the home page | required |
106
106
  | `catalog` | `/products` | every product | required |
107
107
  | `category` | `/category/:slug` | one category's products | required when the store has categories |
108
+ | `collection` | `/collections/:handle` | one collection's products, in the merchant's order | required — a menu can link to any collection |
108
109
  | `product` | `/products/:handle` | one product (its public handle, never an id) | required |
109
110
  | `page` | `/pages/:slug` | a legal or merchant page | required — checkout consent links to the store's legal pages |
110
111
  | `landing` | `/lp/:slug` | a landing page | required when the store runs landing pages |
@@ -483,23 +484,25 @@ store.
483
484
  ## Store configuration
484
485
 
485
486
  Use config.get() near the root of every page. It is the theme's starting point
486
- for branding and feature flags:
487
+ for store identity and feature flags:
487
488
 
488
489
  ~~~ts
489
490
  const config = await store.config.get();
490
491
 
491
492
  const cartEnabled = config?.cartEnabled === true;
492
493
  const currencySymbol = config?.currencySymbol ?? "دج";
493
- const announcement = config?.announcementBar ?? null;
494
494
  ~~~
495
495
 
496
496
  StoreConfig contains the current store identity and presentation fields,
497
497
  including:
498
498
 
499
- - name, domain, logoUrl, themeId, colors, fontFamily, and fontUrl;
499
+ - name, domain, logoUrl and themeId (the colors, fontFamily and fontUrl
500
+ fields are deprecated: colors and fonts are theme settings, read from
501
+ themeSettings);
500
502
  - lang (the primary language), locales, contentOverrides, themeSettings,
501
503
  currency, currencySymbol, metaTitle, metaDescription, and ogImage;
502
- - announcementBar and reviewsEnabled;
504
+ - reviewsEnabled (announcementBar is deprecated and always null: an
505
+ announcement is a theme's own setting);
503
506
  - cartEnabled, freeShippingThreshold, and cartShippingMode;
504
507
  - checkoutForm policy;
505
508
  - OTP and Turnstile settings;
@@ -630,11 +633,33 @@ decides whether a product is publicly available.
630
633
  ~~~ts
631
634
  const products = await store.products.list({
632
635
  categoryId: selectedCategoryId,
633
- featured: true,
634
636
  limit: 12,
635
637
  });
636
638
  ~~~
637
639
 
640
+ **Collections** are lists of products the merchant curates by hand in the
641
+ dashboard (Products → Collections): every store starts with `featured`,
642
+ `best-sellers` and `new-arrivals`, and may add any other. A collection lists
643
+ in the merchant's order:
644
+
645
+ ~~~ts
646
+ const bestSellers = await store.products.list({ collection: "best-sellers", limit: 8 });
647
+ const onSale = await store.products.list({ onSale: true, limit: 8 });
648
+ ~~~
649
+
650
+ - Never hardcode which collection a section shows: declare a `collection`
651
+ field (`@wocart/theme`) with a default handle, so the merchant picks it.
652
+ - An empty or unknown collection lists nothing — show nothing, never the
653
+ whole catalog instead: "Best sellers" means the merchant's picks.
654
+ - `onSale` is computed: a product priced below its compare-at price.
655
+ - A collection has a page of its own (page type `collection`,
656
+ `/collections/:handle`) that a merchant's menu can link to. Name it with
657
+ `store.collections.get(handle)` — title and description in the client's
658
+ language, `null` for an unknown handle (answer a real 404) — and list it
659
+ with `products.list({ collection: handle })`.
660
+ - `featured: true` and `ProductList.storeFeatured` are deprecated: they mean
661
+ the `featured` collection.
662
+
638
663
  Product lists return ProductList objects. ProductList and Product are
639
664
  different shapes: a list item is not a detail object and must not be cast to
640
665
  Product. ProductList carries list-oriented fields such as coverImage and
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Collections Namespace - the merchant's curated product lists
3
+ *
4
+ * A collection has a page of its own (`/collections/:handle`, page type
5
+ * `collection`): `get(handle)` names it — title and description in the
6
+ * client's language — and `products.list({ collection })` lists its products
7
+ * in the merchant's order.
8
+ *
9
+ * Failure contract: `get(handle)` → null means "no such collection" or a
10
+ * failed read — the page answers a real 404.
11
+ */
12
+ import { type ClientCore } from "./core.js";
13
+ import type { StoreCollection } from "../types.js";
14
+ export interface CollectionsNamespace {
15
+ /** A collection by handle (`best-sellers`). Resolves to `null` for an unknown handle or a failed read. */
16
+ get(handle: string): Promise<StoreCollection | null>;
17
+ }
18
+ export declare function createCollectionsNamespace(core: ClientCore): CollectionsNamespace;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Collections Namespace - the merchant's curated product lists
3
+ *
4
+ * A collection has a page of its own (`/collections/:handle`, page type
5
+ * `collection`): `get(handle)` names it — title and description in the
6
+ * client's language — and `products.list({ collection })` lists its products
7
+ * in the merchant's order.
8
+ *
9
+ * Failure contract: `get(handle)` → null means "no such collection" or a
10
+ * failed read — the page answers a real 404.
11
+ */
12
+ import { StoreCollectionSchema } from "../schemas.js";
13
+ import { STOREFRONT_OPERATIONS } from "../domain.js";
14
+ import { localizedPath } from "./paths.js";
15
+ import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
16
+ export function createCollectionsNamespace(core) {
17
+ return {
18
+ async get(handle) {
19
+ try {
20
+ return await core.get(localizedPath(core.config, `/store/collections/${encodeURIComponent(handle)}`), StoreCollectionSchema, { operation: STOREFRONT_OPERATIONS.collectionsGet });
21
+ }
22
+ catch (error) {
23
+ if (!isNotFoundAnswer(error))
24
+ reportDegradedRead(core.config, error, "collections.get");
25
+ return null;
26
+ }
27
+ },
28
+ };
29
+ }
@@ -2,7 +2,8 @@
2
2
  * Store Client Factory — the SDK's front door.
3
3
  *
4
4
  * One server-side call builds the whole read/write surface a theme needs, over one
5
- * shared core: config, products, categories, pages, menus, shipping, geo, cart,
5
+ * shared core: config, products, categories, collections, pages, menus,
6
+ * shipping, geo, cart,
6
7
  * orders. The core is internal — consumers see only namespaces.
7
8
  *
8
9
  * One core means one connection story for every namespace: every read is
@@ -32,6 +33,7 @@ export declare function createStoreClient(config: StoreScope): {
32
33
  categories: import("./categories.js").CategoriesNamespace;
33
34
  pages: import("./pages.js").PagesNamespace;
34
35
  menus: import("./menus.js").MenusNamespace;
36
+ collections: import("./collections.js").CollectionsNamespace;
35
37
  pageData: import("./page-data.js").PageDataNamespace;
36
38
  shipping: import("./shipping.js").ShippingNamespace;
37
39
  geo: import("./geo.js").GeoNamespace;
@@ -2,7 +2,8 @@
2
2
  * Store Client Factory — the SDK's front door.
3
3
  *
4
4
  * One server-side call builds the whole read/write surface a theme needs, over one
5
- * shared core: config, products, categories, pages, menus, shipping, geo, cart,
5
+ * shared core: config, products, categories, collections, pages, menus,
6
+ * shipping, geo, cart,
6
7
  * orders. The core is internal — consumers see only namespaces.
7
8
  *
8
9
  * One core means one connection story for every namespace: every read is
@@ -32,6 +33,7 @@ import { createProductsNamespace } from "./products.js";
32
33
  import { createCategoriesNamespace } from "./categories.js";
33
34
  import { createPagesNamespace } from "./pages.js";
34
35
  import { createMenusNamespace } from "./menus.js";
36
+ import { createCollectionsNamespace } from "./collections.js";
35
37
  import { createPageDataNamespace } from "./page-data.js";
36
38
  import { createShippingNamespace } from "./shipping.js";
37
39
  import { createGeoNamespace } from "./geo.js";
@@ -46,6 +48,7 @@ export function createStoreClient(config) {
46
48
  categories: createCategoriesNamespace(core),
47
49
  pages: createPagesNamespace(core),
48
50
  menus: createMenusNamespace(core),
51
+ collections: createCollectionsNamespace(core),
49
52
  pageData: createPageDataNamespace(core),
50
53
  shipping: createShippingNamespace(core),
51
54
  geo: createGeoNamespace(core),
@@ -26,7 +26,7 @@ export interface ProductsNamespace {
26
26
  get(handle: string): Promise<Product | null>;
27
27
  /**
28
28
  * List products with optional filters
29
- * @param filters - Optional category, featured flag, limit and offset
29
+ * @param filters - Optional collection, category, on-sale flag, limit and offset
30
30
  * @returns Array of products
31
31
  */
32
32
  list(filters?: ProductListFilters): Promise<ProductList[]>;
@@ -28,6 +28,8 @@ export function createProductsNamespace(core) {
28
28
  try {
29
29
  const products = await core.get(localizedPath(core.config, "/store/products", {
30
30
  categoryId: filters?.categoryId || undefined,
31
+ collection: filters?.collection || undefined,
32
+ onSale: filters?.onSale ? "true" : undefined,
31
33
  featured: filters?.featured ? "true" : undefined,
32
34
  limit: filters?.limit === undefined ? undefined : String(filters.limit),
33
35
  offset: filters?.offset ? String(filters.offset) : undefined,
package/dist/domain.d.ts CHANGED
@@ -354,6 +354,7 @@ export declare const STOREFRONT_OPERATIONS: {
354
354
  readonly pagesGet: "pages.get";
355
355
  readonly pagesLanding: "pages.landing";
356
356
  readonly menusGet: "menus.get";
357
+ readonly collectionsGet: "collections.get";
357
358
  readonly shippingRates: "shipping.rates";
358
359
  readonly shippingRate: "shipping.rate";
359
360
  readonly geoCommunes: "geo.communes";
@@ -392,6 +393,7 @@ export declare const STOREFRONT_RETRY_POLICIES: {
392
393
  readonly "pages.get": "safe-read";
393
394
  readonly "pages.landing": "safe-read";
394
395
  readonly "menus.get": "safe-read";
396
+ readonly "collections.get": "safe-read";
395
397
  readonly "products.get": "safe-read";
396
398
  readonly "products.list": "safe-read";
397
399
  readonly "products.reviews.list": "safe-read";
package/dist/domain.js CHANGED
@@ -207,6 +207,7 @@ export const STOREFRONT_OPERATIONS = {
207
207
  pagesGet: "pages.get",
208
208
  pagesLanding: "pages.landing",
209
209
  menusGet: "menus.get",
210
+ collectionsGet: "collections.get",
210
211
  shippingRates: "shipping.rates",
211
212
  shippingRate: "shipping.rate",
212
213
  geoCommunes: "geo.communes",
@@ -244,6 +245,7 @@ export const STOREFRONT_RETRY_POLICIES = {
244
245
  [STOREFRONT_OPERATIONS.pagesGet]: "safe-read",
245
246
  [STOREFRONT_OPERATIONS.pagesLanding]: "safe-read",
246
247
  [STOREFRONT_OPERATIONS.menusGet]: "safe-read",
248
+ [STOREFRONT_OPERATIONS.collectionsGet]: "safe-read",
247
249
  [STOREFRONT_OPERATIONS.productsGet]: "safe-read",
248
250
  [STOREFRONT_OPERATIONS.productsList]: "safe-read",
249
251
  [STOREFRONT_OPERATIONS.productsReviewsList]: "safe-read",
package/dist/routes.d.ts CHANGED
@@ -20,6 +20,7 @@ export declare const STOREFRONT_PAGE_PATHS: {
20
20
  readonly home: "/";
21
21
  readonly catalog: "/products";
22
22
  readonly category: "/category/:slug";
23
+ readonly collection: "/collections/:handle";
23
24
  readonly product: "/products/:handle";
24
25
  readonly page: "/pages/:slug";
25
26
  readonly landing: "/lp/:slug";
package/dist/routes.js CHANGED
@@ -3,6 +3,7 @@ export const STOREFRONT_PAGE_PATHS = {
3
3
  home: "/",
4
4
  catalog: "/products",
5
5
  category: "/category/:slug",
6
+ collection: "/collections/:handle",
6
7
  product: "/products/:handle",
7
8
  page: "/pages/:slug",
8
9
  landing: "/lp/:slug",
@@ -58,6 +59,7 @@ const WIDGET_SURFACE = {
58
59
  home: "home",
59
60
  catalog: "catalog",
60
61
  category: "catalog",
62
+ collection: "catalog",
61
63
  product: "product",
62
64
  page: "pages",
63
65
  landing: "landing",
package/dist/schemas.d.ts CHANGED
@@ -246,6 +246,12 @@ interface StoreMenuItemShape {
246
246
  type: string;
247
247
  children: StoreMenuItemShape[];
248
248
  }
249
+ /** A collection's page — GET /store/collections/{handle}. Its products come from `products.list({ collection })`. */
250
+ export declare const StoreCollectionSchema: z.ZodObject<{
251
+ handle: z.ZodString;
252
+ title: z.ZodString;
253
+ description: z.ZodNullable<z.ZodString>;
254
+ }, z.core.$strip>;
249
255
  export declare const StoreMenuSchema: z.ZodObject<{
250
256
  handle: z.ZodString;
251
257
  title: z.ZodString;
package/dist/schemas.js CHANGED
@@ -82,6 +82,7 @@ const productBaseFields = {
82
82
  visibility: z.boolean(),
83
83
  status: z.enum(["DRAFT", "ACTIVE", "ARCHIVED"]),
84
84
  showInStore: z.boolean(),
85
+ /** @deprecated Whether the product is in the `featured` collection; list it with `collection: "featured"`. Removed in 2.0. */
85
86
  storeFeatured: z.boolean(),
86
87
  /**
87
88
  * @deprecated Always null: the server never serves a removed product. The
@@ -144,6 +145,12 @@ export const StoreMenuItemSchema = z.lazy(() => z.object({
144
145
  type: z.string(),
145
146
  children: z.array(StoreMenuItemSchema),
146
147
  }));
148
+ /** A collection's page — GET /store/collections/{handle}. Its products come from `products.list({ collection })`. */
149
+ export const StoreCollectionSchema = z.object({
150
+ handle: z.string(),
151
+ title: z.string(),
152
+ description: z.string().nullable(),
153
+ });
147
154
  export const StoreMenuSchema = z.object({
148
155
  handle: z.string(),
149
156
  title: z.string(),
package/dist/types.d.ts CHANGED
@@ -64,10 +64,31 @@ export interface StoreConfig {
64
64
  domain: string | null;
65
65
  logoUrl: string | null;
66
66
  themeId: string;
67
+ /**
68
+ * @deprecated A store's colors and font are its theme's settings: declare
69
+ * them in `defineTheme({ settings })` and read `themeSettings`. This is the
70
+ * value saved before that, no longer editable; removed in 2.0.
71
+ */
67
72
  primaryColor: string;
73
+ /**
74
+ * @deprecated A store's colors and font are its theme's settings: declare
75
+ * them in `defineTheme({ settings })` and read `themeSettings`. This is the
76
+ * value saved before that, no longer editable; removed in 2.0.
77
+ */
68
78
  accentColor: string;
79
+ /**
80
+ * @deprecated A store's colors and font are its theme's settings: declare
81
+ * them in `defineTheme({ settings })` and read `themeSettings`. This is the
82
+ * value saved before that, no longer editable; removed in 2.0.
83
+ */
69
84
  bgColor: string;
85
+ /**
86
+ * @deprecated A store's colors and font are its theme's settings: declare
87
+ * them in `defineTheme({ settings })` and read `themeSettings`. This is the
88
+ * value saved before that, no longer editable; removed in 2.0.
89
+ */
70
90
  fontFamily: string;
91
+ /** @deprecated Always null; removed in 2.0. */
71
92
  fontUrl: string | null;
72
93
  /** The store's primary language: a BCP 47 code such as `ar`. */
73
94
  lang: string;
@@ -99,6 +120,10 @@ export interface StoreConfig {
99
120
  metaTitle: string | null;
100
121
  metaDescription: string | null;
101
122
  ogImage: string | null;
123
+ /**
124
+ * @deprecated Always null. An announcement is a theme's own content: declare
125
+ * it as a theme setting and read `themeSettings`. Removed in 2.0.
126
+ */
102
127
  announcementBar: string | null;
103
128
  reviewsEnabled: boolean;
104
129
  /** When true the storefront renders the cart alongside the direct order form. */
@@ -187,6 +212,13 @@ export interface StorePagePublic {
187
212
  metaTitle: string | null;
188
213
  metaDescription: string | null;
189
214
  }
215
+ /** A collection as its page names it — GET /store/collections/{handle}. */
216
+ export interface StoreCollection {
217
+ handle: string;
218
+ /** In the client's language when the store translated it, else the primary. */
219
+ title: string;
220
+ description: string | null;
221
+ }
190
222
  /** A navigation menu — GET /store/menus/{handle}. */
191
223
  export interface StoreMenu {
192
224
  handle: string;
@@ -494,6 +526,16 @@ export interface ReviewSubmitResult {
494
526
  */
495
527
  export interface ProductListFilters {
496
528
  categoryId?: string;
529
+ /**
530
+ * Only this collection's products, by handle (`best-sellers`), in the order
531
+ * the merchant gave them. A collection is a list the merchant curates in the
532
+ * dashboard; a theme names it through a `collection` theme setting. An
533
+ * unknown handle lists nothing.
534
+ */
535
+ collection?: string;
536
+ /** Only products priced below their compare-at price. */
537
+ onSale?: boolean;
538
+ /** @deprecated Use `collection: "featured"`. */
497
539
  featured?: boolean;
498
540
  /** Server-side cap; the server clamps anything larger. */
499
541
  limit?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/storefront",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Typed, validated SDK for Wocart cash-on-delivery storefronts: catalog reads, basket, checkout, OTP and order tracking, with Astro adapters.",
5
5
  "keywords": [
6
6
  "wocart",