@wocart/storefront 1.23.0 → 2.0.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/README.md +22 -117
- package/USAGE.md +31 -70
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/schemas.d.ts +84 -35
- package/dist/schemas.js +17 -12
- package/dist/seo.js +1 -1
- package/dist/types.d.ts +7 -7
- package/dist/utils/image.d.ts +50 -169
- package/dist/utils/image.js +72 -165
- package/dist/utils/product-image.d.ts +6 -6
- package/dist/utils/product-image.js +7 -7
- package/package.json +1 -1
package/dist/schemas.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
2
|
+
import { MEDIA_DELIVERY_KINDS } from "./utils/image.js";
|
|
3
|
+
export const MediaImageSchema = z.object({
|
|
4
|
+
key: z.string(),
|
|
5
|
+
url: z.string().url(),
|
|
6
|
+
width: z.number().int().nullable(),
|
|
7
|
+
height: z.number().int().nullable(),
|
|
8
|
+
});
|
|
9
|
+
export const MediaDeliverySchema = z.object({
|
|
10
|
+
kind: z.enum(MEDIA_DELIVERY_KINDS),
|
|
11
|
+
origin: z.string().url(),
|
|
12
|
+
});
|
|
13
|
+
export const ProductImageSchema = MediaImageSchema.extend({
|
|
3
14
|
id: z.string(),
|
|
4
|
-
src: z.string().url(),
|
|
5
|
-
srcSm: z.string().url().nullable(),
|
|
6
|
-
srcMd: z.string().url().nullable(),
|
|
7
|
-
srcLg: z.string().url().nullable(),
|
|
8
15
|
altText: z.string().nullable(),
|
|
9
16
|
position: z.number().int().min(0),
|
|
10
17
|
});
|
|
@@ -63,7 +70,7 @@ const productCategorySchema = z.object({
|
|
|
63
70
|
slug: z.string(),
|
|
64
71
|
description: z.string().nullable(),
|
|
65
72
|
parentId: z.string().nullable(),
|
|
66
|
-
|
|
73
|
+
image: MediaImageSchema.nullable(),
|
|
67
74
|
metaTitle: z.string().nullable(),
|
|
68
75
|
metaDescription: z.string().nullable(),
|
|
69
76
|
metaKeywords: z.string().nullable(),
|
|
@@ -265,7 +272,7 @@ export const StoreConfigSchema = z.object({
|
|
|
265
272
|
id: z.string(),
|
|
266
273
|
name: z.string(),
|
|
267
274
|
domain: z.string().nullable(),
|
|
268
|
-
|
|
275
|
+
logo: MediaImageSchema.nullable(),
|
|
269
276
|
themeId: z.string(),
|
|
270
277
|
primaryColor: z.string(),
|
|
271
278
|
accentColor: z.string(),
|
|
@@ -290,7 +297,8 @@ export const StoreConfigSchema = z.object({
|
|
|
290
297
|
currencySymbol: z.string(),
|
|
291
298
|
metaTitle: z.string().nullable(),
|
|
292
299
|
metaDescription: z.string().nullable(),
|
|
293
|
-
ogImage:
|
|
300
|
+
ogImage: MediaImageSchema.nullable(),
|
|
301
|
+
media: MediaDeliverySchema,
|
|
294
302
|
announcementBar: z.string().nullable(),
|
|
295
303
|
reviewsEnabled: z.boolean(),
|
|
296
304
|
cartEnabled: z.boolean(),
|
|
@@ -317,13 +325,10 @@ export const StoreConfigSchema = z.object({
|
|
|
317
325
|
legalContact: StoreLegalContactSchema.nullable().default(null),
|
|
318
326
|
whatsapp: StoreWhatsAppWidgetSchema.nullable().default(null),
|
|
319
327
|
});
|
|
320
|
-
export const LandingPageImageSchema =
|
|
328
|
+
export const LandingPageImageSchema = MediaImageSchema.extend({
|
|
321
329
|
id: z.string(),
|
|
322
|
-
src: z.string(),
|
|
323
330
|
altText: z.string().nullable(),
|
|
324
331
|
position: z.number().int(),
|
|
325
|
-
width: z.number().int().nullable().default(null),
|
|
326
|
-
height: z.number().int().nullable().default(null),
|
|
327
332
|
});
|
|
328
333
|
export const LandingPageSchema = z.object({
|
|
329
334
|
id: z.string(),
|
package/dist/seo.js
CHANGED
|
@@ -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 = [...product.images].sort((a, b) => a.position - b.position).map((image) => image.
|
|
58
|
+
const images = [...product.images].sort((a, b) => a.position - b.position).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/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { COUPON_REFUSAL_REASONS } from "./coupon.js";
|
|
2
2
|
import type { PRODUCT_SORTS, SHOPPER_ORDER_STAGES } from "./catalog.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import type { MediaDelivery, MediaImage } from "./utils/image.js";
|
|
4
5
|
import { ProductSchema, ProductListSchema, CategorySchema, ProductImageSchema, ProductVariantSchema, OfferSchema, QuantityPricingSchema, PageDocumentPayloadSchema } from "./schemas.js";
|
|
5
6
|
export type Product = z.infer<typeof ProductSchema>;
|
|
6
7
|
export type ProductList = z.infer<typeof ProductListSchema>;
|
|
@@ -67,7 +68,7 @@ export interface StoreConfig {
|
|
|
67
68
|
id: string;
|
|
68
69
|
name: string;
|
|
69
70
|
domain: string | null;
|
|
70
|
-
|
|
71
|
+
logo: MediaImage | null;
|
|
71
72
|
themeId: string;
|
|
72
73
|
/**
|
|
73
74
|
* @deprecated A store's colors and font are its theme's settings: declare
|
|
@@ -124,7 +125,10 @@ export interface StoreConfig {
|
|
|
124
125
|
currencySymbol: string;
|
|
125
126
|
metaTitle: string | null;
|
|
126
127
|
metaDescription: string | null;
|
|
127
|
-
|
|
128
|
+
/** The image links to the store show when shared. */
|
|
129
|
+
ogImage: MediaImage | null;
|
|
130
|
+
/** Where every image key is served from, and how it is resized: pass it to `mediaUrl`, `mediaImageUrl` and `pictureOf`. */
|
|
131
|
+
media: MediaDelivery;
|
|
128
132
|
/**
|
|
129
133
|
* @deprecated Always null. An announcement is a theme's own content: declare
|
|
130
134
|
* it as a theme setting and read `themeSettings`. Removed in 2.0.
|
|
@@ -256,14 +260,10 @@ export interface WilayaRates {
|
|
|
256
260
|
stopDesk: number;
|
|
257
261
|
}
|
|
258
262
|
/** One image in a landing page's ordered stack. */
|
|
259
|
-
export interface LandingPageImage {
|
|
263
|
+
export interface LandingPageImage extends MediaImage {
|
|
260
264
|
id: string;
|
|
261
|
-
src: string;
|
|
262
265
|
altText: string | null;
|
|
263
266
|
position: number;
|
|
264
|
-
/** Intrinsic dimensions (px), captured at upload — the page reserves the box. */
|
|
265
|
-
width: number | null;
|
|
266
|
-
height: number | null;
|
|
267
267
|
}
|
|
268
268
|
/**
|
|
269
269
|
* A published landing page — GET /store/landing-pages/{slug}.
|
package/dist/utils/image.d.ts
CHANGED
|
@@ -1,88 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The store's images: where a key is served, and how a photo is asked for at
|
|
3
|
+
* the size a page shows it.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* negotiation, and no-op fallback logic behind a simple interface.
|
|
5
|
+
* Every image the store's server answers is a `MediaImage` — its key and its
|
|
6
|
+
* URL — and the store's config carries its `MediaDelivery`: the origin every
|
|
7
|
+
* key is served from and how that origin resizes. These helpers take that
|
|
8
|
+
* delivery, so one theme works on every host a store can use.
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
export declare const MEDIA_DELIVERY_KINDS: readonly ["imagekit", "cloudflare", "direct"];
|
|
11
|
+
/** How a store's images are resized: `imagekit` (`?tr=`), `cloudflare` (`/cdn-cgi/image/`), or `direct` (served as stored). */
|
|
12
|
+
export type MediaDeliveryKind = (typeof MEDIA_DELIVERY_KINDS)[number];
|
|
13
|
+
/** Where a store's images are served from: a key's URL is `origin`, a slash, and the key. */
|
|
14
|
+
export interface MediaDelivery {
|
|
15
|
+
kind: MediaDeliveryKind;
|
|
16
|
+
origin: string;
|
|
16
17
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* @example
|
|
26
|
-
* ```ts
|
|
27
|
-
* const url = optimizeImage(
|
|
28
|
-
* 'https://media.example.com/products/image.jpg',
|
|
29
|
-
* { width: 400, height: 400 },
|
|
30
|
-
* 'media.example.com'
|
|
31
|
-
* );
|
|
32
|
-
* // Returns: https://media.example.com/cdn-cgi/image/onerror=redirect,width=400,height=400,format=webp,quality=85,fit=cover/products/image.jpg
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
export declare function optimizeImage(originalUrl: string, options: ImageResizeOptions, mediaDomain?: string): string;
|
|
36
|
-
/** What `mediaImageUrl` can ask Cloudflare for; an omitted value is left to Cloudflare's default. */
|
|
18
|
+
/** An image as the store's server answers it. */
|
|
19
|
+
export interface MediaImage {
|
|
20
|
+
key: string;
|
|
21
|
+
url: string;
|
|
22
|
+
width: number | null;
|
|
23
|
+
height: number | null;
|
|
24
|
+
}
|
|
25
|
+
/** How to show a photo; an omitted value is left to the host's default. */
|
|
37
26
|
export interface MediaImageParams {
|
|
38
27
|
width?: number;
|
|
39
28
|
height?: number;
|
|
29
|
+
/** Encoder quality, 1–100. */
|
|
40
30
|
quality?: number;
|
|
31
|
+
/** `auto` lets the host pick the best format the browser takes. */
|
|
41
32
|
format?: string;
|
|
33
|
+
/** CSS `object-fit` words: `cover` and `crop` fill the box, `contain` and `scale-down` fit inside it (`scale-down` never enlarges), `squeeze` stretches, `pad` letterboxes. */
|
|
42
34
|
fit?: "scale-down" | "contain" | "cover" | "crop" | "pad" | "squeeze";
|
|
43
35
|
}
|
|
36
|
+
/** Whether `value` can be a Media Key: a store folder, then segments of letters, digits, `.`, `_` and `-` that start with a letter or digit. */
|
|
37
|
+
export declare function isMediaKey(value: unknown): value is string;
|
|
44
38
|
/**
|
|
45
|
-
* The
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* domain's zone has Image Transformations on.
|
|
39
|
+
* The URL of a key in the store's media — what a theme's `image` field
|
|
40
|
+
* stores (`products/3f2a9c.webp`). The key is the image's identity; its URL
|
|
41
|
+
* depends on the store's delivery, so it is derived at render, never stored.
|
|
49
42
|
*
|
|
50
|
-
* `
|
|
51
|
-
* the original image instead of answering with an error.
|
|
52
|
-
*
|
|
53
|
-
* @returns `null` when there is no media domain, or the URL is not on it.
|
|
43
|
+
* @returns `null` without a delivery, or for a value that is not a Media Key.
|
|
54
44
|
*/
|
|
55
|
-
export declare function
|
|
45
|
+
export declare function mediaUrl(key: string, delivery: MediaDelivery | null | undefined): string | null;
|
|
56
46
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* so it is derived here, at render, never stored. Pass the result to
|
|
61
|
-
* `mediaImageUrl` or Astro's `<Image>` to resize it.
|
|
47
|
+
* A photo of the store's media at the size and format a page shows it, from
|
|
48
|
+
* the delivery's own resizer: ImageKit's `?tr=` or Cloudflare's
|
|
49
|
+
* `/cdn-cgi/image/` (which falls back to the original when a resize fails).
|
|
62
50
|
*
|
|
63
|
-
* @returns `null`
|
|
64
|
-
*
|
|
65
|
-
* makes an empty one), whitespace, control characters, a backslash, `?` or `#`.
|
|
51
|
+
* @returns `null` without a delivery, for a URL not served by it, or for a
|
|
52
|
+
* `direct` delivery, which does not resize — show the original then.
|
|
66
53
|
*/
|
|
67
|
-
export declare function
|
|
54
|
+
export declare function mediaImageUrl(url: string, params: MediaImageParams, delivery: MediaDelivery | null | undefined): string | null;
|
|
68
55
|
export interface MediaImageSrcsetOptions {
|
|
69
|
-
/** The store's
|
|
70
|
-
|
|
71
|
-
/** Height ÷ width: given, each size is cropped to it (`
|
|
56
|
+
/** The store's delivery; without one there is no srcset. */
|
|
57
|
+
delivery: MediaDelivery | null | undefined;
|
|
58
|
+
/** Height ÷ width: given, each size is cropped to it (`cover`); absent, the photo keeps its shape and is never enlarged (`scale-down`). */
|
|
72
59
|
ratio?: number;
|
|
73
|
-
/** Encoder quality, 1–100;
|
|
60
|
+
/** Encoder quality, 1–100; the host's default when absent. */
|
|
74
61
|
quality?: number;
|
|
75
62
|
}
|
|
76
63
|
/**
|
|
77
64
|
* The same photo at several widths, with width descriptors
|
|
78
65
|
* (`… 360w, … 720w`), for an `<img>` whose `sizes` says how wide it shows —
|
|
79
|
-
* a phone then downloads a phone-sized file
|
|
80
|
-
* best format the browser takes (`format=auto`).
|
|
66
|
+
* a phone then downloads a phone-sized file, in the best format it takes.
|
|
81
67
|
*
|
|
82
|
-
* @returns `null` when
|
|
68
|
+
* @returns `null` when the photo cannot be resized (see `mediaImageUrl`) or no
|
|
83
69
|
* width is a positive whole number.
|
|
84
70
|
*/
|
|
85
|
-
export declare function mediaImageSrcset(src: string, widths: readonly number[], options
|
|
71
|
+
export declare function mediaImageSrcset(src: string, widths: readonly number[], options: MediaImageSrcsetOptions): string | null;
|
|
86
72
|
/** An image a merchant picked, by its media key — what a theme's `image` field resolves to. */
|
|
87
73
|
export interface PickedImage {
|
|
88
74
|
key: string;
|
|
@@ -105,116 +91,11 @@ export interface Picture {
|
|
|
105
91
|
position: string | undefined;
|
|
106
92
|
}
|
|
107
93
|
/**
|
|
108
|
-
* A picked image as a theme draws it: its URL
|
|
109
|
-
*
|
|
110
|
-
*
|
|
94
|
+
* A picked image as a theme draws it: its URL in the store's media, its alt
|
|
95
|
+
* text (empty when the merchant wrote none), its size, and its focal point as
|
|
96
|
+
* `object-position`, so a cropped photo keeps what matters in view.
|
|
111
97
|
*
|
|
112
|
-
* @returns `null` when no image is picked, the key
|
|
113
|
-
*
|
|
114
|
-
*/
|
|
115
|
-
export declare function pictureOf(image: PickedImage | null | undefined, mediaDomain?: string): Picture | null;
|
|
116
|
-
/**
|
|
117
|
-
* Predefined image sizes for common use cases
|
|
98
|
+
* @returns `null` when no image is picked, the key is not a Media Key, or there
|
|
99
|
+
* is no delivery.
|
|
118
100
|
*/
|
|
119
|
-
export declare
|
|
120
|
-
readonly CARD: {
|
|
121
|
-
readonly width: 400;
|
|
122
|
-
readonly height: 400;
|
|
123
|
-
readonly quality: 85;
|
|
124
|
-
};
|
|
125
|
-
readonly GALLERY_MAIN: {
|
|
126
|
-
readonly width: 800;
|
|
127
|
-
readonly height: 800;
|
|
128
|
-
readonly quality: 90;
|
|
129
|
-
};
|
|
130
|
-
readonly GALLERY_THUMB: {
|
|
131
|
-
readonly width: 80;
|
|
132
|
-
readonly height: 80;
|
|
133
|
-
readonly quality: 80;
|
|
134
|
-
};
|
|
135
|
-
readonly HERO: {
|
|
136
|
-
readonly width: 1200;
|
|
137
|
-
readonly height: 600;
|
|
138
|
-
readonly quality: 90;
|
|
139
|
-
};
|
|
140
|
-
readonly CATEGORY: {
|
|
141
|
-
readonly width: 300;
|
|
142
|
-
readonly height: 200;
|
|
143
|
-
readonly quality: 85;
|
|
144
|
-
};
|
|
145
|
-
readonly LOGO: {
|
|
146
|
-
readonly width: 200;
|
|
147
|
-
readonly height: 80;
|
|
148
|
-
readonly quality: 90;
|
|
149
|
-
readonly fit: "contain";
|
|
150
|
-
};
|
|
151
|
-
};
|
|
152
|
-
/**
|
|
153
|
-
* Quick helper functions for common image sizes
|
|
154
|
-
*
|
|
155
|
-
* These take the media domain per call. Theme code usually binds it once with
|
|
156
|
-
* `createImageHelpers(mediaDomain)` and uses the returned helpers, so a call
|
|
157
|
-
* site can never forget the domain and silently ship an unoptimized image.
|
|
158
|
-
*/
|
|
159
|
-
export declare const getCardImage: (url: string, mediaDomain?: string) => string;
|
|
160
|
-
export declare const getGalleryImage: (url: string, mediaDomain?: string) => string;
|
|
161
|
-
export declare const getThumbnailImage: (url: string, mediaDomain?: string) => string;
|
|
162
|
-
export declare const getHeroImage: (url: string, mediaDomain?: string) => string;
|
|
163
|
-
export declare const getCategoryImage: (url: string, mediaDomain?: string) => string;
|
|
164
|
-
export declare const getLogoImage: (url: string, mediaDomain?: string) => string;
|
|
165
|
-
/**
|
|
166
|
-
* Generate responsive image srcset for different screen sizes
|
|
167
|
-
*
|
|
168
|
-
* @param originalUrl - The original image URL
|
|
169
|
-
* @param baseWidth - Base width for 1x display
|
|
170
|
-
* @param baseHeight - Base height for 1x display
|
|
171
|
-
* @param mediaDomain - Media domain
|
|
172
|
-
* @returns srcset string for responsive images
|
|
173
|
-
*
|
|
174
|
-
* @example
|
|
175
|
-
* ```ts
|
|
176
|
-
* const srcset = generateSrcSet(
|
|
177
|
-
* 'https://media.example.com/products/image.jpg',
|
|
178
|
-
* 400,
|
|
179
|
-
* 400,
|
|
180
|
-
* 'media.example.com'
|
|
181
|
-
* );
|
|
182
|
-
* // Returns: "url 1x, url 1.5x, url 2x"
|
|
183
|
-
* ```
|
|
184
|
-
*/
|
|
185
|
-
export declare function generateSrcSet(originalUrl: string, baseWidth: number, baseHeight: number, mediaDomain?: string): string;
|
|
186
|
-
/**
|
|
187
|
-
* Bind a media domain once and get the whole image toolkit back.
|
|
188
|
-
*
|
|
189
|
-
* The single binding point for theme code: a media domain is configured once
|
|
190
|
-
* (an env var, a store setting), and every helper then takes only the URL. A
|
|
191
|
-
* per-call domain still wins, so a theme with two media hosts can override.
|
|
192
|
-
*
|
|
193
|
-
* With no domain bound every helper is a pass-through — the correct behavior
|
|
194
|
-
* for a store that has no media domain configured (original R2 URLs).
|
|
195
|
-
*
|
|
196
|
-
* @param mediaDomain - Media domain (e.g., "media.example.com" or "https://media.example.com")
|
|
197
|
-
*
|
|
198
|
-
* @example
|
|
199
|
-
* ```ts
|
|
200
|
-
* const images = createImageHelpers(env.MEDIA_DOMAIN);
|
|
201
|
-
* images.getThumbnailImage(product.coverImage.src);
|
|
202
|
-
* images.getCardImage(url);
|
|
203
|
-
* images.generateSrcSet(url, 400, 400);
|
|
204
|
-
* ```
|
|
205
|
-
*/
|
|
206
|
-
export interface ImageHelpers {
|
|
207
|
-
/** `optimizeImage` with the media domain bound. */
|
|
208
|
-
optimizeImage: (originalUrl: string, options: ImageResizeOptions, mediaDomain?: string) => string;
|
|
209
|
-
/** `generateSrcSet` with the media domain bound. */
|
|
210
|
-
generateSrcSet: (originalUrl: string, baseWidth: number, baseHeight: number, mediaDomain?: string) => string;
|
|
211
|
-
/** The shared size presets, for callers that want the raw dimensions. */
|
|
212
|
-
IMAGE_SIZES: typeof IMAGE_SIZES;
|
|
213
|
-
getCardImage: (url: string) => string;
|
|
214
|
-
getGalleryImage: (url: string) => string;
|
|
215
|
-
getThumbnailImage: (url: string) => string;
|
|
216
|
-
getHeroImage: (url: string) => string;
|
|
217
|
-
getCategoryImage: (url: string) => string;
|
|
218
|
-
getLogoImage: (url: string) => string;
|
|
219
|
-
}
|
|
220
|
-
export declare function createImageHelpers(mediaDomain?: string): ImageHelpers;
|
|
101
|
+
export declare function pictureOf(image: PickedImage | null | undefined, delivery: MediaDelivery | null | undefined): Picture | null;
|