@wocart/storefront 1.6.0 → 1.7.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,30 @@ 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.7.0 — 2026-10-17
9
+
10
+ ### Added
11
+
12
+ - **Page types and their URLs.** `STOREFRONT_PAGE_PATHS` is the platform's
13
+ table of public paths (`home`, `catalog`, `category`, `product`, `page`,
14
+ `landing`, `checkout`, `thankYou`); `storefrontPath(type, params)` builds
15
+ one (encoded; a missing parameter throws) and `matchStorefrontPath(pathname)`
16
+ recognises one, exactly. Types `StorefrontPageType`, `StorefrontPathParams`,
17
+ `StorefrontPageMatch`.
18
+ - **Navigation menus.** `store.menus.get(handle)` reads a menu as a shopper
19
+ sees it: labels in the client's language, every `href` resolved by the
20
+ server, items leading nowhere public already left out; `null` for an
21
+ unknown handle, which is not reported to `onError`. Types `StoreMenu`,
22
+ `StoreMenuItem`, schemas `StoreMenuSchema`, `StoreMenuItemSchema`.
23
+ - `StoreConfig.themeSettings`: the merchant's values for the requesting
24
+ theme's settings, as saved — resolve them with `resolveThemeSettings` from
25
+ `@wocart/theme` 1.1. Optional: absent from a server that predates them.
26
+
27
+ ### Deprecated
28
+
29
+ - `STOREFRONT_ROUTES` lists six of the paths, without the home and the
30
+ catalog; use `STOREFRONT_PAGE_PATHS` and `matchStorefrontPath`.
31
+
8
32
  ## 1.6.0 — 2026-10-17
9
33
 
10
34
  ### Added
package/USAGE.md CHANGED
@@ -90,34 +90,55 @@ allowlists, delivery availability, stock, discounts, final totals, OTP
90
90
  acceptance, or tracking eligibility. Those rules are platform data or platform
91
91
  decisions. The theme presents them and gives the shopper a clear next action.
92
92
 
93
- ## The storefront route contract
93
+ ## Page types and their URLs
94
94
 
95
- A storefront's public paths are a platform contract, not a theme preference.
96
- Three parts of the platform meet on them: the theme renders the pages, the server writes the
97
- URL Meta is told about (`/lp/:slug` for a sale attributed to a landing page,
98
- `/thank-you` otherwise), and the dashboard prints them as the store's public
99
- links. These six patterns are frozen — restyle anything else, but change one of
100
- these only as a platform change:
95
+ A URL *means* something to the platform — a product, a category, a store
96
+ page — and that meaning is a **page type**. Each page type lives at one
97
+ public path. The paths are a platform contract, not a theme preference: the
98
+ platform builds them without running your theme (the dashboard prints a page's
99
+ public link, the server names the landing page in the URL Meta is told about,
100
+ the AI tools hand out landing-page links), so a theme that moved one would
101
+ break those links.
101
102
 
102
- | Route | Renders |
103
- | --- | --- |
104
- | `/products/:handle` | product detail (public handle, never an internal id) |
105
- | `/category/:slug` | a category listing |
106
- | `/pages/:slug` | a legal or CMS page |
107
- | `/lp/:slug` | a landing page |
108
- | `/checkout` | the checkout |
109
- | `/thank-you` | order confirmation |
110
-
111
- The root (`/`) and the catalog index (`/products`) carry no parameter and are
112
- the two further surfaces a theme implements.
113
-
114
- The contract is exported, so a theme can read it instead of repeating it:
115
- `STOREFRONT_ROUTES` is the table above, and `showsWidgetOn(surfaces, pathname)`
116
- is the platform's half of "which surface is this page" — the merchant's
117
- per-surface switches then mean the same thing as the dashboard's preview. The
118
- gate never prefix-matches: `/productsomething`, `/checkout-success` and
119
- `/thank-you-again` are no surface at all, never a widget on a page the merchant
120
- switched off.
103
+ | Page type | Path | Renders | Your theme |
104
+ | --- | --- | --- | --- |
105
+ | `home` | `/` | the home page | required |
106
+ | `catalog` | `/products` | every product | required |
107
+ | `category` | `/category/:slug` | one category's products | required when the store has categories |
108
+ | `product` | `/products/:handle` | one product (its public handle, never an id) | required |
109
+ | `page` | `/pages/:slug` | a legal or merchant page | required — checkout consent links to the store's legal pages |
110
+ | `landing` | `/lp/:slug` | a landing page | required when the store runs landing pages |
111
+ | `checkout` | `/checkout` | the cart's checkout | required when the store turns the cart on |
112
+ | `thankYou` | `/thank-you` | the order confirmation | required |
113
+
114
+ How a theme implements them, in Astro's own terms:
115
+
116
+ - **Render each page type at its path with a file route**:
117
+ `src/pages/products/[handle].astro`, `src/pages/lp/[slug].astro` … The
118
+ parameter's name in your file is yours; the path is the platform's.
119
+ - **Build every link with `storefrontPath`**, never a hand-written string:
120
+ `storefrontPath("product", { handle: product.handle })` → `/products/robe`,
121
+ `storefrontPath("checkout")` → `/checkout`. Parameters are encoded for you;
122
+ a missing one throws, so a link to nowhere fails loudly. Add the language
123
+ prefix with `localizeHref(storefrontPath(…), locale, routing)`.
124
+ - **Recognise a page with `matchStorefrontPath(pathname)`** →
125
+ `{ type: "product", params: { handle: "robe" } }`, or `null` for any other
126
+ path. It matches exactly — `/productsomething`, `/products/a/b` and
127
+ `/checkout-success` are nothing — and ignores the query, fragment and a
128
+ trailing slash. Pass the path without its language prefix.
129
+ - **Answer a missing resource with a real 404** (`return Astro.rewrite("/404")`),
130
+ never a redirect to the home page: a redirect turns a dead link into a
131
+ "soft 404" for search engines and ad reviews.
132
+ - **Every other route is yours.** `/about`, `/offers/summer`, a store
133
+ locator — add them as Astro file routes; nothing in the contract restricts
134
+ them. Avoid the platform's paths above and `/api/*` (your same-origin
135
+ endpoints, see "Storefront API routes").
136
+
137
+ `showsWidgetOn(surfaces, pathname)` is the platform's half of "should the
138
+ WhatsApp widget show here": the merchant switches surfaces on; the page type
139
+ says which surface a pathname is. `STOREFRONT_PAGE_PATHS` is the table above
140
+ as data. `STOREFRONT_ROUTES` is deprecated: it lists six of the paths, without
141
+ the home and the catalog.
121
142
 
