@wocart/storefront 1.21.0 → 1.23.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 +41 -0
- package/USAGE.md +52 -3
- package/dist/abandoned-checkout.d.ts +2 -3
- package/dist/abandoned-checkout.js +2 -12
- package/dist/actions.d.ts +1 -0
- package/dist/actions.js +3 -0
- package/dist/basket.d.ts +14 -0
- package/dist/basket.js +20 -0
- package/dist/checkout-key.d.ts +25 -0
- package/dist/checkout-key.js +42 -0
- package/dist/client/orders.js +2 -1
- package/dist/client/paths.d.ts +4 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- package/dist/seo.d.ts +21 -0
- package/dist/seo.js +17 -1
- package/dist/types.d.ts +7 -0
- package/dist/utils/uuid.d.ts +6 -0
- package/dist/utils/uuid.js +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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.23.0 — 2026-10-25
|
|
9
|
+
|
|
10
|
+
One checkout, one order: a double click, a refreshed submission or a retry
|
|
11
|
+
after a dropped connection no longer places the order twice. And an order
|
|
12
|
+
records the language it was placed in.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `bindCheckoutKey(form)`: puts a `checkoutKey` on every submission a checkout
|
|
17
|
+
form makes, through its `formdata` event. The key stays the same while the
|
|
18
|
+
shopper changes nothing and is drawn anew once they change any field; it is
|
|
19
|
+
drawn at the first submission, never rendered into a cached page. Returns a
|
|
20
|
+
function that unbinds it.
|
|
21
|
+
- `placeOrderInput` and `CreateOrderInput` take `checkoutKey`, a UUID. The same
|
|
22
|
+
key again answers with the order it already placed, exactly as the first
|
|
23
|
+
time; for another phone it fails with `CHECKOUT_KEY_IN_USE`. Without a key,
|
|
24
|
+
every order is placed as before.
|
|
25
|
+
- `orders.create` sends the client's `locale`: the order records the
|
|
26
|
+
language the shopper checked out in (the primary when the store does not
|
|
27
|
+
publish it), and the customer's messages are written in it.
|
|
28
|
+
|
|
29
|
+
## 1.22.0 — 2026-10-24
|
|
30
|
+
|
|
31
|
+
What moving Theme-00 onto the SDK showed every theme needs: an add reported
|
|
32
|
+
as what it put in, one canonical address per page, and product descriptions
|
|
33
|
+
read the same for legacy rows.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `addedLines(before, after)`: what an add put in the basket — each line that
|
|
38
|
+
grew, by what it grew — for an `AddToCart` event that reports the add, not
|
|
39
|
+
the whole basket; empty when the basket had no room.
|
|
40
|
+
- `canonicalUrl(origin, { pathname, locale, routing }, search)`: a page's
|
|
41
|
+
canonical address — its path in its language, keeping only the listing page
|
|
42
|
+
(`?page=2`), never an order, a search, a coupon or an ad's click id.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- `productMeta` reads an empty `descriptionPlain` as no plain text: a legacy
|
|
47
|
+
text row is then described by its own text, as before plain text existed.
|
|
48
|
+
|
|
8
49
|
## 1.21.0 — 2026-10-24
|
|
9
50
|
|
|
10
51
|
### Added
|
package/USAGE.md
CHANGED
|
@@ -236,6 +236,7 @@ server directly.
|
|
|
236
236
|
| `basketLineKey`, `toOrderItems`, `MAX_BASKET_LINES`, `MAX_BASKET_LINE_QUANTITY` | Basket identity, the wire projection, and the caps the server re-checks |
|
|
237
237
|
| `createOtpMachine` | The OTP step's state machine |
|
|
238
238
|
| `createOtpTransport`, `mapOtpResponse`, `otpTransportFailure` | Route-agnostic OTP result mapping; the theme supplies its same-origin route functions |
|
|
239
|
+
| `bindCheckoutKey` | One checkout attempt, one order: the key a checkout form's submissions carry |
|
|
239
240
|
| `createAbandonedCheckoutTracker` | Port-injected session, capture eligibility, debounce, update, cookie, fetch, and exit-beacon policy |
|
|
240
241
|
| `STOREFRONT_OPERATIONS` | The exact operation labels used by namespace calls |
|
|
241
242
|
| `STOREFRONT_RETRY_POLICIES` | Which operations retry, by exact operation label |
|
|
@@ -260,7 +261,7 @@ not add a local product-view or offer-price module around them.
|
|
|
260
261
|
| --- | --- | --- |
|
|
261
262
|
| `serverUrl` | yes | Your store's API origin. No default: a wrong origin must be a visible mistake, not a silent one. |
|
|
262
263
|
| `apiKey` | yes | The store's API key. Sent as `X-Store-API-Key` on every request. **Server-only.** |
|
|
263
|
-
| `locale` | no | The language to read merchant text in — see "Store languages". |
|
|
264
|
+
| `locale` | no | The language to read merchant text in, and the one an order is recorded in — see "Store languages". |
|
|
264
265
|
| `theme` | no | Your theme's key, to receive the merchant's edits to your theme's text — see "Store languages". |
|
|
265
266
|
| `onError` | no | Observability hook for degraded reads — see "When a read degrades: onError". |
|
|
266
267
|
|
|
@@ -716,12 +717,23 @@ script:
|
|
|
716
717
|
|
|
717
718
|
~~~astro
|
|
718
719
|
---
|
|
719
|
-
import { jsonLd, productMeta, productStructuredData } from "@wocart/storefront";
|
|
720
|
+
import { canonicalUrl, jsonLd, productMeta, productStructuredData } from "@wocart/storefront";
|
|
720
721
|
const meta = productMeta(product);
|
|
722
|
+
const canonical = canonicalUrl(Astro.url.origin, store, Astro.url.search);
|
|
721
723
|
---
|
|
722
724
|
<title>{meta.title} | {store.config.name}</title>
|
|
723
725
|
{meta.description && <meta name="description" content={meta.description} />}
|
|
724
|
-
<script type="application/ld+json" set:html={jsonLd(productStructuredData(product, { url:
|
|
726
|
+
<script type="application/ld+json" set:html={jsonLd(productStructuredData(product, { url: canonical, storeName: store.config.name }))} />
|
|
727
|
+
~~~
|
|
728
|
+
|
|
729
|
+
Every page's `<link rel="canonical">` is `canonicalUrl(origin, { pathname,
|
|
730
|
+
locale, routing }, search)`: the page's path in its language, keeping only the
|
|
731
|
+
listing page it shows (`?page=2`). An order, a search, a coupon or an ad's
|
|
732
|
+
click id (`fbclid`, `utm_*`) in the address is the same page to a search
|
|
733
|
+
engine; a canonical that kept them would split one page into many.
|
|
734
|
+
|
|
735
|
+
~~~ts
|
|
736
|
+
const canonical = canonicalUrl(Astro.url.origin, store, Astro.url.search);
|
|
725
737
|
~~~
|
|
726
738
|
|
|
727
739
|
### Product lists
|
|
@@ -1190,6 +1202,17 @@ basket.addSelections(
|
|
|
1190
1202
|
);
|
|
1191
1203
|
~~~
|
|
1192
1204
|
|
|
1205
|
+
What an add put in the basket is `addedLines(before, after)`: each line that
|
|
1206
|
+
grew, by what it grew. Report that as the `AddToCart` event — never the whole
|
|
1207
|
+
basket — and read an empty answer as a basket with no room left.
|
|
1208
|
+
|
|
1209
|
+
~~~ts
|
|
1210
|
+
const before = basket.getLines();
|
|
1211
|
+
const added = addedLines(before, basket.addSelections(line, selections));
|
|
1212
|
+
if (added.length === 0) showBasketFull();
|
|
1213
|
+
else tracker.track("AddToCart", buildMetaAddToCartPayload(added));
|
|
1214
|
+
~~~
|
|
1215
|
+
|
|
1193
1216
|
### Single-product order versus basket order
|
|
1194
1217
|
|
|
1195
1218
|
An order is either:
|
|
@@ -1236,6 +1259,32 @@ CreatedOrder contains the order ID, customer-facing order number, product
|
|
|
1236
1259
|
price, delivery fee, and total, all in whole DZD — and `couponDiscount`, what
|
|
1237
1260
|
the order's coupon took off.
|
|
1238
1261
|
|
|
1262
|
+
### One checkout, one order
|
|
1263
|
+
|
|
1264
|
+
A shopper who double-clicks, refreshes the thank-you page's POST, or retries
|
|
1265
|
+
after a dropped connection sends the same checkout again. Give the order its
|
|
1266
|
+
`checkoutKey` and the server places it once: the same key again answers with
|
|
1267
|
+
the order it already placed, exactly as the first time — no second order, no
|
|
1268
|
+
second stock deduction, no second notification. For another phone the key
|
|
1269
|
+
fails with `CHECKOUT_KEY_IN_USE`; without a key, every call places an order.
|
|
1270
|
+
|
|
1271
|
+
In the browser, bind the key to the checkout form once:
|
|
1272
|
+
|
|
1273
|
+
~~~ts
|
|
1274
|
+
import { bindCheckoutKey } from "@wocart/storefront";
|
|
1275
|
+
|
|
1276
|
+
bindCheckoutKey(document.querySelector("form.order-form")!);
|
|
1277
|
+
~~~
|
|
1278
|
+
|
|
1279
|
+
It adds `checkoutKey` to every submission the form makes, through the form's
|
|
1280
|
+
`formdata` event, so a native submit and a `new FormData(form)` both carry it.
|
|
1281
|
+
The key stays the same while the shopper changes nothing and is drawn anew
|
|
1282
|
+
once they change any field, so a different order is never answered with the
|
|
1283
|
+
one already placed. It is drawn at the first submission, never rendered into
|
|
1284
|
+
the page: a storefront page is cached and served to every shopper.
|
|
1285
|
+
`placeOrderInput` passes the key through; the function `bindCheckoutKey`
|
|
1286
|
+
returns unbinds it.
|
|
1287
|
+
|
|
1239
1288
|
### Coupons
|
|
1240
1289
|
|
|
1241
1290
|
A coupon is a code the shopper types. It applies after offers, only to the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AbandonedCheckoutInput } from "./domain.js";
|
|
2
|
+
import { type UuidSource } from "./utils/uuid.js";
|
|
2
3
|
export type AbandonedCheckoutSnapshot = Omit<AbandonedCheckoutInput, "sessionId" | "fbc" | "fbp">;
|
|
3
4
|
export interface AbandonedCheckoutStorage {
|
|
4
5
|
getItem(key: string): string | null;
|
|
@@ -29,9 +30,7 @@ export declare function createAbandonedCheckoutTracker(ports: AbandonedCheckoutP
|
|
|
29
30
|
/** What the browser wiring uses of the page; the page's own globals by default. */
|
|
30
31
|
export interface AbandonedCheckoutHost {
|
|
31
32
|
readonly sessionStorage?: Pick<Storage, "getItem" | "setItem" | "removeItem">;
|
|
32
|
-
readonly crypto?:
|
|
33
|
-
randomUUID?: () => string;
|
|
34
|
-
};
|
|
33
|
+
readonly crypto?: UuidSource;
|
|
35
34
|
readonly document?: {
|
|
36
35
|
readonly cookie: string;
|
|
37
36
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toLocalAlgerianMobile } from "./phone.js";
|
|
2
|
+
import { uuidV4 } from "./utils/uuid.js";
|
|
2
3
|
/**
|
|
3
4
|
* Where the browser keeps the checkout's id for the tab (`sessionStorage`):
|
|
4
5
|
* every capture of one checkout updates one record, and the thank-you page
|
|
@@ -110,7 +111,7 @@ export function createBrowserAbandonedCheckoutTracker(options, host = globalThis
|
|
|
110
111
|
getItem: (key) => host.sessionStorage?.getItem(key) ?? null,
|
|
111
112
|
setItem: (key, value) => host.sessionStorage?.setItem(key, value),
|
|
112
113
|
},
|
|
113
|
-
createId: () =>
|
|
114
|
+
createId: () => uuidV4(host.crypto ?? globalThis.crypto),
|
|
114
115
|
schedule: (callback, delayMs) => host.setTimeout(callback, delayMs),
|
|
115
116
|
cancel: (handle) => host.clearTimeout(handle),
|
|
116
117
|
readCookie: (name) => cookieValue(host.document?.cookie ?? "", name),
|
|
@@ -154,14 +155,3 @@ function cookieValue(cookies, name) {
|
|
|
154
155
|
}
|
|
155
156
|
return undefined;
|
|
156
157
|
}
|
|
157
|
-
/** A version 4 UUID: the platform's `randomUUID`, else one drawn from `getRandomValues`. */
|
|
158
|
-
function uuid(host) {
|
|
159
|
-
const crypto = host.crypto ?? globalThis.crypto;
|
|
160
|
-
if (typeof crypto.randomUUID === "function")
|
|
161
|
-
return crypto.randomUUID();
|
|
162
|
-
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
163
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
164
|
-
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
165
|
-
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
166
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
167
|
-
}
|
package/dist/actions.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export declare const placeOrderInput: z.ZodObject<{
|
|
|
86
86
|
}, z.core.$strip>>>, unknown>;
|
|
87
87
|
landingPageSlug: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
88
88
|
couponCode: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
89
|
+
checkoutKey: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
89
90
|
}, z.core.$strip>;
|
|
90
91
|
export type PlaceOrderInput = z.infer<typeof placeOrderInput>;
|
|
91
92
|
/**
|
package/dist/actions.js
CHANGED
|
@@ -153,6 +153,9 @@ export const placeOrderInput = z.object({
|
|
|
153
153
|
// The code the shopper typed; an empty box sends none. Whether it applies
|
|
154
154
|
// is the server's to say — a refusal comes back as `couponRefusal`.
|
|
155
155
|
couponCode: z.preprocess((v) => (typeof v === "string" && v.trim() === "") || v == null ? undefined : v, z.string().max(64).optional()),
|
|
156
|
+
// The checkout attempt's key (`bindCheckoutKey`): the same key again is
|
|
157
|
+
// answered with the order it placed, never a second one.
|
|
158
|
+
checkoutKey: z.preprocess((v) => (v === "" || v == null ? undefined : v), z.string().uuid().optional()),
|
|
156
159
|
});
|
|
157
160
|
/**
|
|
158
161
|
* Collect the shopper's attribution headers so the server records the
|
package/dist/basket.d.ts
CHANGED
|
@@ -95,3 +95,17 @@ export declare function toOrderItems(lines: readonly BasketLine[]): BasketLineIn
|
|
|
95
95
|
* Create a deep Basket Store.
|
|
96
96
|
*/
|
|
97
97
|
export declare function createBasketStore(options?: BasketStoreOptions): BasketStore;
|
|
98
|
+
/**
|
|
99
|
+
* What an add put in the basket: each line of `after` whose quantity grew
|
|
100
|
+
* since `before`, with the quantity it grew by — what an `AddToCart` event
|
|
101
|
+
* reports, never the whole basket. Empty when nothing went in, as when the
|
|
102
|
+
* basket was full.
|
|
103
|
+
*
|
|
104
|
+
* ```ts
|
|
105
|
+
* const before = basket.getLines();
|
|
106
|
+
* const added = addedLines(before, basket.addSelections(line, selections));
|
|
107
|
+
* if (added.length === 0) showBasketFull();
|
|
108
|
+
* else tracker.track("AddToCart", buildMetaAddToCartPayload(added));
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare function addedLines(before: readonly BasketLine[], after: readonly BasketLine[]): BasketLine[];
|
package/dist/basket.js
CHANGED
|
@@ -277,3 +277,23 @@ export function createBasketStore(options = {}) {
|
|
|
277
277
|
},
|
|
278
278
|
};
|
|
279
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* What an add put in the basket: each line of `after` whose quantity grew
|
|
282
|
+
* since `before`, with the quantity it grew by — what an `AddToCart` event
|
|
283
|
+
* reports, never the whole basket. Empty when nothing went in, as when the
|
|
284
|
+
* basket was full.
|
|
285
|
+
*
|
|
286
|
+
* ```ts
|
|
287
|
+
* const before = basket.getLines();
|
|
288
|
+
* const added = addedLines(before, basket.addSelections(line, selections));
|
|
289
|
+
* if (added.length === 0) showBasketFull();
|
|
290
|
+
* else tracker.track("AddToCart", buildMetaAddToCartPayload(added));
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export function addedLines(before, after) {
|
|
294
|
+
const had = new Map(before.map((line) => [basketLineKey(line.productId, line.variantId), line.quantity]));
|
|
295
|
+
return after.flatMap((line) => {
|
|
296
|
+
const grew = line.quantity - (had.get(basketLineKey(line.productId, line.variantId)) ?? 0);
|
|
297
|
+
return grew > 0 ? [{ ...line, quantity: grew }] : [];
|
|
298
|
+
});
|
|
299
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One checkout attempt, one order. The server places the order a checkout key
|
|
3
|
+
* arrives with once: the same key again answers with that order, never a
|
|
4
|
+
* second one, a second stock deduction or a second notification.
|
|
5
|
+
*
|
|
6
|
+
* `bindCheckoutKey` puts a key on every submission a checkout form makes. It
|
|
7
|
+
* stays the same while the shopper changes nothing — a double click, a
|
|
8
|
+
* refreshed or retried submission carries it — and is drawn anew once they
|
|
9
|
+
* change anything, so a different order is never answered with the one
|
|
10
|
+
* already placed.
|
|
11
|
+
*
|
|
12
|
+
* The key is drawn in the browser at the first submission, never rendered: a
|
|
13
|
+
* storefront page is cached and served to every shopper.
|
|
14
|
+
*/
|
|
15
|
+
import { type UuidSource } from "./utils/uuid.js";
|
|
16
|
+
/** What the binding uses of the form: its events. */
|
|
17
|
+
export type CheckoutKeyForm = Pick<EventTarget, "addEventListener" | "removeEventListener">;
|
|
18
|
+
/**
|
|
19
|
+
* Bind a checkout key to the form; the function returned unbinds it. The key
|
|
20
|
+
* is added through the form's `formdata` event, so it reaches a native submit
|
|
21
|
+
* and a `new FormData(form)` alike, whatever order the theme's own submit
|
|
22
|
+
* handlers run in. A browser without that event sends no key and places its
|
|
23
|
+
* orders as before.
|
|
24
|
+
*/
|
|
25
|
+
export declare function bindCheckoutKey(form: CheckoutKeyForm, crypto?: UuidSource): () => void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One checkout attempt, one order. The server places the order a checkout key
|
|
3
|
+
* arrives with once: the same key again answers with that order, never a
|
|
4
|
+
* second one, a second stock deduction or a second notification.
|
|
5
|
+
*
|
|
6
|
+
* `bindCheckoutKey` puts a key on every submission a checkout form makes. It
|
|
7
|
+
* stays the same while the shopper changes nothing — a double click, a
|
|
8
|
+
* refreshed or retried submission carries it — and is drawn anew once they
|
|
9
|
+
* change anything, so a different order is never answered with the one
|
|
10
|
+
* already placed.
|
|
11
|
+
*
|
|
12
|
+
* The key is drawn in the browser at the first submission, never rendered: a
|
|
13
|
+
* storefront page is cached and served to every shopper.
|
|
14
|
+
*/
|
|
15
|
+
import { uuidV4 } from "./utils/uuid.js";
|
|
16
|
+
const FIELD = "checkoutKey";
|
|
17
|
+
const CHANGES = ["input", "change", "reset"];
|
|
18
|
+
/**
|
|
19
|
+
* Bind a checkout key to the form; the function returned unbinds it. The key
|
|
20
|
+
* is added through the form's `formdata` event, so it reaches a native submit
|
|
21
|
+
* and a `new FormData(form)` alike, whatever order the theme's own submit
|
|
22
|
+
* handlers run in. A browser without that event sends no key and places its
|
|
23
|
+
* orders as before.
|
|
24
|
+
*/
|
|
25
|
+
export function bindCheckoutKey(form, crypto = globalThis.crypto) {
|
|
26
|
+
let key = null;
|
|
27
|
+
const stamp = (event) => {
|
|
28
|
+
key ??= uuidV4(crypto);
|
|
29
|
+
event.formData.set(FIELD, key);
|
|
30
|
+
};
|
|
31
|
+
const forget = () => {
|
|
32
|
+
key = null;
|
|
33
|
+
};
|
|
34
|
+
form.addEventListener("formdata", stamp);
|
|
35
|
+
for (const type of CHANGES)
|
|
36
|
+
form.addEventListener(type, forget);
|
|
37
|
+
return () => {
|
|
38
|
+
form.removeEventListener("formdata", stamp);
|
|
39
|
+
for (const type of CHANGES)
|
|
40
|
+
form.removeEventListener(type, forget);
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/client/orders.js
CHANGED
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
import { CreatedOrderSchema, IdResponseSchema, OrderTrackingSchema, OtpSendResultSchema, ShopperOrderStatusSchema, OtpVerifyResultSchema, } from "../schemas.js";
|
|
15
15
|
import { STOREFRONT_OPERATIONS } from "../operations.js";
|
|
16
16
|
import { isNotFoundAnswer, reportDegradedRead } from "./core.js";
|
|
17
|
+
import { localizedPath } from "./paths.js";
|
|
17
18
|
/**
|
|
18
19
|
* Create orders namespace
|
|
19
20
|
*/
|
|
20
21
|
export function createOrdersNamespace(core) {
|
|
21
22
|
return {
|
|
22
23
|
async create(body, forwardedHeaders) {
|
|
23
|
-
return core.post("/store/orders", body, {
|
|
24
|
+
return core.post(localizedPath(core.config, "/store/orders"), body, {
|
|
24
25
|
...(forwardedHeaders ? { headers: forwardedHeaders } : {}),
|
|
25
26
|
schema: CreatedOrderSchema,
|
|
26
27
|
operation: STOREFRONT_OPERATIONS.ordersCreate,
|
package/dist/client/paths.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Request paths
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* language.
|
|
2
|
+
* Request paths that name the client's language (`StoreScope.locale`): reads
|
|
3
|
+
* that carry merchant text, so the server serves them with their
|
|
4
|
+
* translations (caches keyed by path keep one entry per language), and an
|
|
5
|
+
* order, which records the language it was placed in.
|
|
6
6
|
*/
|
|
7
7
|
import type { StoreScope } from "../domain.js";
|
|
8
8
|
export declare function localizedPath(scope: Pick<StoreScope, "locale">, path: string, params?: Readonly<Record<string, string | undefined>>): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -22,9 +22,11 @@ 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
24
|
export type { StorefrontPageMatch, StorefrontPageType, StorefrontPathParams, StorefrontRoute } from "./routes.js";
|
|
25
|
-
export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
25
|
+
export { addedLines, createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
26
26
|
export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
|
|
27
27
|
export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, BasketSelectionLine, } from "./basket.js";
|
|
28
|
+
export { bindCheckoutKey } from "./checkout-key.js";
|
|
29
|
+
export type { CheckoutKeyForm } from "./checkout-key.js";
|
|
28
30
|
export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
|
|
29
31
|
export type { OtpMachine, OtpMachineOptions, OtpFailureContext, OtpResponseOptions, OtpState, OtpStatus, OtpTransport, OtpTransportResult, OtpStateListener, } from "./otp.js";
|
|
30
32
|
export { WILAYAS } from "./data/wilayas.js";
|
|
@@ -35,8 +37,8 @@ export { createQuoter } from "./quote.js";
|
|
|
35
37
|
export type { Quoter, QuoterOptions, QuoteResult } from "./quote.js";
|
|
36
38
|
export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
|
|
37
39
|
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
38
|
-
export { SEO_TEXT_LIMITS, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
39
|
-
export type { PageMeta, ProductStructuredDataOptions } from "./seo.js";
|
|
40
|
+
export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
41
|
+
export type { CanonicalPage, PageMeta, ProductStructuredDataOptions } from "./seo.js";
|
|
40
42
|
export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
|
|
41
43
|
export type { ReviewRefusalCode } from "./reviews.js";
|
|
42
44
|
export type { CommuneTuple } from "./data/communes.js";
|
package/dist/index.js
CHANGED
|
@@ -16,8 +16,9 @@ export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, trunc
|
|
|
16
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
|
-
export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
19
|
+
export { addedLines, createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
20
20
|
export { MAX_BASKET_LINES, MAX_BASKET_LINE_QUANTITY, basketLineKey } from "./basket-line.js";
|
|
21
|
+
export { bindCheckoutKey } from "./checkout-key.js";
|
|
21
22
|
export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
|
|
22
23
|
export { WILAYAS } from "./data/wilayas.js";
|
|
23
24
|
export { ALGERIA_COMMUNES, communesForWilaya } from "./data/communes.js";
|
|
@@ -26,7 +27,7 @@ export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-re
|
|
|
26
27
|
export { createQuoter } from "./quote.js";
|
|
27
28
|
export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
|
|
28
29
|
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
29
|
-
export { SEO_TEXT_LIMITS, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
30
|
+
export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
30
31
|
export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
|
|
31
32
|
export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
|
|
32
33
|
export * from "./types.js";
|
package/dist/seo.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a product page tells search engines and shared links: its title and
|
|
3
|
+
* description, and its structured data — decided once, so every theme ranks
|
|
4
|
+
* the same products the same way.
|
|
5
|
+
*/
|
|
6
|
+
import { type LocaleRouting } from "./locale-routing.js";
|
|
1
7
|
import type { Product, ProductList } from "./types.js";
|
|
2
8
|
/** The lengths a search-engine title and description may have: about what a search result shows. */
|
|
3
9
|
export declare const SEO_TEXT_LIMITS: {
|
|
@@ -44,4 +50,19 @@ export declare function productStructuredData(product: Product, options: Product
|
|
|
44
50
|
* ```
|
|
45
51
|
*/
|
|
46
52
|
export declare function jsonLd(value: unknown): string;
|
|
53
|
+
/** Where a page is, as its canonical address needs it: the route it renders, in its language. */
|
|
54
|
+
export interface CanonicalPage {
|
|
55
|
+
/** The page's path without its language prefix (`/products/robe`). */
|
|
56
|
+
pathname: string;
|
|
57
|
+
locale: string;
|
|
58
|
+
routing: LocaleRouting;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A page's canonical address: its path in its language on `origin`, keeping
|
|
62
|
+
* only the listing page it shows (`?page=2`). An order (`?sort=`), a search,
|
|
63
|
+
* an ad's click id (`fbclid`, `utm_*`) or a coupon in the address is the same
|
|
64
|
+
* page to a search engine, and a canonical that kept them would split one
|
|
65
|
+
* page into many.
|
|
66
|
+
*/
|
|
67
|
+
export declare function canonicalUrl(origin: string, page: CanonicalPage, search?: string | URLSearchParams): string;
|
|
47
68
|
export {};
|
package/dist/seo.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* description, and its structured data — decided once, so every theme ranks
|
|
4
4
|
* the same products the same way.
|
|
5
5
|
*/
|
|
6
|
+
import { localizeHref } from "./locale-routing.js";
|
|
6
7
|
import { getProductView } from "./product-view.js";
|
|
7
8
|
import { initialVariant } from "./variant-picker.js";
|
|
8
9
|
/** The lengths a search-engine title and description may have: about what a search result shows. */
|
|
@@ -27,7 +28,8 @@ export function productMeta(product) {
|
|
|
27
28
|
const written = collapse(product.metaDescription ?? "");
|
|
28
29
|
if (written)
|
|
29
30
|
return { title, description: written };
|
|
30
|
-
|
|
31
|
+
// An empty plain text is no plain text: a text row is then read as it is.
|
|
32
|
+
const plain = product.descriptionPlain || (product.descriptionFormat === "text" ? product.description : null);
|
|
31
33
|
const text = collapse(plain ?? "");
|
|
32
34
|
return { title, description: text ? shorten(text, SEO_TEXT_LIMITS.descriptionMax) : null };
|
|
33
35
|
}
|
|
@@ -85,3 +87,17 @@ export function jsonLd(value) {
|
|
|
85
87
|
.replace(/\u2028/g, "\\u2028")
|
|
86
88
|
.replace(/\u2029/g, "\\u2029");
|
|
87
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* A page's canonical address: its path in its language on `origin`, keeping
|
|
92
|
+
* only the listing page it shows (`?page=2`). An order (`?sort=`), a search,
|
|
93
|
+
* an ad's click id (`fbclid`, `utm_*`) or a coupon in the address is the same
|
|
94
|
+
* page to a search engine, and a canonical that kept them would split one
|
|
95
|
+
* page into many.
|
|
96
|
+
*/
|
|
97
|
+
export function canonicalUrl(origin, page, search = "") {
|
|
98
|
+
const url = new URL(localizeHref(page.pathname, page.locale, page.routing), origin);
|
|
99
|
+
const asked = Number(new URLSearchParams(search).get("page"));
|
|
100
|
+
if (Number.isSafeInteger(asked) && asked > 1)
|
|
101
|
+
url.searchParams.set("page", String(asked));
|
|
102
|
+
return url.href;
|
|
103
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -475,6 +475,13 @@ export interface CreateOrderInput {
|
|
|
475
475
|
landingPageSlug?: string | undefined;
|
|
476
476
|
/** The code the shopper typed. An order whose code does not apply is refused whole (`couponRefusalOf`). */
|
|
477
477
|
couponCode?: string | undefined;
|
|
478
|
+
/**
|
|
479
|
+
* The checkout attempt's key, a UUID (`bindCheckoutKey`). The same key again
|
|
480
|
+
* answers with the order it placed, exactly as the first time; for another
|
|
481
|
+
* phone it fails with `CHECKOUT_KEY_IN_USE`. Without it, every call places
|
|
482
|
+
* an order.
|
|
483
|
+
*/
|
|
484
|
+
checkoutKey?: string | undefined;
|
|
478
485
|
}
|
|
479
486
|
/**
|
|
480
487
|
* The order's tracking destination — which Meta pixel it belongs to and
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** What a UUID is drawn from: the platform's `crypto`, whose `randomUUID` exists only in a secure context. */
|
|
2
|
+
export type UuidSource = Pick<Crypto, "getRandomValues"> & {
|
|
3
|
+
randomUUID?: () => string;
|
|
4
|
+
};
|
|
5
|
+
/** A version 4 UUID: the platform's `randomUUID`, else one drawn from `getRandomValues`. */
|
|
6
|
+
export declare function uuidV4(crypto: UuidSource): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A version 4 UUID: the platform's `randomUUID`, else one drawn from `getRandomValues`. */
|
|
2
|
+
export function uuidV4(crypto) {
|
|
3
|
+
if (typeof crypto.randomUUID === "function")
|
|
4
|
+
return crypto.randomUUID();
|
|
5
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
6
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
7
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
8
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
9
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
10
|
+
}
|
package/package.json
CHANGED