@wocart/storefront 2.2.0 → 3.1.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 +138 -0
- package/USAGE.md +189 -95
- package/dist/answer-readers.js +16 -43
- package/dist/astro.d.ts +6 -2
- package/dist/astro.js +3 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/products.d.ts +2 -3
- package/dist/client/products.js +2 -3
- package/dist/endpoints/revalidate-endpoint.d.ts +37 -0
- package/dist/endpoints/revalidate-endpoint.js +32 -0
- package/dist/endpoints/storefront-routes.js +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +4 -2
- package/dist/locale-routing.d.ts +11 -20
- package/dist/locale-routing.js +28 -8
- package/dist/middleware.d.ts +6 -26
- package/dist/middleware.js +0 -9
- package/dist/product-view.d.ts +0 -8
- package/dist/product-view.js +0 -17
- package/dist/revalidate.d.ts +6 -0
- package/dist/revalidate.js +6 -0
- package/dist/routes.d.ts +44 -27
- package/dist/routes.js +54 -13
- package/dist/schemas.d.ts +144 -222
- package/dist/schemas.js +77 -137
- package/dist/seo.js +2 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/store-request.d.ts +5 -4
- package/dist/store-request.js +1 -1
- package/dist/storefront-middleware.d.ts +88 -0
- package/dist/storefront-middleware.js +37 -0
- package/dist/thank-you.d.ts +32 -0
- package/dist/thank-you.js +33 -0
- package/dist/types.d.ts +76 -111
- package/dist/unavailable-store.d.ts +23 -0
- package/dist/unavailable-store.js +58 -0
- package/dist/variant-picker.d.ts +6 -2
- package/dist/variant-picker.js +6 -9
- package/dist/whatsapp.d.ts +59 -6
- package/dist/whatsapp.js +28 -10
- package/package.json +1 -1
package/dist/schemas.js
CHANGED
|
@@ -15,24 +15,18 @@ export const ProductImageSchema = MediaImageSchema.extend({
|
|
|
15
15
|
altText: z.string().nullable(),
|
|
16
16
|
position: z.number().int().min(0),
|
|
17
17
|
});
|
|
18
|
+
/** One active variant of a product, in the merchant's order; its selection in the client's language. */
|
|
18
19
|
export const ProductVariantSchema = z.object({
|
|
19
20
|
id: z.string(),
|
|
20
|
-
productId: z.string(),
|
|
21
21
|
variations: z.record(z.string(), z.string()),
|
|
22
|
-
currency: z.string(),
|
|
23
22
|
price: z.number().int().min(0),
|
|
24
23
|
compareAtPrice: z.number().int().min(0).nullable(),
|
|
25
24
|
sku: z.string().nullable(),
|
|
26
|
-
barcode: z.string().nullable(),
|
|
27
25
|
inventory: z.number().int().min(0),
|
|
28
26
|
lowStockThreshold: z.number().int().min(0),
|
|
29
|
-
|
|
27
|
+
/** The product image this variant shows. */
|
|
30
28
|
imageId: z.string().nullable(),
|
|
31
29
|
isDefault: z.boolean(),
|
|
32
|
-
active: z.boolean(),
|
|
33
|
-
position: z.number().int().min(0),
|
|
34
|
-
createdAt: z.string().datetime(),
|
|
35
|
-
updatedAt: z.string().datetime(),
|
|
36
30
|
});
|
|
37
31
|
export const OfferSchema = z.object({
|
|
38
32
|
id: z.string(),
|
|
@@ -45,9 +39,8 @@ export const OfferSchema = z.object({
|
|
|
45
39
|
rewardProductName: z.string(),
|
|
46
40
|
rewardVariantId: z.string().nullable(),
|
|
47
41
|
rewardVariantLabel: z.string().nullable(),
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
rewardUnitPrice: z.number().nullish(),
|
|
42
|
+
/** What one gift unit is worth at list price: the reward variant's price, or its product's. */
|
|
43
|
+
rewardUnitPrice: z.number().nullable(),
|
|
51
44
|
});
|
|
52
45
|
/**
|
|
53
46
|
* Quantity pricing — price tiers by quantity ("2 for 3,500 DA"). The highest
|
|
@@ -83,6 +76,18 @@ const reviewStatsSchema = z.object({
|
|
|
83
76
|
avgRating: z.number(),
|
|
84
77
|
reviewCount: z.number().int(),
|
|
85
78
|
});
|
|
79
|
+
const variantOptionSchema = z.object({
|
|
80
|
+
name: z.string(),
|
|
81
|
+
values: z.array(z.object({
|
|
82
|
+
value: z.string(),
|
|
83
|
+
// The key may be ABSENT, not just null: rows written before the colour
|
|
84
|
+
// picker existed never carry it, and a strict `.nullable()` would reject
|
|
85
|
+
// the whole product, since a failed read degrades to null. Normalising to
|
|
86
|
+
// null keeps the public shape `string | null` for every consumer.
|
|
87
|
+
hexColor: z.string().nullable().default(null),
|
|
88
|
+
})),
|
|
89
|
+
});
|
|
90
|
+
/** A product as a storefront reads it, in the client's language — the fields a listing and a product page share. */
|
|
86
91
|
const productBaseFields = {
|
|
87
92
|
id: z.string(),
|
|
88
93
|
name: z.string(),
|
|
@@ -93,71 +98,35 @@ const productBaseFields = {
|
|
|
93
98
|
currency: z.string(),
|
|
94
99
|
price: z.number(),
|
|
95
100
|
compareAtPrice: z.number().nullable(),
|
|
96
|
-
/**
|
|
97
|
-
* @deprecated Always null: what the merchant paid is never served to a
|
|
98
|
-
* storefront. Older servers sent it; a later major drops it. Do not read it.
|
|
99
|
-
*/
|
|
100
|
-
costPrice: z.number().nullable(),
|
|
101
101
|
type: z.enum(["PHYSICAL", "DIGITAL"]),
|
|
102
102
|
hasVariants: z.boolean(),
|
|
103
|
+
/** The option axes a shopper picks from; null for a product without variants. */
|
|
104
|
+
variantOptions: z.array(variantOptionSchema).nullable(),
|
|
103
105
|
sku: z.string().nullable(),
|
|
104
106
|
inventory: z.number().int(),
|
|
105
107
|
trackInventory: z.boolean(),
|
|
106
108
|
lowStockThreshold: z.number().int(),
|
|
107
109
|
categoryId: z.string().nullable(),
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* @deprecated Always null: the server never serves a removed product. The
|
|
115
|
-
* server still sends it for older releases; it will stop, and a later major
|
|
116
|
-
* drops it. Do not read it.
|
|
117
|
-
*/
|
|
118
|
-
deletedAt: z.null().optional(),
|
|
119
|
-
publishedAt: z.string().datetime().nullable(),
|
|
120
|
-
createdAt: z.string().datetime(),
|
|
121
|
-
updatedAt: z.string().datetime(),
|
|
122
|
-
shippingProfileId: z.string().nullable().optional(),
|
|
123
|
-
/** The title search engines and shared links show, in the client's language; null or absent: use `productMeta(product)`, which falls back to the name. */
|
|
124
|
-
metaTitle: z.string().nullish(),
|
|
125
|
-
/** The description search engines and shared links show; null or absent: `productMeta(product)` shortens the product's own. */
|
|
126
|
-
metaDescription: z.string().nullish(),
|
|
110
|
+
tags: z.array(z.string()),
|
|
111
|
+
/** The title search engines and shared links show; null: `productMeta(product)` falls back to the name. */
|
|
112
|
+
metaTitle: z.string().nullable(),
|
|
113
|
+
/** The description search engines and shared links show; null: `productMeta(product)` shortens the product's own. */
|
|
114
|
+
metaDescription: z.string().nullable(),
|
|
115
|
+
reviewStats: reviewStatsSchema.nullable(),
|
|
127
116
|
};
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
values: z.array(z.object({
|
|
131
|
-
value: z.string(),
|
|
132
|
-
// The key may be ABSENT, not just null: rows written before the colour
|
|
133
|
-
// picker existed never carry it, and a strict `.nullable()` would reject
|
|
134
|
-
// the whole product, since a failed read degrades to null. Normalising to
|
|
135
|
-
// null keeps the public shape `string | null` for every consumer.
|
|
136
|
-
hexColor: z.string().nullable().default(null),
|
|
137
|
-
})),
|
|
138
|
-
});
|
|
139
|
-
const rawProductFields = {
|
|
117
|
+
/** A product in a listing: its first photo stands for it. */
|
|
118
|
+
export const ProductListSchema = z.object({
|
|
140
119
|
...productBaseFields,
|
|
141
|
-
variantOptions: z.string().nullable(),
|
|
142
|
-
/** The tags as stored JSON text; read them with `productTags(product)`, which a product page's list also goes through. */
|
|
143
|
-
tags: z.string().nullable(),
|
|
144
120
|
coverImage: ProductImageSchema.nullable(),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
export const ProductListSchema = z.object(rawProductFields);
|
|
121
|
+
});
|
|
122
|
+
/** A product's page: every photo, variant and offer, and its category. */
|
|
148
123
|
export const ProductSchema = z.object({
|
|
149
124
|
...productBaseFields,
|
|
150
|
-
variantOptions: z.array(variantOptionSchema).nullable(),
|
|
151
|
-
tags: z.array(z.string()),
|
|
152
125
|
category: productCategorySchema.nullable(),
|
|
153
126
|
variants: z.array(ProductVariantSchema),
|
|
154
127
|
images: z.array(ProductImageSchema),
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
* The product's live quantity pricing; null or absent when it has none
|
|
158
|
-
* (absent from a server that predates it).
|
|
159
|
-
*/
|
|
160
|
-
quantityPricing: QuantityPricingSchema.nullish(),
|
|
128
|
+
/** The product's live quantity pricing; null when it has none. */
|
|
129
|
+
quantityPricing: QuantityPricingSchema.nullable(),
|
|
161
130
|
offers: z.array(OfferSchema),
|
|
162
131
|
});
|
|
163
132
|
export const StorePagePublicSchema = z.object({
|
|
@@ -186,10 +155,10 @@ export const StoreCollectionSchema = z.object({
|
|
|
186
155
|
handle: z.string(),
|
|
187
156
|
title: z.string(),
|
|
188
157
|
description: z.string().nullable(),
|
|
189
|
-
/** How many products a shopper sees in it
|
|
190
|
-
productCount: z.number().int().min(0)
|
|
191
|
-
/** Its first listed product's first photo;
|
|
192
|
-
image: ProductImageSchema.
|
|
158
|
+
/** How many products a shopper sees in it. */
|
|
159
|
+
productCount: z.number().int().min(0),
|
|
160
|
+
/** Its first listed product's first photo; null while it lists none with one. */
|
|
161
|
+
image: ProductImageSchema.nullable(),
|
|
193
162
|
});
|
|
194
163
|
export const StoreMenuSchema = z.object({
|
|
195
164
|
handle: z.string(),
|
|
@@ -218,11 +187,10 @@ export const StorePageLinkSchema = z.object({
|
|
|
218
187
|
title: z.string(),
|
|
219
188
|
position: z.number().int(),
|
|
220
189
|
});
|
|
221
|
-
export const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
deliveryMaxDays: z.number().int(),
|
|
190
|
+
export const StoreContactSchema = z.object({
|
|
191
|
+
phone: z.string().nullable(),
|
|
192
|
+
email: z.string().nullable(),
|
|
193
|
+
address: z.string().nullable(),
|
|
226
194
|
});
|
|
227
195
|
export const StoreWhatsAppWidgetSchema = z.object({
|
|
228
196
|
href: z.string(),
|
|
@@ -234,8 +202,8 @@ export const StoreWhatsAppWidgetSchema = z.object({
|
|
|
234
202
|
ctaLabel: z.string().nullable(),
|
|
235
203
|
prefillGeneral: z.string().nullable(),
|
|
236
204
|
prefillProduct: z.string().nullable(),
|
|
237
|
-
thankYouButtonLabel: z.string().nullable()
|
|
238
|
-
prefillThankYou: z.string().nullable()
|
|
205
|
+
thankYouButtonLabel: z.string().nullable(),
|
|
206
|
+
prefillThankYou: z.string().nullable(),
|
|
239
207
|
accent: z.enum(["whatsapp", "primary"]),
|
|
240
208
|
position: z.enum(["right", "left"]),
|
|
241
209
|
attention: z.boolean(),
|
|
@@ -273,12 +241,6 @@ export const StoreConfigSchema = z.object({
|
|
|
273
241
|
name: z.string(),
|
|
274
242
|
domain: z.string().nullable(),
|
|
275
243
|
logo: MediaImageSchema.nullable(),
|
|
276
|
-
themeId: z.string(),
|
|
277
|
-
primaryColor: z.string(),
|
|
278
|
-
accentColor: z.string(),
|
|
279
|
-
bgColor: z.string(),
|
|
280
|
-
fontFamily: z.string(),
|
|
281
|
-
fontUrl: z.string().nullable(),
|
|
282
244
|
lang: z.string().min(1),
|
|
283
245
|
locale: z.string().min(1),
|
|
284
246
|
/**
|
|
@@ -286,44 +248,28 @@ export const StoreConfigSchema = z.object({
|
|
|
286
248
|
* server owns which languages exist, so a new one never fails this parse.
|
|
287
249
|
*/
|
|
288
250
|
locales: z.array(StorefrontLanguageSchema),
|
|
289
|
-
/** The requesting theme's text edits, one map per published language. */
|
|
290
|
-
contentOverrides: z.record(z.string(), z.record(z.string(), z.string())),
|
|
291
|
-
/**
|
|
292
|
-
* The store's values for the requesting theme's settings, as saved. Absent
|
|
293
|
-
* from a server older than theme settings.
|
|
294
|
-
*/
|
|
295
|
-
themeSettings: z.record(z.string(), z.unknown()).optional(),
|
|
296
251
|
currency: z.string(),
|
|
297
252
|
currencySymbol: z.string(),
|
|
298
253
|
metaTitle: z.string().nullable(),
|
|
299
254
|
metaDescription: z.string().nullable(),
|
|
300
255
|
ogImage: MediaImageSchema.nullable(),
|
|
301
256
|
media: MediaDeliverySchema,
|
|
302
|
-
announcementBar: z.string().nullable(),
|
|
303
257
|
reviewsEnabled: z.boolean(),
|
|
304
258
|
cartEnabled: z.boolean(),
|
|
305
259
|
freeShippingThreshold: z.number().int().nullable(),
|
|
306
|
-
|
|
260
|
+
/** The requesting theme's text edits, one map per published language. */
|
|
261
|
+
contentOverrides: z.record(z.string(), z.record(z.string(), z.string())),
|
|
262
|
+
/** The store's values for the requesting theme's settings, as saved. */
|
|
263
|
+
themeSettings: z.record(z.string(), z.unknown()),
|
|
264
|
+
checkoutForm: CheckoutFormPolicySchema,
|
|
265
|
+
whatsapp: StoreWhatsAppWidgetSchema.nullable(),
|
|
266
|
+
pixelId: z.string().nullable(),
|
|
267
|
+
conversionEvent: z.enum(["Purchase", "Purchase_Confirmed", "Purchase_Delivered", "Lead"]),
|
|
268
|
+
pages: z.array(StorePageLinkSchema),
|
|
269
|
+
contact: StoreContactSchema,
|
|
307
270
|
otpEnabled: z.boolean(),
|
|
308
271
|
turnstileEnabled: z.boolean(),
|
|
309
272
|
turnstileSiteKey: z.string().nullable(),
|
|
310
|
-
status: z.enum(["active", "inactive"]),
|
|
311
|
-
createdAt: z.string().optional().default(() => new Date().toISOString()),
|
|
312
|
-
updatedAt: z.string().optional().default(() => new Date().toISOString()),
|
|
313
|
-
checkoutForm: CheckoutFormPolicySchema.default({
|
|
314
|
-
address: "optional",
|
|
315
|
-
notes: "optional",
|
|
316
|
-
email: "optional",
|
|
317
|
-
deliveryOptions: { home: true, stopDesk: true },
|
|
318
|
-
customFields: [],
|
|
319
|
-
}),
|
|
320
|
-
pixelId: z.string().nullable().default(null),
|
|
321
|
-
conversionEvent: z
|
|
322
|
-
.enum(["Purchase", "Purchase_Confirmed", "Purchase_Delivered", "Lead"])
|
|
323
|
-
.default("Purchase"),
|
|
324
|
-
pages: z.array(StorePageLinkSchema).default([]),
|
|
325
|
-
legalContact: StoreLegalContactSchema.nullable().default(null),
|
|
326
|
-
whatsapp: StoreWhatsAppWidgetSchema.nullable().default(null),
|
|
327
273
|
});
|
|
328
274
|
export const LandingPageImageSchema = MediaImageSchema.extend({
|
|
329
275
|
id: z.string(),
|
|
@@ -341,7 +287,7 @@ export const LandingPageSchema = z.object({
|
|
|
341
287
|
publishedAt: z.string().nullable(),
|
|
342
288
|
images: z.array(LandingPageImageSchema),
|
|
343
289
|
product: ProductSchema.nullable(),
|
|
344
|
-
document: PageDocumentPayloadSchema.nullable().catch(null)
|
|
290
|
+
document: PageDocumentPayloadSchema.nullable().catch(null),
|
|
345
291
|
tracking: z.object({
|
|
346
292
|
pixelId: z.string().nullable(),
|
|
347
293
|
conversionEvent: z.enum([
|
|
@@ -370,24 +316,24 @@ export const CartValidationLineSchema = z.object({
|
|
|
370
316
|
quantity: z.number().int(),
|
|
371
317
|
unitPrice: z.number(),
|
|
372
318
|
lineTotal: z.number(),
|
|
373
|
-
/** The offer applied to the line
|
|
374
|
-
offerId: z.string().
|
|
375
|
-
/** That offer's name, in the shopper's language
|
|
376
|
-
offerName: z.string().
|
|
377
|
-
/** The line's share of the coupon's discount, already out of `lineTotal
|
|
378
|
-
discount: z.number()
|
|
319
|
+
/** The offer applied to the line. */
|
|
320
|
+
offerId: z.string().nullable(),
|
|
321
|
+
/** That offer's name, in the shopper's language. */
|
|
322
|
+
offerName: z.string().nullable(),
|
|
323
|
+
/** The line's share of the coupon's discount, already out of `lineTotal`. */
|
|
324
|
+
discount: z.number(),
|
|
379
325
|
maxQuantity: z.number().nullable(),
|
|
380
326
|
blocker: z.enum(["missing", "unlisted", "out_of_stock"]).nullable(),
|
|
381
327
|
});
|
|
382
328
|
export const CartRewardSchema = z.object({
|
|
383
329
|
offerId: z.string(),
|
|
384
|
-
/** The offer's name, in the shopper's language
|
|
385
|
-
offerName: z.string().
|
|
330
|
+
/** The offer's name, in the shopper's language. */
|
|
331
|
+
offerName: z.string().nullable(),
|
|
386
332
|
productId: z.string().nullable(),
|
|
387
333
|
productName: z.string().nullable(),
|
|
388
|
-
/** The variant given and its label;
|
|
389
|
-
variantId: z.string().
|
|
390
|
-
variantLabel: z.string().
|
|
334
|
+
/** The variant given and its label; null for a product without variants. */
|
|
335
|
+
variantId: z.string().nullable(),
|
|
336
|
+
variantLabel: z.string().nullable(),
|
|
391
337
|
quantity: z.number().int(),
|
|
392
338
|
});
|
|
393
339
|
/**
|
|
@@ -409,6 +355,8 @@ export const CartValidationSchema = z.object({
|
|
|
409
355
|
threshold: z.number().nullable(),
|
|
410
356
|
qualified: z.boolean(),
|
|
411
357
|
remaining: z.number(),
|
|
358
|
+
/** Why delivery is free: an offer, the coupon or the store's amount; null when it is not. */
|
|
359
|
+
by: z.enum(["offer", "coupon", "threshold"]).nullable(),
|
|
412
360
|
}),
|
|
413
361
|
delivery: z
|
|
414
362
|
.discriminatedUnion("available", [
|
|
@@ -416,8 +364,8 @@ export const CartValidationSchema = z.object({
|
|
|
416
364
|
z.object({ available: z.literal(false), fee: z.null(), total: z.null() }),
|
|
417
365
|
])
|
|
418
366
|
.optional(),
|
|
419
|
-
/** The code's outcome: null without one
|
|
420
|
-
coupon: CouponOutcomeSchema.
|
|
367
|
+
/** The code's outcome: null without one. */
|
|
368
|
+
coupon: CouponOutcomeSchema.nullable(),
|
|
421
369
|
});
|
|
422
370
|
export const CreatedOrderSchema = z.object({
|
|
423
371
|
orderId: z.string(),
|
|
@@ -425,8 +373,8 @@ export const CreatedOrderSchema = z.object({
|
|
|
425
373
|
price: z.number(),
|
|
426
374
|
deliveryFee: z.number(),
|
|
427
375
|
total: z.number(),
|
|
428
|
-
/** What the coupon took off
|
|
429
|
-
couponDiscount: z.number()
|
|
376
|
+
/** What the coupon took off. */
|
|
377
|
+
couponDiscount: z.number(),
|
|
430
378
|
});
|
|
431
379
|
export const OrderTrackingSchema = z.object({
|
|
432
380
|
pixelId: z.string().nullable(),
|
|
@@ -445,10 +393,10 @@ export const ShopperOrderStatusSchema = z.object({
|
|
|
445
393
|
total: z.number(),
|
|
446
394
|
tracking: z.object({ number: z.string(), carrier: z.string().nullable() }).nullable(),
|
|
447
395
|
});
|
|
448
|
-
/** A page of products with the whole listing's size
|
|
396
|
+
/** A page of products with the whole listing's size. */
|
|
449
397
|
export const ProductPageEnvelopeSchema = z.object({
|
|
450
398
|
data: z.array(ProductListSchema),
|
|
451
|
-
total: z.number().int().min(0)
|
|
399
|
+
total: z.number().int().min(0),
|
|
452
400
|
});
|
|
453
401
|
/**
|
|
454
402
|
* The `{ id }` success payload of the write endpoints that only mint a row:
|
|
@@ -483,25 +431,17 @@ export const ReviewMediaSchema = z.object({
|
|
|
483
431
|
height: z.number().int().nullable(),
|
|
484
432
|
durationMs: z.number().int().nullable(),
|
|
485
433
|
});
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const obj = raw;
|
|
489
|
-
return {
|
|
490
|
-
...obj,
|
|
491
|
-
customerName: obj.customerName ?? obj.authorName ?? "Customer",
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
return raw;
|
|
495
|
-
}, z.object({
|
|
434
|
+
/** An approved review, newest first: who wrote it, and whether a delivered order proves the purchase. */
|
|
435
|
+
export const ReviewSchema = z.object({
|
|
496
436
|
id: z.string(),
|
|
497
437
|
customerName: z.string(),
|
|
498
438
|
rating: z.number(),
|
|
499
|
-
title: z.string().nullable()
|
|
439
|
+
title: z.string().nullable(),
|
|
500
440
|
body: z.string(),
|
|
501
441
|
createdAt: z.string(),
|
|
502
|
-
verified: z.boolean()
|
|
503
|
-
media: z.array(ReviewMediaSchema)
|
|
504
|
-
})
|
|
442
|
+
verified: z.boolean(),
|
|
443
|
+
media: z.array(ReviewMediaSchema),
|
|
444
|
+
});
|
|
505
445
|
export const ReviewEligibilitySchema = z.object({
|
|
506
446
|
orderNumber: z.string(),
|
|
507
447
|
customerName: z.string(),
|
|
@@ -527,7 +467,7 @@ export const ReviewUploadSchema = z.object({
|
|
|
527
467
|
});
|
|
528
468
|
export const ReviewSubmitResultSchema = z.object({
|
|
529
469
|
id: z.string(),
|
|
530
|
-
visibility: z.enum(["published", "after_review"])
|
|
470
|
+
visibility: z.enum(["published", "after_review"]),
|
|
531
471
|
});
|
|
532
472
|
/**
|
|
533
473
|
* GET /store/reviews answers `{ success, data: rows, count, total }` — the
|
package/dist/seo.js
CHANGED
|
@@ -44,7 +44,7 @@ const AVAILABILITY = { inStock: "https://schema.org/InStock", outOfStock: "https
|
|
|
44
44
|
export function productStructuredData(product, options) {
|
|
45
45
|
const meta = productMeta(product);
|
|
46
46
|
const view = getProductView(product);
|
|
47
|
-
const variants = product.hasVariants ? product.variants
|
|
47
|
+
const variants = product.hasVariants ? product.variants : [];
|
|
48
48
|
const prices = variants.map((variant) => variant.price);
|
|
49
49
|
const sellable = product.hasVariants
|
|
50
50
|
? variants.some((variant) => !product.trackInventory || variant.inventory > 0)
|
|
@@ -55,7 +55,7 @@ export function productStructuredData(product, options) {
|
|
|
55
55
|
? { "@type": "AggregateOffer", url: options.url, priceCurrency: product.currency, lowPrice: Math.min(...prices), highPrice: Math.max(...prices), offerCount: prices.length, availability }
|
|
56
56
|
: { "@type": "Offer", url: options.url, priceCurrency: product.currency, price: view.basePrice, availability, itemCondition: "https://schema.org/NewCondition" };
|
|
57
57
|
const sku = product.sku ?? (product.hasVariants ? initialVariant(product)?.sku : null) ?? null;
|
|
58
|
-
const images =
|
|
58
|
+
const images = product.images.map((image) => image.url);
|
|
59
59
|
const rating = product.reviewStats && product.reviewStats.reviewCount > 0 ? product.reviewStats : null;
|
|
60
60
|
return {
|
|
61
61
|
"@context": "https://schema.org",
|
package/dist/server.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { StoreClient } from "./client/index.js";
|
|
|
3
3
|
export type { ClientResult } from "./client/core.js";
|
|
4
4
|
export { StorefrontError, isStorefrontError } from "./client/errors.js";
|
|
5
5
|
export { previewConfig, previewResponseHeaders, readPreviewRequest, verifyPreviewToken } from "./preview.js";
|
|
6
|
-
export { REVALIDATE_PATH, readRevalidateRequest } from "./revalidate.js";
|
|
6
|
+
export { PAGES_CACHE_TAG, REVALIDATE_PATH, readRevalidateRequest } from "./revalidate.js";
|
|
7
7
|
export type { RevalidateClaims, RevalidateRequest, RevalidateRequestOptions } from "./revalidate.js";
|
|
8
8
|
export type { PreviewClaims, PreviewDraft, PreviewRequest, PreviewRequestOptions, PreviewTokenCheck, PreviewTokenOptions, PreviewTokenRefusal, } from "./preview.js";
|
|
9
9
|
export { catalogPaths, sitemapXml } from "./sitemap.js";
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createStoreClient } from "./client/index.js";
|
|
2
2
|
export { StorefrontError, isStorefrontError } from "./client/errors.js";
|
|
3
3
|
export { previewConfig, previewResponseHeaders, readPreviewRequest, verifyPreviewToken } from "./preview.js";
|
|
4
|
-
export { REVALIDATE_PATH, readRevalidateRequest } from "./revalidate.js";
|
|
4
|
+
export { PAGES_CACHE_TAG, REVALIDATE_PATH, readRevalidateRequest } from "./revalidate.js";
|
|
5
5
|
export { catalogPaths, sitemapXml } from "./sitemap.js";
|
package/dist/store-request.d.ts
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* language the draft asks for when the store serves it.
|
|
15
15
|
*
|
|
16
16
|
* The theme keeps what is its own: the client it creates (one per request),
|
|
17
|
-
* the config it falls back to when the store cannot be read
|
|
18
|
-
* it builds from what these
|
|
17
|
+
* the config it falls back to when the store cannot be read
|
|
18
|
+
* (`unavailableStoreConfig`), and the context it builds from what these
|
|
19
|
+
* return.
|
|
19
20
|
*/
|
|
20
21
|
import type { StoreClient } from "./client/index.js";
|
|
21
22
|
import { type LocaleRouting } from "./locale-routing.js";
|
|
@@ -24,7 +25,7 @@ import type { StoreConfig } from "./types.js";
|
|
|
24
25
|
export interface StoreRequestOptions {
|
|
25
26
|
/** The request's client in `locale`, or in the store's primary language when absent. */
|
|
26
27
|
client: (locale?: string) => StoreClient;
|
|
27
|
-
/** What renders when the store's config cannot be read:
|
|
28
|
+
/** What renders when the store's config cannot be read: `unavailableStoreConfig(…)`. */
|
|
28
29
|
fallback: StoreConfig;
|
|
29
30
|
/** Whether a path serves part of a page rather than a page. Default: `/api/` and `/_server-islands/`. */
|
|
30
31
|
embedded?: (pathname: string) => boolean;
|
|
@@ -60,7 +61,7 @@ export type StoreRequestResolution = ({
|
|
|
60
61
|
* ```ts
|
|
61
62
|
* const request = await resolveStoreRequest(
|
|
62
63
|
* { url: context.url, referer: context.request.headers.get("Referer") },
|
|
63
|
-
* { client: createStore, fallback:
|
|
64
|
+
* { client: createStore, fallback: unavailableStoreConfig({ serverUrl: env.COD_SERVER_URL }) },
|
|
64
65
|
* );
|
|
65
66
|
* if (request.kind === "redirect") return context.redirect(request.location, request.status);
|
|
66
67
|
* context.locals.store = buildContext(request);
|
package/dist/store-request.js
CHANGED
|
@@ -38,7 +38,7 @@ function askingPage(url, referer) {
|
|
|
38
38
|
* ```ts
|
|
39
39
|
* const request = await resolveStoreRequest(
|
|
40
40
|
* { url: context.url, referer: context.request.headers.get("Referer") },
|
|
41
|
-
* { client: createStore, fallback:
|
|
41
|
+
* { client: createStore, fallback: unavailableStoreConfig({ serverUrl: env.COD_SERVER_URL }) },
|
|
42
42
|
* );
|
|
43
43
|
* if (request.kind === "redirect") return context.redirect(request.location, request.status);
|
|
44
44
|
* context.locals.store = buildContext(request);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The request lifecycle every theme's `src/middleware.ts` runs, in one call:
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* // src/middleware.ts
|
|
6
|
+
* import { getActionContext } from "astro:actions";
|
|
7
|
+
* import { createStorefrontMiddleware } from "@wocart/storefront/astro";
|
|
8
|
+
* import { THEME_PREVIEW_PATH } from "@wocart/theme";
|
|
9
|
+
*
|
|
10
|
+
* export const onRequest = createStorefrontMiddleware({
|
|
11
|
+
* client: createStore,
|
|
12
|
+
* fallback: unavailableStoreConfig({ serverUrl, locale: "ar" }),
|
|
13
|
+
* preview: { path: THEME_PREVIEW_PATH, publicKey: () => WOCART_PREVIEW_KEY, theme: THEME_KEY },
|
|
14
|
+
* context: (store) => buildStoreContext(store),
|
|
15
|
+
* getActionContext,
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* In order:
|
|
20
|
+
*
|
|
21
|
+
* - A prerendered file (the manifest, the theme's styles) and the
|
|
22
|
+
* revalidation route (`REVALIDATE_PATH`) need no store.
|
|
23
|
+
* - A request that already has a store keeps it: a page's `notFound()`
|
|
24
|
+
* rewrites to `/404` and Astro runs the middleware again, and the 404 must
|
|
25
|
+
* stay in the language the shopper asked for.
|
|
26
|
+
* - The editor's preview (`preview.path`) renders the store as the draft
|
|
27
|
+
* would leave it, after its token is verified; it never runs a form action.
|
|
28
|
+
* - Any other request resolves its store and language
|
|
29
|
+
* (`resolveStoreRequest`), answers its redirect if it has one, builds the
|
|
30
|
+
* theme's context, runs a form-called action for the page this POST renders
|
|
31
|
+
* (`runFormAction`), then renders the unprefixed route.
|
|
32
|
+
*
|
|
33
|
+
* `astro:actions` is an Astro virtual module, so the theme passes
|
|
34
|
+
* `getActionContext` in. The theme's context becomes `locals.store`; a
|
|
35
|
+
* verified preview's claims, draft and token become `locals.preview`.
|
|
36
|
+
*/
|
|
37
|
+
import type { StoreClient } from "./client/index.js";
|
|
38
|
+
import { type ActionDescriptor, type ActionHandlerResult } from "./middleware.js";
|
|
39
|
+
import { type PreviewClaims, type PreviewDraft, type PreviewRequestOptions } from "./preview.js";
|
|
40
|
+
import { type ResolvedStore } from "./store-request.js";
|
|
41
|
+
import type { StoreConfig } from "./types.js";
|
|
42
|
+
/** What a verified preview leaves on `locals.preview`. */
|
|
43
|
+
export interface PreviewLocals {
|
|
44
|
+
claims: PreviewClaims;
|
|
45
|
+
draft: PreviewDraft;
|
|
46
|
+
token: string;
|
|
47
|
+
}
|
|
48
|
+
/** What the middleware uses of Astro's `APIContext`. */
|
|
49
|
+
export interface StorefrontMiddlewareContext {
|
|
50
|
+
isPrerendered: boolean;
|
|
51
|
+
url: URL;
|
|
52
|
+
request: Request;
|
|
53
|
+
/** `App.Locals`: the middleware writes `store` and, on a preview, `preview`. */
|
|
54
|
+
locals: object;
|
|
55
|
+
redirect(location: string, status: 301 | 302): Response;
|
|
56
|
+
}
|
|
57
|
+
/** What the middleware uses of `getActionContext(context)`. */
|
|
58
|
+
export interface StorefrontActionContext<TResult extends ActionHandlerResult, TSerialized> {
|
|
59
|
+
action?: ActionDescriptor<TResult> | undefined;
|
|
60
|
+
setActionResult: (name: string, serialized: TSerialized) => void;
|
|
61
|
+
serializeActionResult: (result: TResult) => TSerialized;
|
|
62
|
+
}
|
|
63
|
+
export interface StorefrontMiddlewareOptions<TContext extends StorefrontMiddlewareContext, TStore, TResult extends ActionHandlerResult, TSerialized> {
|
|
64
|
+
/** The theme's one client factory: in `locale`, or in the store's primary language when absent. */
|
|
65
|
+
client: (locale?: string) => StoreClient;
|
|
66
|
+
/** What renders when the store's config cannot be read: `unavailableStoreConfig(…)`. */
|
|
67
|
+
fallback: StoreConfig;
|
|
68
|
+
/**
|
|
69
|
+
* The editor's preview route (`THEME_PREVIEW_PATH`) and how its token is
|
|
70
|
+
* checked; no `publicKey`: the route answers 404. Pass the key as a
|
|
71
|
+
* function to read it on each request: a secret from `astro:env/server`
|
|
72
|
+
* can be set after the module loads.
|
|
73
|
+
*/
|
|
74
|
+
preview: {
|
|
75
|
+
path: string;
|
|
76
|
+
publicKey: PreviewRequestOptions["publicKey"] | (() => PreviewRequestOptions["publicKey"]);
|
|
77
|
+
} & Pick<PreviewRequestOptions, "theme" | "maxBytes">;
|
|
78
|
+
/** The theme's context for a resolved store; it becomes `locals.store`. */
|
|
79
|
+
context: (store: ResolvedStore) => TStore;
|
|
80
|
+
/** Astro's `getActionContext`, from `astro:actions`. */
|
|
81
|
+
getActionContext: (context: TContext) => StorefrontActionContext<TResult, TSerialized>;
|
|
82
|
+
/** Whether a path serves part of a page rather than a page. Default: `/api/` and `/_server-islands/`. */
|
|
83
|
+
embedded?: (pathname: string) => boolean;
|
|
84
|
+
}
|
|
85
|
+
/** An Astro middleware handler (`onRequest`). */
|
|
86
|
+
export type StorefrontMiddleware<TContext extends StorefrontMiddlewareContext> = (context: TContext, next: (rewrite?: string) => Promise<Response>) => Promise<Response>;
|
|
87
|
+
/** The theme's `onRequest`: store, language, redirects, preview and form actions, as described above. */
|
|
88
|
+
export declare function createStorefrontMiddleware<TContext extends StorefrontMiddlewareContext, TStore, TResult extends ActionHandlerResult, TSerialized>(options: StorefrontMiddlewareOptions<TContext, TStore, TResult, TSerialized>): StorefrontMiddleware<TContext>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runFormAction } from "./middleware.js";
|
|
2
|
+
import { readPreviewRequest } from "./preview.js";
|
|
3
|
+
import { REVALIDATE_PATH } from "./revalidate.js";
|
|
4
|
+
import { resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
|
|
5
|
+
/** The theme's `onRequest`: store, language, redirects, preview and form actions, as described above. */
|
|
6
|
+
export function createStorefrontMiddleware(options) {
|
|
7
|
+
const store = { client: options.client, fallback: options.fallback, ...(options.embedded ? { embedded: options.embedded } : {}) };
|
|
8
|
+
const { path: previewPath, publicKey, ...previewOptions } = options.preview;
|
|
9
|
+
return async (context, next) => {
|
|
10
|
+
const locals = context.locals;
|
|
11
|
+
if (context.isPrerendered)
|
|
12
|
+
return next();
|
|
13
|
+
if (context.url.pathname === REVALIDATE_PATH)
|
|
14
|
+
return next();
|
|
15
|
+
if (locals.store !== undefined)
|
|
16
|
+
return next();
|
|
17
|
+
if (context.url.pathname === previewPath) {
|
|
18
|
+
const preview = await readPreviewRequest(context.request, { ...previewOptions, publicKey: typeof publicKey === "function" ? publicKey() : publicKey });
|
|
19
|
+
if (!preview.ok)
|
|
20
|
+
return new Response(null, { status: preview.status, headers: { "Cache-Control": "no-store" } });
|
|
21
|
+
locals.store = options.context(await resolvePreviewStore(preview.draft, store));
|
|
22
|
+
locals.preview = { claims: preview.claims, draft: preview.draft, token: preview.token };
|
|
23
|
+
return next();
|
|
24
|
+
}
|
|
25
|
+
const request = await resolveStoreRequest({ url: context.url, referer: context.request.headers.get("Referer") }, store);
|
|
26
|
+
if (request.kind === "redirect")
|
|
27
|
+
return context.redirect(request.location, request.status);
|
|
28
|
+
locals.store = options.context(request);
|
|
29
|
+
const { action, setActionResult, serializeActionResult } = options.getActionContext(context);
|
|
30
|
+
await runFormAction({
|
|
31
|
+
...(action ? { action: { calledFrom: action.calledFrom, name: action.name, handler: action.handler } } : {}),
|
|
32
|
+
setActionResult,
|
|
33
|
+
serializeActionResult,
|
|
34
|
+
});
|
|
35
|
+
return request.rewrite === null ? next() : next(request.rewrite);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thank-you page's address for a placed order, and its reading back. The
|
|
3
|
+
* query carries the order's number, total and id — never the shopper's
|
|
4
|
+
* details — so the page can thank, show the total and fire a conversion
|
|
5
|
+
* without reading the order again. One spelling, here: a theme writes it
|
|
6
|
+
* after `placeOrder`, its thank-you page reads it, and an editor previews
|
|
7
|
+
* the page with a sample order by building the same address.
|
|
8
|
+
*/
|
|
9
|
+
import { type LocaleRouting } from "./locale-routing.js";
|
|
10
|
+
/** What the thank-you page reads from its address. */
|
|
11
|
+
export interface ThankYouOrder {
|
|
12
|
+
/** The order number the shopper is given; empty when the address has none. */
|
|
13
|
+
orderNumber: string;
|
|
14
|
+
/** The order's id, for a conversion's deduplication; empty when the address has none. */
|
|
15
|
+
orderId: string;
|
|
16
|
+
/** The order's total in the store's currency; null when missing or not a number. */
|
|
17
|
+
total: number | null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The thank-you page for a placed order: `/thank-you?order=…&total=…&oid=…`,
|
|
21
|
+
* in the request's language when `language` is given (`/fr/thank-you?…`).
|
|
22
|
+
*/
|
|
23
|
+
export declare function thankYouHref(order: {
|
|
24
|
+
orderNumber: string;
|
|
25
|
+
total: number;
|
|
26
|
+
orderId?: string | null;
|
|
27
|
+
}, language?: {
|
|
28
|
+
locale: string;
|
|
29
|
+
routing: LocaleRouting;
|
|
30
|
+
}): string;
|
|
31
|
+
/** The order a thank-you address names: `thankYouHref`'s query, read back and trimmed. */
|
|
32
|
+
export declare function readThankYou(url: URL | URLSearchParams): ThankYouOrder;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thank-you page's address for a placed order, and its reading back. The
|
|
3
|
+
* query carries the order's number, total and id — never the shopper's
|
|
4
|
+
* details — so the page can thank, show the total and fire a conversion
|
|
5
|
+
* without reading the order again. One spelling, here: a theme writes it
|
|
6
|
+
* after `placeOrder`, its thank-you page reads it, and an editor previews
|
|
7
|
+
* the page with a sample order by building the same address.
|
|
8
|
+
*/
|
|
9
|
+
import { localizeHref } from "./locale-routing.js";
|
|
10
|
+
import { storefrontPath } from "./routes.js";
|
|
11
|
+
const ORDER_NUMBER = "order";
|
|
12
|
+
const TOTAL = "total";
|
|
13
|
+
const ORDER_ID = "oid";
|
|
14
|
+
/**
|
|
15
|
+
* The thank-you page for a placed order: `/thank-you?order=…&total=…&oid=…`,
|
|
16
|
+
* in the request's language when `language` is given (`/fr/thank-you?…`).
|
|
17
|
+
*/
|
|
18
|
+
export function thankYouHref(order, language) {
|
|
19
|
+
const query = new URLSearchParams({ [ORDER_NUMBER]: order.orderNumber, [TOTAL]: String(order.total), [ORDER_ID]: order.orderId ?? "" });
|
|
20
|
+
const path = storefrontPath("thankYou");
|
|
21
|
+
return `${language ? localizeHref(path, language.locale, language.routing) : path}?${query.toString()}`;
|
|
22
|
+
}
|
|
23
|
+
/** The order a thank-you address names: `thankYouHref`'s query, read back and trimmed. */
|
|
24
|
+
export function readThankYou(url) {
|
|
25
|
+
const params = url instanceof URLSearchParams ? url : url.searchParams;
|
|
26
|
+
const raw = params.get(TOTAL)?.trim() ?? "";
|
|
27
|
+
const total = /^-?\d+$/.test(raw) ? Number.parseInt(raw, 10) : Number.NaN;
|
|
28
|
+
return {
|
|
29
|
+
orderNumber: (params.get(ORDER_NUMBER) ?? "").trim(),
|
|
30
|
+
orderId: (params.get(ORDER_ID) ?? "").trim(),
|
|
31
|
+
total: Number.isFinite(total) ? total : null,
|
|
32
|
+
};
|
|
33
|
+
}
|