122
143
  ## The five rules to remember
123
144
 
@@ -196,7 +217,8 @@ server directly.
196
217
  | `WILAYAS`, `ALGERIA_COMMUNES`, `communesForWilaya` | Reference data: 58 wilayas, 1,551 communes, one wilaya's rows |
197
218
  | `normalizeAlgerianPhone`, `toLocalAlgerianMobile` | The one Algerian phone rule (accept/format) |
198
219
  | `resolvePrefillText`, `buildWhatsAppHref`, `stripTrackingParams`, `truncateAtWordBoundary`, `PREFILL_TOKENS`, `MAX_RESOLVED_PREFILL_LENGTH` | WhatsApp prefill text and the `wa.me` link |
199
- | `showsWidgetOn`, `STOREFRONT_ROUTES` | Which surface a pathname is (the frozen route contract) |
220
+ | `storefrontPath`, `matchStorefrontPath`, `STOREFRONT_PAGE_PATHS` | The page types and their public paths: build a link, recognise a page |
221
+ | `showsWidgetOn`, `STOREFRONT_ROUTES` | Which widget surface a pathname is (`STOREFRONT_ROUTES` deprecated) |
200
222
  | `productImageSource` | Which photo represents a product (cover → gallery fallback) |
201
223
  | `getProductView`, `getProductListView` | Pure price, discount, default-variant, and stock values for detail and list projections |
202
224
  | `getOfferTierView` | Pure offer-tier price, compare-price, savings, and free-shipping projection; display-only, never order authority |
@@ -295,6 +317,7 @@ carries the key, which the package's own contract test asserts.
295
317
  | products | Product catalog and reviews | Product, ProductList, or empty/null |
296
318
  | categories | Catalog navigation | Category array |
297
319
  | pages | Legal and CMS pages, landing pages | StorePagePublic, LandingPage, or null |
320
+ | menus | The store's navigation menus | StoreMenu or null |
298
321
  | pageData | The store's saved page document | PageDocumentPayload or null |
299
322
  | shipping | Delivery prices | ShippingRates or one rate/null |
300
323
  | geo | Wilayas and communes | Static wilayas or Commune array |
@@ -474,8 +497,8 @@ StoreConfig contains the current store identity and presentation fields,
474
497
  including:
475
498
 
476
499
  - name, domain, logoUrl, themeId, colors, fontFamily, and fontUrl;
477
- - lang (the primary language), locales, contentOverrides, currency,
478
- currencySymbol, metaTitle, metaDescription, and ogImage;
500
+ - lang (the primary language), locales, contentOverrides, themeSettings,
501
+ currency, currencySymbol, metaTitle, metaDescription, and ogImage;
479
502
  - announcementBar and reviewsEnabled;
480
503
  - cartEnabled, freeShippingThreshold, and cartShippingMode;
481
504
  - checkoutForm policy;
@@ -483,6 +506,21 @@ including:
483
506
  - pixelId and conversionEvent;
484
507
  - public page links, legal contact information, and WhatsApp widget settings.
485
508
 
