@wocart/storefront 1.20.0 → 1.22.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 +46 -0
- package/USAGE.md +56 -3
- package/dist/astro.d.ts +2 -0
- package/dist/astro.js +1 -0
- package/dist/basket.d.ts +14 -0
- package/dist/basket.js +20 -0
- package/dist/catalog.d.ts +6 -0
- package/dist/catalog.js +6 -0
- package/dist/endpoints/storefront-routes.js +2 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/order-lookup.d.ts +51 -0
- package/dist/order-lookup.js +44 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.js +3 -0
- package/dist/seo.d.ts +21 -0
- package/dist/seo.js +17 -1
- package/dist/store-request.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ 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.22.0 — 2026-10-24
|
|
9
|
+
|
|
10
|
+
What moving Theme-00 onto the SDK showed every theme needs: an add reported
|
|
11
|
+
as what it put in, one canonical address per page, and product descriptions
|
|
12
|
+
read the same for legacy rows.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `addedLines(before, after)`: what an add put in the basket — each line that
|
|
17
|
+
grew, by what it grew — for an `AddToCart` event that reports the add, not
|
|
18
|
+
the whole basket; empty when the basket had no room.
|
|
19
|
+
- `canonicalUrl(origin, { pathname, locale, routing }, search)`: a page's
|
|
20
|
+
canonical address — its path in its language, keeping only the listing page
|
|
21
|
+
(`?page=2`), never an order, a search, a coupon or an ad's click id.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `productMeta` reads an empty `descriptionPlain` as no plain text: a legacy
|
|
26
|
+
text row is then described by its own text, as before plain text existed.
|
|
27
|
+
|
|
28
|
+
## 1.21.0 — 2026-10-24
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **A shopper's track-order page, with no script.** `lookupOrder(input,
|
|
33
|
+
request, client)` and `orderLookupInput` in `@wocart/storefront/astro`, for
|
|
34
|
+
an Astro Action a plain form posts: it answers `found` (the order),
|
|
35
|
+
`invalid-number` (not an order number — the server is not asked),
|
|
36
|
+
`invalid-phone`, `not-found` (a wrong number or phone, never saying which),
|
|
37
|
+
`rate-limited` (with the wait) or `failed`, and forwards the shopper's
|
|
38
|
+
address so the server's limit counts per shopper. The `orderStatus` route
|
|
39
|
+
takes the same input.
|
|
40
|
+
- **The track-order page is a platform page.** `STOREFRONT_PAGE_PATHS.trackOrder`
|
|
41
|
+
is `/track-order` — `?order=<number>` fills its number in — so the platform
|
|
42
|
+
can link a shopper to it like the thank-you page, and every theme renders it
|
|
43
|
+
at the same place. `showsWidgetOn` treats it as a store page.
|
|
44
|
+
- `SHOPPER_ORDER_JOURNEY`: the stages from `placed` to `delivered`, in order,
|
|
45
|
+
for a tracking page's progress; `returned` and `cancelled` end an order off
|
|
46
|
+
it.
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- `notFound(Astro)` type-checks against Astro's own global and endpoint
|
|
51
|
+
context: `NotFoundContext.response.status` is optional, as Astro's
|
|
52
|
+
`ResponseInit` declares it. Nothing changes at runtime.
|
|
53
|
+
|
|
8
54
|
## 1.20.0 — 2026-10-24
|
|
9
55
|
|
|
10
56
|
What every theme used to write itself is now the package's: the order form's
|
package/USAGE.md
CHANGED
|
@@ -111,6 +111,7 @@ break those links.
|
|
|
111
111
|
| `landing` | `/lp/:slug` | a landing page | required when the store runs landing pages |
|
|
112
112
|
| `checkout` | `/checkout` | the cart's checkout | required when the store turns the cart on |
|
|
113
113
|
| `thankYou` | `/thank-you` | the order confirmation | required |
|
|
114
|
+
| `trackOrder` | `/track-order` | where a shopper follows an order by its number and phone; `?order=<number>` fills the number in | required |
|
|
114
115
|
|
|
115
116
|
How a theme implements them, in Astro's own terms:
|
|
116
117
|
|
|
@@ -715,12 +716,23 @@ script:
|
|
|
715
716
|
|
|
716
717
|
~~~astro
|
|
717
718
|
---
|
|
718
|
-
import { jsonLd, productMeta, productStructuredData } from "@wocart/storefront";
|
|
719
|
+
import { canonicalUrl, jsonLd, productMeta, productStructuredData } from "@wocart/storefront";
|
|
719
720
|
const meta = productMeta(product);
|
|
721
|
+
const canonical = canonicalUrl(Astro.url.origin, store, Astro.url.search);
|
|
720
722
|
---
|
|
721
723
|
<title>{meta.title} | {store.config.name}</title>
|
|
722
724
|
{meta.description && <meta name="description" content={meta.description} />}
|
|
723
|
-
<script type="application/ld+json" set:html={jsonLd(productStructuredData(product, { url:
|
|
725
|
+
<script type="application/ld+json" set:html={jsonLd(productStructuredData(product, { url: canonical, storeName: store.config.name }))} />
|
|
726
|
+
~~~
|
|
727
|
+
|
|
728
|
+
Every page's `<link rel="canonical">` is `canonicalUrl(origin, { pathname,
|
|
729
|
+
locale, routing }, search)`: the page's path in its language, keeping only the
|
|
730
|
+
listing page it shows (`?page=2`). An order, a search, a coupon or an ad's
|
|
731
|
+
click id (`fbclid`, `utm_*`) in the address is the same page to a search
|
|
732
|
+
engine; a canonical that kept them would split one page into many.
|
|
733
|
+
|
|
734
|
+
~~~ts
|
|
735
|
+
const canonical = canonicalUrl(Astro.url.origin, store, Astro.url.search);
|
|
724
736
|
~~~
|
|
725
737
|
|
|
726
738
|
### Product lists
|
|
@@ -1189,6 +1201,17 @@ basket.addSelections(
|
|
|
1189
1201
|
);
|
|
1190
1202
|
~~~
|
|
1191
1203
|
|
|
1204
|
+
What an add put in the basket is `addedLines(before, after)`: each line that
|
|
1205
|
+
grew, by what it grew. Report that as the `AddToCart` event — never the whole
|
|
1206
|
+
basket — and read an empty answer as a basket with no room left.
|
|
1207
|
+
|
|
1208
|
+
~~~ts
|
|
1209
|
+
const before = basket.getLines();
|
|
1210
|
+
const added = addedLines(before, basket.addSelections(line, selections));
|
|
1211
|
+
if (added.length === 0) showBasketFull();
|
|
1212
|
+
else tracker.track("AddToCart", buildMetaAddToCartPayload(added));
|
|
1213
|
+
~~~
|
|
1214
|
+
|
|
1192
1215
|
### Single-product order versus basket order
|
|
1193
1216
|
|
|
1194
1217
|
An order is either:
|
|
@@ -1410,7 +1433,9 @@ number and the phone it was placed with prove it is theirs, as for a review.
|
|
|
1410
1433
|
const found = await store.orders.status({ orderNumber, phone }, forwardedHeaders);
|
|
1411
1434
|
if (found.success) {
|
|
1412
1435
|
// found.data.stage: placed, confirmed, preparing, shipped, out_for_delivery,
|
|
1413
|
-
// delivered, returned (refused at the door) or cancelled — SHOPPER_ORDER_STAGES
|
|
1436
|
+
// delivered, returned (refused at the door) or cancelled — SHOPPER_ORDER_STAGES;
|
|
1437
|
+
// a progress bar shows SHOPPER_ORDER_JOURNEY, placed to delivered, which
|
|
1438
|
+
// returned and cancelled leave
|
|
1414
1439
|
// found.data.lines, found.data.total, found.data.placedAt
|
|
1415
1440
|
// found.data.tracking: { number, carrier } once the carrier has the parcel
|
|
1416
1441
|
}
|
|
@@ -1419,6 +1444,34 @@ if (found.success) {
|
|
|
1419
1444
|
~~~
|
|
1420
1445
|
|
|
1421
1446
|
It never answers the address, the notes or the merchant's own statuses.
|
|
1447
|
+
|
|
1448
|
+
A theme's track-order page needs no script: an Astro Action posted by a plain
|
|
1449
|
+
form, with the outcome already worded in the few cases a page shows.
|
|
1450
|
+
|
|
1451
|
+
~~~ts
|
|
1452
|
+
// src/actions/index.ts
|
|
1453
|
+
import { defineAction } from "astro:actions";
|
|
1454
|
+
import { lookupOrder, orderLookupInput } from "@wocart/storefront/astro";
|
|
1455
|
+
|
|
1456
|
+
export const server = {
|
|
1457
|
+
orderStatus: defineAction({
|
|
1458
|
+
accept: "form",
|
|
1459
|
+
input: orderLookupInput,
|
|
1460
|
+
handler: (input, context) => lookupOrder(input, context.request, context.locals.store.client),
|
|
1461
|
+
}),
|
|
1462
|
+
};
|
|
1463
|
+
~~~
|
|
1464
|
+
|
|
1465
|
+
~~~astro
|
|
1466
|
+
---
|
|
1467
|
+
// src/pages/track.astro — the page renders from the same POST
|
|
1468
|
+
const lookup = Astro.getActionResult(actions.orderStatus)?.data;
|
|
1469
|
+
// lookup?.status: "found" (lookup.order), "invalid-number" (the server is not
|
|
1470
|
+
// asked), "invalid-phone", "not-found", "rate-limited" (retryAfterSeconds) or "failed"
|
|
1471
|
+
---
|
|
1472
|
+
<form method="POST" action={actions.orderStatus}>…</form>
|
|
1473
|
+
~~~
|
|
1474
|
+
|
|
1422
1475
|
From the browser, use the storefront routes' `orderStatus`.
|
|
1423
1476
|
|
|
1424
1477
|
### Meta browser primitives
|
package/dist/astro.d.ts
CHANGED
|
@@ -6,5 +6,7 @@ export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./
|
|
|
6
6
|
export type { PlaceOrderInput, PlaceOrderResult } from "./actions.js";
|
|
7
7
|
export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
|
|
8
8
|
export type { PrgDecision, ActionMiddlewareDeps, ActionDescriptor } from "./middleware.js";
|
|
9
|
+
export { lookupOrder, orderLookupInput } from "./order-lookup.js";
|
|
10
|
+
export type { OrderLookup, OrderLookupInput } from "./order-lookup.js";
|
|
9
11
|
export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
|
|
10
12
|
export type { NotFoundContext, ResolvedStore, StoreRequestOptions, StoreRequestResolution } from "./store-request.js";
|
package/dist/astro.js
CHANGED
|
@@ -2,4 +2,5 @@ export { createProxyEndpoint } from "./endpoints/proxy.js";
|
|
|
2
2
|
export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
|
|
3
3
|
export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
|
|
4
4
|
export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
|
|
5
|
+
export { lookupOrder, orderLookupInput } from "./order-lookup.js";
|
|
5
6
|
export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
|
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
|
+
}
|
package/dist/catalog.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export declare const PRODUCT_SORTS: readonly ["newest", "price-ascending", "pric
|
|
|
10
10
|
* words.
|
|
11
11
|
*/
|
|
12
12
|
export declare const SHOPPER_ORDER_STAGES: readonly ["placed", "confirmed", "preparing", "shipped", "out_for_delivery", "delivered", "returned", "cancelled"];
|
|
13
|
+
/**
|
|
14
|
+
* The steps an order passes on its way to the shopper, in order — what a
|
|
15
|
+
* tracking page's progress shows. `returned` and `cancelled` end an order off
|
|
16
|
+
* this path, and a stage a newer server adds is on none of it.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SHOPPER_ORDER_JOURNEY: readonly ["placed", "confirmed", "preparing", "shipped", "out_for_delivery", "delivered"];
|
|
13
19
|
/**
|
|
14
20
|
* The collections every store starts with, the likeliest to hold products
|
|
15
21
|
* first: what to show when a page needs a collection and none was named (an
|
package/dist/catalog.js
CHANGED
|
@@ -19,6 +19,12 @@ export const SHOPPER_ORDER_STAGES = [
|
|
|
19
19
|
"returned",
|
|
20
20
|
"cancelled",
|
|
21
21
|
];
|
|
22
|
+
/**
|
|
23
|
+
* The steps an order passes on its way to the shopper, in order — what a
|
|
24
|
+
* tracking page's progress shows. `returned` and `cancelled` end an order off
|
|
25
|
+
* this path, and a stage a newer server adds is on none of it.
|
|
26
|
+
*/
|
|
27
|
+
export const SHOPPER_ORDER_JOURNEY = ["placed", "confirmed", "preparing", "shipped", "out_for_delivery", "delivered"];
|
|
22
28
|
/**
|
|
23
29
|
* The collections every store starts with, the likeliest to hold products
|
|
24
30
|
* first: what to show when a page needs a collection and none was named (an
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { z } from "zod";
|
|
25
25
|
import { AbandonedCheckoutInputSchema, BasketLineInputSchema, DeliveryDestinationSchema, ProductQuoteInputSchema, WilayaIdSchema, } from "../domain.js";
|
|
26
26
|
import { MAX_BASKET_LINES } from "../basket-line.js";
|
|
27
|
+
import { orderLookupInput } from "../order-lookup.js";
|
|
27
28
|
import { createProxyEndpoint, } from "./proxy.js";
|
|
28
29
|
export const REVIEWS_PAGE_DEFAULT = 20;
|
|
29
30
|
export const REVIEWS_PAGE_MAX = 50;
|
|
@@ -82,10 +83,6 @@ const reviewSubmitInput = z.object({
|
|
|
82
83
|
grant: z.string().min(1).max(2048).optional(),
|
|
83
84
|
uploads: z.array(z.string().min(1).max(64)).max(8).optional(),
|
|
84
85
|
});
|
|
85
|
-
const orderStatusInput = z.object({
|
|
86
|
-
orderNumber: z.string().min(1).max(64),
|
|
87
|
-
phone: z.string().min(1).max(32),
|
|
88
|
-
});
|
|
89
86
|
const reviewEligibilityInput = z.object({
|
|
90
87
|
orderNumber: z.string().min(1).max(64),
|
|
91
88
|
phone: z.string().min(1).max(32),
|
|
@@ -197,7 +194,7 @@ export function createStorefrontRoutes(options) {
|
|
|
197
194
|
}),
|
|
198
195
|
orderStatus: route({
|
|
199
196
|
method: "POST",
|
|
200
|
-
inputSchema:
|
|
197
|
+
inputSchema: orderLookupInput,
|
|
201
198
|
forwardAttribution: true,
|
|
202
199
|
call: (client, proof, headers) => client.orders.status(proof, headers),
|
|
203
200
|
}),
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ 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
28
|
export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
|
|
@@ -34,9 +34,9 @@ export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-re
|
|
|
34
34
|
export { createQuoter } from "./quote.js";
|
|
35
35
|
export type { Quoter, QuoterOptions, QuoteResult } from "./quote.js";
|
|
36
36
|
export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
|
|
37
|
-
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, 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";
|
|
37
|
+
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
38
|
+
export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
39
|
+
export type { CanonicalPage, PageMeta, ProductStructuredDataOptions } from "./seo.js";
|
|
40
40
|
export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
|
|
41
41
|
export type { ReviewRefusalCode } from "./reviews.js";
|
|
42
42
|
export type { CommuneTuple } from "./data/communes.js";
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ 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
21
|
export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
|
|
22
22
|
export { WILAYAS } from "./data/wilayas.js";
|
|
@@ -25,8 +25,8 @@ export { isWilayaId } from "./wilaya-id.js";
|
|
|
25
25
|
export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-readers.js";
|
|
26
26
|
export { createQuoter } from "./quote.js";
|
|
27
27
|
export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
|
|
28
|
-
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
29
|
-
export { SEO_TEXT_LIMITS, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
28
|
+
export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
29
|
+
export { SEO_TEXT_LIMITS, canonicalUrl, jsonLd, productMeta, productStructuredData } from "./seo.js";
|
|
30
30
|
export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
|
|
31
31
|
export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
|
|
32
32
|
export * from "./types.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A shopper following their order from a theme's own page: the form's input,
|
|
3
|
+
* and what the lookup comes to — in the few outcomes a page words, never a
|
|
4
|
+
* server code to interpret.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // src/actions/index.ts
|
|
8
|
+
* orderStatus: defineAction({
|
|
9
|
+
* accept: "form",
|
|
10
|
+
* input: orderLookupInput,
|
|
11
|
+
* handler: (input, context) => lookupOrder(input, context.request, context.locals.store.client),
|
|
12
|
+
* }),
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import type { StoreClient } from "./client/index.js";
|
|
17
|
+
import type { ShopperOrderStatus } from "./types.js";
|
|
18
|
+
/** The order number and the phone it was placed with, as a form posts them. */
|
|
19
|
+
export declare const orderLookupInput: z.ZodObject<{
|
|
20
|
+
orderNumber: z.ZodString;
|
|
21
|
+
phone: z.ZodString;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export type OrderLookupInput = z.infer<typeof orderLookupInput>;
|
|
24
|
+
/**
|
|
25
|
+
* - `found` — the order, as a shopper may see it.
|
|
26
|
+
* - `invalid-number` — what was typed is not an order number; the server is not asked.
|
|
27
|
+
* - `invalid-phone` — the server cannot read the phone as a phone number.
|
|
28
|
+
* - `not-found` — no order has that number and phone; which of the two is wrong is never said.
|
|
29
|
+
* - `rate-limited` — too many tries from this shopper or for this order; try again after the wait.
|
|
30
|
+
* - `failed` — nothing the shopper can fix: try again later.
|
|
31
|
+
*/
|
|
32
|
+
export type OrderLookup = {
|
|
33
|
+
status: "found";
|
|
34
|
+
order: ShopperOrderStatus;
|
|
35
|
+
} | {
|
|
36
|
+
status: "invalid-number";
|
|
37
|
+
} | {
|
|
38
|
+
status: "invalid-phone";
|
|
39
|
+
} | {
|
|
40
|
+
status: "not-found";
|
|
41
|
+
} | {
|
|
42
|
+
status: "rate-limited";
|
|
43
|
+
retryAfterSeconds: number | null;
|
|
44
|
+
} | {
|
|
45
|
+
status: "failed";
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Look an order up for its shopper, forwarding their address so the server's
|
|
49
|
+
* limit counts per shopper rather than per theme server.
|
|
50
|
+
*/
|
|
51
|
+
export declare function lookupOrder(input: OrderLookupInput, request: Request, client: StoreClient): Promise<OrderLookup>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A shopper following their order from a theme's own page: the form's input,
|
|
3
|
+
* and what the lookup comes to — in the few outcomes a page words, never a
|
|
4
|
+
* server code to interpret.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // src/actions/index.ts
|
|
8
|
+
* orderStatus: defineAction({
|
|
9
|
+
* accept: "form",
|
|
10
|
+
* input: orderLookupInput,
|
|
11
|
+
* handler: (input, context) => lookupOrder(input, context.request, context.locals.store.client),
|
|
12
|
+
* }),
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { collectForwardedHeaders } from "./actions.js";
|
|
17
|
+
import { isOrderNumber } from "./order-number.js";
|
|
18
|
+
/** The order number and the phone it was placed with, as a form posts them. */
|
|
19
|
+
export const orderLookupInput = z.object({
|
|
20
|
+
orderNumber: z.string().trim().min(1).max(64),
|
|
21
|
+
phone: z.string().trim().min(1).max(32),
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Look an order up for its shopper, forwarding their address so the server's
|
|
25
|
+
* limit counts per shopper rather than per theme server.
|
|
26
|
+
*/
|
|
27
|
+
export async function lookupOrder(input, request, client) {
|
|
28
|
+
if (!isOrderNumber(input.orderNumber))
|
|
29
|
+
return { status: "invalid-number" };
|
|
30
|
+
const result = await client.orders.status({ orderNumber: input.orderNumber.trim(), phone: input.phone.trim() }, collectForwardedHeaders(request));
|
|
31
|
+
if (result.success)
|
|
32
|
+
return { status: "found", order: result.data };
|
|
33
|
+
switch (result.domainError.code) {
|
|
34
|
+
case "ORDER_NOT_FOUND":
|
|
35
|
+
return { status: "not-found" };
|
|
36
|
+
case "VALIDATION_FAILED":
|
|
37
|
+
// The number was checked against the server's own pattern: what it refuses now is the phone.
|
|
38
|
+
return { status: "invalid-phone" };
|
|
39
|
+
case "ORDER_LOOKUP_RATE_LIMITED":
|
|
40
|
+
return { status: "rate-limited", retryAfterSeconds: result.domainError.retryAfterSeconds ?? null };
|
|
41
|
+
default:
|
|
42
|
+
return { status: "failed" };
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/routes.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare const STOREFRONT_PAGE_PATHS: {
|
|
|
26
26
|
readonly landing: "/lp/:slug";
|
|
27
27
|
readonly checkout: "/checkout";
|
|
28
28
|
readonly thankYou: "/thank-you";
|
|
29
|
+
/** Where a shopper follows an order by its number and phone; `?order=<number>` fills the number in. */
|
|
30
|
+
readonly trackOrder: "/track-order";
|
|
29
31
|
};
|
|
30
32
|
/** What a storefront URL means. */
|
|
31
33
|
export type StorefrontPageType = keyof typeof STOREFRONT_PAGE_PATHS;
|
package/dist/routes.js
CHANGED
|
@@ -9,6 +9,8 @@ export const STOREFRONT_PAGE_PATHS = {
|
|
|
9
9
|
landing: "/lp/:slug",
|
|
10
10
|
checkout: "/checkout",
|
|
11
11
|
thankYou: "/thank-you",
|
|
12
|
+
/** Where a shopper follows an order by its number and phone; `?order=<number>` fills the number in. */
|
|
13
|
+
trackOrder: "/track-order",
|
|
12
14
|
};
|
|
13
15
|
/**
|
|
14
16
|
* The public path of a page type, its parameter encoded:
|
|
@@ -65,6 +67,7 @@ const WIDGET_SURFACE = {
|
|
|
65
67
|
landing: "landing",
|
|
66
68
|
checkout: "checkout",
|
|
67
69
|
thankYou: "thankYou",
|
|
70
|
+
trackOrder: "pages",
|
|
68
71
|
};
|
|
69
72
|
/**
|
|
70
73
|
* @deprecated Use `STOREFRONT_PAGE_PATHS` (every page type, the home and the
|
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/store-request.d.ts
CHANGED
package/package.json
CHANGED