@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/dist/schemas.js CHANGED
@@ -1,10 +1,17 @@
1
1
  import { z } from "zod";
2
- export const ProductImageSchema = z.object({
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
- imageUrl: z.string().url().nullable(),
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
- logoUrl: z.string().nullable(),
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: z.string().nullable(),
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 = z.object({
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.src);
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
- logoUrl: string | null;
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
- ogImage: string | null;
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}.
@@ -1,88 +1,74 @@
1
1
  /**
2
- * Cloudflare Image Resizing Utility
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
- * Generates optimized image URLs using Cloudflare's Image Resizing service.
5
- * Automatically resizes images to exact dimensions and converts to WebP format.
6
- *
7
- * Deep module: hides Cloudflare CDN-CGI URL construction, fit modes, format
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 interface ImageResizeOptions {
11
- width: number;
12
- height: number;
13
- quality?: number;
14
- format?: "webp" | "avif" | "auto";
15
- fit?: "scale-down" | "contain" | "cover" | "crop" | "pad";
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
- * Generate an optimized image URL using Cloudflare Image Resizing
19
- *
20
- * @param originalUrl - The original image URL from R2
21
- * @param options - Resize and optimization options
22
- * @param mediaDomain - Media domain (e.g., "media.example.com" or "https://media.example.com")
23
- * @returns Optimized image URL
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 Cloudflare Image Resizing URL of an image on the media domain, served
46
- * by the media domain itself (`https://<media>/cdn-cgi/image/<options>/<path>`)
47
- * — so it works wherever the storefront is hosted, as long as the media
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
- * `onerror=redirect` comes first: when a resize fails, Cloudflare redirects to
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 mediaImageUrl(originalUrl: string, params: MediaImageParams, mediaDomain?: string): string | null;
45
+ export declare function mediaUrl(key: string, delivery: MediaDelivery | null | undefined): string | null;
56
46
  /**
57
- * The public URL of an object in the store's media bucket, from its key —
58
- * what a theme's `image` field stores (`products/3f2a9c.webp`). The key is
59
- * the image's identity; the URL depends on where the store serves its media,
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` when there is no media domain, or the key cannot name an
64
- * object: empty, too long, an empty, `.` or `..` segment (a leading slash
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 mediaUrl(key: string, mediaDomain?: string): string | null;
54
+ export declare function mediaImageUrl(url: string, params: MediaImageParams, delivery: MediaDelivery | null | undefined): string | null;
68
55
  export interface MediaImageSrcsetOptions {
69
- /** The store's media domain; without one there is no srcset. */
70
- mediaDomain?: string;
71
- /** Height ÷ width: given, each size is cropped to it (`fit=cover`); absent, the photo keeps its shape (`fit=scale-down`). */
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; Cloudflare's default when absent. */
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. Each size is converted to the
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 there is no media domain, the URL is not on it, or no
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?: MediaImageSrcsetOptions): string | null;
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 on the store's media domain,
109
- * its alt text (empty when the merchant wrote none), its size, and its focal
110
- * point as `object-position`, so a cropped photo keeps what matters in view.
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 cannot name an object, or
113
- * the store has no media domain.
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 const IMAGE_SIZES: {
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;