509
+ `themeSettings` holds the merchant's values for your theme's settings (the
510
+ ones it declares with `defineTheme({ settings })` in `@wocart/theme`), by
511
+ setting key, exactly as saved. It is sent only to a client created with
512
+ `theme`, is `{}` when the store saved none, and is absent from a server that
513
+ predates theme settings — `resolveThemeSettings` takes either. Never read it raw: resolve it
514
+ against your declaration, which checks every value and falls back to your
515
+ default for one it no longer accepts:
516
+
517
+ ~~~ts
518
+ import { resolveThemeSettings } from "@wocart/theme";
519
+
520
+ const { values } = resolveThemeSettings(theme, config?.themeSettings, { locales: [locale] });
521
+ const headerMenu = values.headerMenu; // a menu handle, for store.menus.get()
522
+ ~~~
523
+
486
524
  The checkout policy is not a suggestion. Use it to decide whether address,
487
525
  email, notes, delivery options, and custom fields are required, optional, or
488
526
  hidden. The policy includes:
@@ -716,6 +754,36 @@ Do not hardcode the legal navigation list. Use config.pages so the store's
716
754
  available public pages determine the links. If a page is null, render the
717
755
  theme's not-found or fallback state.
718
756
 
757
+ ### Navigation menus
758
+
759
+ A store's menus are store data, like its products: the merchant builds them in
760
+ the dashboard (Navigation) — a handle such as `main-menu`, a title, and up to
761
+ three levels of items, each pointing at a product, a category, a store page, a
762
+ landing page, the home, the catalog, or a URL. Your theme decides *where* a
763
+ menu shows; the merchant decides *what* it holds.
764
+
765
+ ~~~ts
766
+ const menu = await store.menus.get(settings.headerMenu); // "main-menu"
767
+
768
+ for (const item of menu?.items ?? []) {
769
+ const href = item.type === "url" ? item.href : localizeHref(item.href, locale, routing);
770
+ // item.label, item.children (same shape, at most three levels)
771
+ }
772
+ ~~~
773
+
774
+ - Name the menu through a `menu` theme setting rather than a hard-coded
775
+ handle, so the merchant picks which menu each place shows.
776
+ - Every link is resolved by the server: `href` is a path of the store without
777
+ a language prefix (pass it through `localizeHref`) or, for `type: "url"`,
778
+ the address the merchant entered — render it as is.
779
+ - An item whose destination is deleted or not public (a draft page, an
780
+ inactive product) is already left out, with its children. You never filter.
781
+ - Labels arrive in the client's language when the store translated them.
782
+ - `null` means the store has no menu with that handle, or the read failed:
783
+ render your own fallback navigation (for example the home and the catalog
784
+ from `storefrontPath`). A missing menu is not a failure and is not reported
785
+ to `onError`.
786
+
719
787
  ### Landing pages
720
788
 
721
789
  ~~~ts
@@ -2,7 +2,7 @@
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, shipping, geo, cart,
5
+ * shared core: config, products, categories, pages, menus, shipping, geo, cart,
6
6
  * orders. The core is internal — consumers see only namespaces.
7
7
  *
8
8
  * One core means one connection story for every namespace: every read is
@@ -31,6 +31,7 @@ export declare function createStoreClient(config: StoreScope): {
31
31
  products: import("./products.js").ProductsNamespace;
32
32
  categories: import("./categories.js").CategoriesNamespace;
33
33
  pages: import("./pages.js").PagesNamespace;
34
+ menus: import("./menus.js").MenusNamespace;
34
35
  pageData: import("./page-data.js").PageDataNamespace;
35
36
  shipping: import("./shipping.js").ShippingNamespace;
36
37
  geo: import("./geo.js").GeoNamespace;
@@ -2,7 +2,7 @@
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, shipping, geo, cart,
5
+ * shared core: config, products, categories, pages, menus, shipping, geo, cart,
6
6
  * orders. The core is internal — consumers see only namespaces.
7
7
  *
8
8
  * One core means one connection story for every namespace: every read is
@@ -31,6 +31,7 @@ import { createConfigNamespace } from "./config.js";
31
31
  import { createProductsNamespace } from "./products.js";
32
32
  import { createCategoriesNamespace } from "./categories.js";
33
33
  import { createPagesNamespace } from "./pages.js";
34
+ import { createMenusNamespace } from "./menus.js";
34
35
  import { createPageDataNamespace } from "./page-data.js";
35
36
  import { createShippingNamespace } from "./shipping.js";
36
37
  import { createGeoNamespace } from "./geo.js";
@@ -44,6 +45,7 @@ export function createStoreClient(config) {
44
45
  products: createProductsNamespace(core),
45
46
  categories: createCategoriesNamespace(core),
46
47
  pages: createPagesNamespace(core),
48
+ menus: createMenusNamespace(core),
47
49
  pageData: createPageDataNamespace(core),
48
50
  shipping: createShippingNamespace(core),
49
51
  geo: createGeoNamespace(core),
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Menus Namespace - the store's navigation menus
3
+ *
4
+ * A menu is store data the merchant edits in the dashboard; a theme names it
5
+ * by handle (usually through a `menu` theme setting) and renders what comes
6
+ * back. Every link is resolved by the server: an item's `href` is a path of
7
+ * the store without a language prefix, or the merchant's URL, and items
8
+ * leading nowhere public are already left out. Fetched fresh every request.
9
+ *
10
+ * Failure contract: `get(handle)` → null means "no such menu" or a failed
11
+ * read — the theme renders its own fallback navigation.
12
+ */
13
+ import { type ClientCore } from "./core.js";
14
+ import type { StoreMenu } from "../types.js";
15
+ export interface MenusNamespace {
16
+ /**
17
+ * A menu by handle (`main-menu`), labels in the client's language.
18
+ * Resolves to `null` for an unknown handle or a failed read.
19
+ */
20
+ get(handle: string): Promise<StoreMenu | null>;
21
+ }
22
+ export declare function createMenusNamespace(core: ClientCore): MenusNamespace;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Menus Namespace - the store's navigation menus
3
+ *
4
+ * A menu is store data the merchant edits in the dashboard; a theme names it
5
+ * by handle (usually through a `menu` theme setting) and renders what comes
6
+ * back. Every link is resolved by the server: an item's `href` is a path of
7
+ * the store without a language prefix, or the merchant's URL, and items
8
+ * leading nowhere public are already left out. Fetched fresh every request.
9
+ *
10
+ * Failure contract: `get(handle)` → null means "no such menu" or a failed
11
+ * read — the theme renders its own fallback navigation.
12
+ */
13
+ import { StoreMenuSchema } from "../schemas.js";
14
+ import { STOREFRONT_OPERATIONS } from "../domain.js";
15
+ import { localizedPath } from "./paths.js";
16
+ import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
17
+ export function createMenusNamespace(core) {
18
+ return {
19
+ async get(handle) {
20
+ try {
21
+ return await core.get(localizedPath(core.config, `/store/menus/${encodeURIComponent(handle)}`), StoreMenuSchema, { operation: STOREFRONT_OPERATIONS.menusGet });
22
+ }
23
+ catch (error) {
24
+ if (!isNotFoundAnswer(error))
25
+ reportDegradedRead(core.config, error, "menus.get");
26
+ return null;
27
+ }
28
+ },
29
+ };
30
+ }
package/dist/domain.d.ts CHANGED
@@ -353,6 +353,7 @@ export declare const STOREFRONT_OPERATIONS: {
353
353
  readonly productsReviewsUpload: "products.reviews.upload";
354
354
  readonly pagesGet: "pages.get";
355
355
  readonly pagesLanding: "pages.landing";
356
+ readonly menusGet: "menus.get";
356
357
  readonly shippingRates: "shipping.rates";
357
358
  readonly shippingRate: "shipping.rate";
358
359
  readonly geoCommunes: "geo.communes";
@@ -390,6 +391,7 @@ export declare const STOREFRONT_RETRY_POLICIES: {
390
391
  readonly "pageData.get": "safe-read";
391
392
  readonly "pages.get": "safe-read";
392
393
  readonly "pages.landing": "safe-read";
394
+ readonly "menus.get": "safe-read";
393
395
  readonly "products.get": "safe-read";
394
396
  readonly "products.list": "safe-read";
395
397
  readonly "products.reviews.list": "safe-read";
package/dist/domain.js CHANGED
@@ -206,6 +206,7 @@ export const STOREFRONT_OPERATIONS = {
206
206
  productsReviewsUpload: "products.reviews.upload",
207
207
  pagesGet: "pages.get",
208
208
  pagesLanding: "pages.landing",
209
+ menusGet: "menus.get",
209
210
  shippingRates: "shipping.rates",
210
211
  shippingRate: "shipping.rate",
211
212
  geoCommunes: "geo.communes",
@@ -242,6 +243,7 @@ export const STOREFRONT_RETRY_POLICIES = {
242
243
  [STOREFRONT_OPERATIONS.pageDataGet]: "safe-read",
243
244
  [STOREFRONT_OPERATIONS.pagesGet]: "safe-read",
244
245
  [STOREFRONT_OPERATIONS.pagesLanding]: "safe-read",
246
+ [STOREFRONT_OPERATIONS.menusGet]: "safe-read",
245
247
  [STOREFRONT_OPERATIONS.productsGet]: "safe-read",
246
248
  [STOREFRONT_OPERATIONS.productsList]: "safe-read",
247
249
  [STOREFRONT_OPERATIONS.productsReviewsList]: "safe-read",
package/dist/index.d.ts CHANGED
@@ -17,11 +17,11 @@ export { productImageSource } from "./utils/product-image.js";
17
17
  export type { ProductImageSourceInput } from "./utils/product-image.js";
18
18
  export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
19
19
  export type { PrefillToken, ResolvePrefillParams } from "./whatsapp.js";
20
- export { STOREFRONT_ROUTES, showsWidgetOn } from "./routes.js";
20
+ export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
21
21
  export { servedLanguage } from "./locales.js";
22
22
  export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
23
23
  export type { LocalePath, LocaleRouting } from "./locale-routing.js";
24
- export type { StorefrontRoute } from "./routes.js";
24
+ export type { StorefrontPageMatch, StorefrontPageType, StorefrontPathParams, StorefrontRoute } from "./routes.js";
25
25
  export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
26
26
  export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, } from "./basket.js";
27
27
  export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ export { formatMoney } from "./utils/money.js";
13
13
  export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, } from "./utils/image.js";
14
14
  export { productImageSource } from "./utils/product-image.js";
15
15
  export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
16
- export { STOREFRONT_ROUTES, showsWidgetOn } from "./routes.js";
16
+ export { STOREFRONT_PAGE_PATHS, STOREFRONT_ROUTES, matchStorefrontPath, showsWidgetOn, storefrontPath } from "./routes.js";
17
17
  export { servedLanguage } from "./locales.js";
18
18
  export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
19
19
  export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
package/dist/routes.d.ts CHANGED
@@ -1,20 +1,59 @@
1
1
  /**
2
- * The canonical storefront route contract.
2
+ * The storefront's page types and their public paths.
3
3
  *
4
- * One deployment serves one store, and three parts of the platform meet on the
5
- * same public URLs: the theme renders them, the server writes them into a conversion's
6
- * landing URL (`/lp/:slug` for an attributed sale, `/thank-you` otherwise), and
7
- * the dashboard shows them as the store's public links (`/pages/:slug`). These
8
- * patterns are what all three already assume, so they are written down here: a
9
- * theme may restyle everything else, but these paths are a platform contract,
10
- * not a theme decision.
4
+ * A page type is what a URL *means* — a product, a category, a store page, a
5
+ * landing page — and the path is where it lives. The paths are a platform
6
+ * contract, not a theme decision: the platform builds them without running
7
+ * the theme (the dashboard shows a page's public link, the server names the
8
+ * landing page in a conversion's source URL), so a theme renders each page
9
+ * type it supports at its path and builds every link to one with
10
+ * `storefrontPath` — never a hand-written string. Any other route is the
11
+ * theme's own (`/about`, `/offers/summer` …): Astro's file routing owns those,
12
+ * and nothing here constrains them.
11
13
  *
12
- * Two further paths carry no parameter and no pattern: the root (`/`, the home
13
- * surface) and the catalog index (`/products`, the catalog surface). The
14
- * surface gate below handles them alongside the six frozen patterns.
14
+ * Language prefixes are applied on top: `localizeHref(storefrontPath(…),
15
+ * locale, routing)`.
15
16
  */
16
17
  import type { WidgetSurfaces } from "./types.js";
17
- /** One route in the contract: the pattern and the surface that renders it. */
18
+ /** Each page type's public path; `:handle` / `:slug` mark its one parameter. */
19
+ export declare const STOREFRONT_PAGE_PATHS: {
20
+ readonly home: "/";
21
+ readonly catalog: "/products";
22
+ readonly category: "/category/:slug";
23
+ readonly product: "/products/:handle";
24
+ readonly page: "/pages/:slug";
25
+ readonly landing: "/lp/:slug";
26
+ readonly checkout: "/checkout";
27
+ readonly thankYou: "/thank-you";
28
+ };
29
+ /** What a storefront URL means. */
30
+ export type StorefrontPageType = keyof typeof STOREFRONT_PAGE_PATHS;
31
+ type ParamOf<P extends string> = P extends `${string}:${infer Name}` ? Name : never;
32
+ /** The parameter a page type's path needs: `{ handle }` for a product, `{ slug }` for a category, page or landing page, nothing otherwise. */
33
+ export type StorefrontPathParams<T extends StorefrontPageType> = ParamOf<(typeof STOREFRONT_PAGE_PATHS)[T]> extends never ? Record<string, never> : {
34
+ [K in ParamOf<(typeof STOREFRONT_PAGE_PATHS)[T]>]: string;
35
+ };
36
+ /** A pathname recognised as one of the page types, with its parameter. */
37
+ export interface StorefrontPageMatch {
38
+ readonly type: StorefrontPageType;
39
+ readonly params: Readonly<Record<string, string>>;
40
+ }
41
+ /**
42
+ * The public path of a page type, its parameter encoded:
43
+ * `storefrontPath("product", { handle: "robe" })` → `/products/robe`.
44
+ * Throws when the parameter is missing — a link to nowhere is a bug to see,
45
+ * not a path to render.
46
+ */
47
+ export declare function storefrontPath<T extends StorefrontPageType>(type: T, params?: StorefrontPathParams<T>): string;
48
+ /**
49
+ * Which page type a pathname is, and its parameter (decoded) — or null for
50
+ * any other path. Exact matches only: `/products/` is the catalog,
51
+ * `/productsomething` and `/products/a/b` are nothing. The query, the
52
+ * fragment and a trailing slash are ignored. Pass a pathname without its
53
+ * language prefix (`resolveLocalePath` gives it).
54
+ */
55
+ export declare function matchStorefrontPath(rawPathname: string): StorefrontPageMatch | null;
56
+ /** One route with a parameter or a fixed path of its own, and the widget surface that renders it. */
18
57
  export interface StorefrontRoute {
19
58
  /** Path pattern; `:handle` and `:slug` mark the single path parameter. */
20
59
  readonly path: string;
@@ -22,24 +61,17 @@ export interface StorefrontRoute {
22
61
  readonly surface: keyof WidgetSurfaces;
23
62
  }
24
63
  /**
25
- * The frozen contract. Adding a route is a deliberate platform change: a path
26
- * that renders a surface but is missing here is a surface the merchant cannot
27
- * switch on with any confidence.
64
+ * @deprecated Use `STOREFRONT_PAGE_PATHS` (every page type, the home and the
65
+ * catalog index included) with `storefrontPath` and `matchStorefrontPath`.
66
+ * Kept equal to what it always listed.
28
67
  */
29
68
  export declare const STOREFRONT_ROUTES: readonly StorefrontRoute[];
30
69
  /**
31
- * Whether the widget belongs on a pathname.
32
- *
33
- * The merchant answers "which surfaces" (`surfaces[surface]`); this function
34
- * answers the platform half — which surface a pathname *is* — and it reads the
35
- * contract above:
36
- *
37
- * - `/` → the home surface;
38
- * - `/products` (the catalog index) and `/category/:slug` → the catalog surface;
39
- * - `/products/:handle` → the product surface;
40
- * - `/pages/:slug`, `/lp/:slug`, `/checkout`, `/thank-you` → their own surfaces;
41
- * - anything else — a 404, `/productsomething`, `/checkout-success` — → no
42
- * surface at all, never a prefix match that leaks a widget onto a page the
43
- * merchant switched off.
70
+ * Whether the widget belongs on a pathname: the merchant answers "which
71
+ * surfaces" (`surfaces[surface]`); this answers which surface a pathname *is*,
72
+ * from the page types above. Any other path — a 404, `/productsomething`,
73
+ * `/checkout-success` — is no surface at all, never a prefix match that leaks
74
+ * the widget onto a page the merchant switched off.
44
75
  */
45
76
  export declare function showsWidgetOn(surfaces: WidgetSurfaces | null | undefined, rawPathname: string): boolean;
77
+ export {};
package/dist/routes.js CHANGED
@@ -1,59 +1,88 @@
1
+ /** Each page type's public path; `:handle` / `:slug` mark its one parameter. */
2
+ export const STOREFRONT_PAGE_PATHS = {
3
+ home: "/",
4
+ catalog: "/products",
5
+ category: "/category/:slug",
6
+ product: "/products/:handle",
7
+ page: "/pages/:slug",
8
+ landing: "/lp/:slug",
9
+ checkout: "/checkout",
10
+ thankYou: "/thank-you",
11
+ };
1
12
  /**
2
- * The frozen contract. Adding a route is a deliberate platform change: a path
3
- * that renders a surface but is missing here is a surface the merchant cannot
4
- * switch on with any confidence.
13
+ * The public path of a page type, its parameter encoded:
14
+ * `storefrontPath("product", { handle: "robe" })` → `/products/robe`.
15
+ * Throws when the parameter is missing — a link to nowhere is a bug to see,
16
+ * not a path to render.
5
17
  */
6
- export const STOREFRONT_ROUTES = [
7
- { path: "/products/:handle", surface: "product" },
8
- { path: "/category/:slug", surface: "catalog" },
9
- { path: "/pages/:slug", surface: "pages" },
10
- { path: "/lp/:slug", surface: "landing" },
11
- { path: "/checkout", surface: "checkout" },
12
- { path: "/thank-you", surface: "thankYou" },
13
- ];
18
+ export function storefrontPath(type, params) {
19
+ return STOREFRONT_PAGE_PATHS[type].replace(/:([a-z]+)/g, (_, name) => {
20
+ const value = params?.[name];
21
+ if (!value)
22
+ throw new Error(`storefrontPath("${type}") needs "${name}"`);
23
+ return encodeURIComponent(value);
24
+ });
25
+ }
26
+ const MATCHERS = Object.entries(STOREFRONT_PAGE_PATHS).map(([type, path]) => {
27
+ const names = [];
28
+ const source = path.replace(/:([a-z]+)/g, (_, name) => {
29
+ names.push(name);
30
+ return "([^/]+)";
31
+ });
32
+ return { type, names, pattern: new RegExp(`^${source}$`) };
33
+ });
34
+ /**
35
+ * Which page type a pathname is, and its parameter (decoded) — or null for
36
+ * any other path. Exact matches only: `/products/` is the catalog,
37
+ * `/productsomething` and `/products/a/b` are nothing. The query, the
38
+ * fragment and a trailing slash are ignored. Pass a pathname without its
39
+ * language prefix (`resolveLocalePath` gives it).
40
+ */
41
+ export function matchStorefrontPath(rawPathname) {
42
+ const pathname = rawPathname.split(/[?#]/)[0].replace(/\/+$/, "") || "/";
43
+ for (const { type, names, pattern } of MATCHERS) {
44
+ const match = pattern.exec(pathname);
45
+ if (!match)
46
+ continue;
47
+ try {
48
+ return { type, params: Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(match[i + 1])])) };
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ /** The WhatsApp widget surface each page type belongs to (the home surface is the root). */
57
+ const WIDGET_SURFACE = {
58
+ home: "home",
59
+ catalog: "catalog",
60
+ category: "catalog",
61
+ product: "product",
62
+ page: "pages",
63
+ landing: "landing",
64
+ checkout: "checkout",
65
+ thankYou: "thankYou",
66
+ };
67
+ /**
68
+ * @deprecated Use `STOREFRONT_PAGE_PATHS` (every page type, the home and the
69
+ * catalog index included) with `storefrontPath` and `matchStorefrontPath`.
70
+ * Kept equal to what it always listed.
71
+ */
72
+ export const STOREFRONT_ROUTES = ["product", "category", "page", "landing", "checkout", "thankYou"].map((type) => ({
73
+ path: STOREFRONT_PAGE_PATHS[type],
74
+ surface: WIDGET_SURFACE[type],
75
+ }));
14
76
  /**
15
- * Whether the widget belongs on a pathname.
16
- *
17
- * The merchant answers "which surfaces" (`surfaces[surface]`); this function
18
- * answers the platform half — which surface a pathname *is* — and it reads the
19
- * contract above:
20
- *
21
- * - `/` → the home surface;
22
- * - `/products` (the catalog index) and `/category/:slug` → the catalog surface;
23
- * - `/products/:handle` → the product surface;
24
- * - `/pages/:slug`, `/lp/:slug`, `/checkout`, `/thank-you` → their own surfaces;
25
- * - anything else — a 404, `/productsomething`, `/checkout-success` — → no
26
- * surface at all, never a prefix match that leaks a widget onto a page the
27
- * merchant switched off.
77
+ * Whether the widget belongs on a pathname: the merchant answers "which
78
+ * surfaces" (`surfaces[surface]`); this answers which surface a pathname *is*,
79
+ * from the page types above. Any other path — a 404, `/productsomething`,
80
+ * `/checkout-success` — is no surface at all, never a prefix match that leaks
81
+ * the widget onto a page the merchant switched off.
28
82
  */
29
83
  export function showsWidgetOn(surfaces, rawPathname) {
30
84
  if (!surfaces)
31
85
  return false;
32
- // Normalise: strip query/hash, strip trailing slashes (except root "/")
33
- const pathname = rawPathname.split("?")[0].split("#")[0].replace(/\/+$/, "") || "/";
34
- if (pathname === "/") {
35
- return Boolean(surfaces.home);
36
- }
37
- if (pathname === "/products" || pathname.startsWith("/category/")) {
38
- return Boolean(surfaces.catalog);
39
- }
40
- if (pathname.startsWith("/products/")) {
41
- const slug = pathname.slice("/products/".length);
42
- return slug.length > 0 ? Boolean(surfaces.product) : Boolean(surfaces.catalog);
43
- }
44
- if (pathname === "/checkout") {
45
- return Boolean(surfaces.checkout);
46
- }
47
- if (pathname === "/thank-you") {
48
- return Boolean(surfaces.thankYou);
49
- }
50
- if (pathname.startsWith("/pages/")) {
51
- const slug = pathname.slice("/pages/".length);
52
- return slug.length > 0 ? Boolean(surfaces.pages) : false;
53
- }
54
- if (pathname.startsWith("/lp/")) {
55
- const slug = pathname.slice("/lp/".length);
56
- return slug.length > 0 ? Boolean(surfaces.landing) : false;
57
- }
58
- return false;
86
+ const match = matchStorefrontPath(rawPathname);
87
+ return match ? Boolean(surfaces[WIDGET_SURFACE[match.type]]) : false;
59
88
  }
package/dist/schemas.d.ts CHANGED
@@ -234,6 +234,23 @@ export declare const StorePagePublicSchema: z.ZodObject<{
234
234
  metaTitle: z.ZodNullable<z.ZodString>;
235
235
  metaDescription: z.ZodNullable<z.ZodString>;
236
236
  }, z.core.$strip>;
237
+ /**
238
+ * A navigation menu as a shopper sees it — GET /store/menus/{handle}.
239
+ * `type` is an open string: the server owns which destinations exist, so a
240
+ * new one never fails this parse.
241
+ */
242
+ export declare const StoreMenuItemSchema: z.ZodType<StoreMenuItemShape>;
243
+ interface StoreMenuItemShape {
244
+ label: string;
245
+ href: string;
246
+ type: string;
247
+ children: StoreMenuItemShape[];
248
+ }
249
+ export declare const StoreMenuSchema: z.ZodObject<{
250
+ handle: z.ZodString;
251
+ title: z.ZodString;
252
+ items: z.ZodArray<z.ZodType<StoreMenuItemShape, unknown, z.core.$ZodTypeInternals<StoreMenuItemShape, unknown>>>;
253
+ }, z.core.$strip>;
237
254
  /**
238
255
  * The persisted theme page document (PageData): the
239
256
  * TRANSPORT shape only. `sections` stays `unknown[]` so instances of a newer
@@ -373,6 +390,7 @@ export declare const StoreConfigSchema: z.ZodObject<{
373
390
  }>;
374
391
  }, z.core.$strip>>;
375
392
  contentOverrides: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>;
393
+ themeSettings: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
376
394
  currency: z.ZodString;
377
395
  currencySymbol: z.ZodString;
378
396
  metaTitle: z.ZodNullable<z.ZodString>;
@@ -832,3 +850,4 @@ export declare const ReviewListEnvelopeSchema: z.ZodObject<{
832
850
  }, z.core.$strip>, unknown>>;
833
851
  total: z.ZodNumber;
834
852
  }, z.core.$strip>;
853
+ export {};
package/dist/schemas.js CHANGED
@@ -133,6 +133,22 @@ export const StorePagePublicSchema = z.object({
133
133
  metaTitle: z.string().nullable(),
134
134
  metaDescription: z.string().nullable(),
135
135
  });
136
+ /**
137
+ * A navigation menu as a shopper sees it — GET /store/menus/{handle}.
138
+ * `type` is an open string: the server owns which destinations exist, so a
139
+ * new one never fails this parse.
140
+ */
141
+ export const StoreMenuItemSchema = z.lazy(() => z.object({
142
+ label: z.string(),
143
+ href: z.string(),
144
+ type: z.string(),
145
+ children: z.array(StoreMenuItemSchema),
146
+ }));
147
+ export const StoreMenuSchema = z.object({
148
+ handle: z.string(),
149
+ title: z.string(),
150
+ items: z.array(StoreMenuItemSchema),
151
+ });
136
152
  /**
137
153
  * The persisted theme page document (PageData): the
138
154
  * TRANSPORT shape only. `sections` stays `unknown[]` so instances of a newer
@@ -225,6 +241,11 @@ export const StoreConfigSchema = z.object({
225
241
  locales: z.array(StorefrontLanguageSchema),
226
242
  /** The requesting theme's text edits, one map per published language. */
227
243
  contentOverrides: z.record(z.string(), z.record(z.string(), z.string())),
244
+ /**
245
+ * The store's values for the requesting theme's settings, as saved. Absent
246
+ * from a server older than theme settings.
247
+ */
248
+ themeSettings: z.record(z.string(), z.unknown()).optional(),
228
249
  currency: z.string(),
229
250
  currencySymbol: z.string(),
230
251
  metaTitle: z.string().nullable(),
package/dist/types.d.ts CHANGED
@@ -85,6 +85,15 @@ export interface StoreConfig {
85
85
  * language — never another language's map.
86
86
  */
87
87
  contentOverrides: Record<string, Record<string, string>>;
88
+ /**
89
+ * The merchant's values for this theme's settings, by setting key, exactly
90
+ * as saved — `{}` when there are none or the client was created without
91
+ * `theme`, absent from a server that predates theme settings. Read them
92
+ * against the theme's declaration with `resolveThemeSettings` from
93
+ * `@wocart/theme` (which takes either): a value the theme no longer accepts
94
+ * falls back to its default there.
95
+ */
96
+ themeSettings?: Record<string, unknown> | undefined;
88
97
  currency: string;
89
98
  currencySymbol: string;
90
99
  metaTitle: string | null;
@@ -178,6 +187,25 @@ export interface StorePagePublic {
178
187
  metaTitle: string | null;
179
188
  metaDescription: string | null;
180
189
  }
190
+ /** A navigation menu — GET /store/menus/{handle}. */
191
+ export interface StoreMenu {
192
+ handle: string;
193
+ title: string;
194
+ /** Only items a shopper can follow, in the merchant's order; at most three levels. */
195
+ items: StoreMenuItem[];
196
+ }
197
+ export interface StoreMenuItem {
198
+ /** In the client's language when the store translated it, else the primary. */
199
+ label: string;
200
+ /**
201
+ * A path of the store without a language prefix (`/products/robe`) — pass
202
+ * it through `localizeHref` — or the absolute URL the merchant entered.
203
+ */
204
+ href: string;
205
+ /** The page type it opens (`home`, `catalog`, `category`, `product`, `page`, `landing`), or `url`. */
206
+ type: string;
207
+ children: StoreMenuItem[];
208
+ }
181
209
  export interface ShippingRates {
182
210
  [wilayaId: string]: WilayaRates;
183
211
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/storefront",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",