@jay-framework/wix-stores 0.15.1 → 0.15.5

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/index.js CHANGED
@@ -1,7 +1,6 @@
1
- import { getCurrentCartClient } from "@jay-framework/wix-cart";
2
1
  import { WIX_CART_CONTEXT, WIX_CART_SERVICE, cartIndicator, cartPage, provideWixCartContext, provideWixCartService } from "@jay-framework/wix-cart";
3
2
  import { createJayService, makeJayQuery, ActionError, makeJayStackComponent, RenderPipeline, makeJayInit } from "@jay-framework/fullstack-component";
4
- import { inventoryItemsV3, productsV3 } from "@wix/stores";
3
+ import { customizationsV3, inventoryItemsV3, productsV3 } from "@wix/stores";
5
4
  import { categories } from "@wix/categories";
6
5
  import { registerService, getService } from "@jay-framework/stack-server-runtime";
7
6
  import { createJayContext } from "@jay-framework/runtime";
@@ -10,6 +9,11 @@ import { WIX_CLIENT_SERVICE } from "@jay-framework/wix-server-client";
10
9
  import * as fs from "fs";
11
10
  import * as path from "path";
12
11
  import * as yaml from "js-yaml";
12
+ var OptionRenderType$2 = /* @__PURE__ */ ((OptionRenderType2) => {
13
+ OptionRenderType2[OptionRenderType2["TEXT_CHOICES"] = 0] = "TEXT_CHOICES";
14
+ OptionRenderType2[OptionRenderType2["SWATCH_CHOICES"] = 1] = "SWATCH_CHOICES";
15
+ return OptionRenderType2;
16
+ })(OptionRenderType$2 || {});
13
17
  var CurrentSort = /* @__PURE__ */ ((CurrentSort2) => {
14
18
  CurrentSort2[CurrentSort2["relevance"] = 0] = "relevance";
15
19
  CurrentSort2[CurrentSort2["priceAsc"] = 1] = "priceAsc";
@@ -22,7 +26,8 @@ var CurrentSort = /* @__PURE__ */ ((CurrentSort2) => {
22
26
  const instances = {
23
27
  productsV3ClientInstance: void 0,
24
28
  categoriesClientInstance: void 0,
25
- inventoryV3ClientInstance: void 0
29
+ inventoryV3ClientInstance: void 0,
30
+ customizationsV3ClientInstance: void 0
26
31
  };
27
32
  function getProductsV3Client(wixClient) {
28
33
  if (!instances.productsV3ClientInstance) {
@@ -42,15 +47,70 @@ function getInventoryClient(wixClient) {
42
47
  }
43
48
  return instances.inventoryV3ClientInstance;
44
49
  }
50
+ function getCustomizationsV3Client(wixClient) {
51
+ if (!instances.customizationsV3ClientInstance) {
52
+ instances.customizationsV3ClientInstance = wixClient.use(customizationsV3);
53
+ }
54
+ return instances.customizationsV3ClientInstance;
55
+ }
45
56
  const WIX_STORES_SERVICE_MARKER = createJayService("Wix Store Service");
46
57
  function provideWixStoresService(wixClient, options) {
58
+ let cachedTree = null;
59
+ let cachedCustomizations = null;
60
+ const categoriesClient = getCategoriesClient(wixClient);
61
+ const customizationsClient = getCustomizationsV3Client(wixClient);
47
62
  const service = {
48
63
  products: getProductsV3Client(wixClient),
49
- categories: getCategoriesClient(wixClient),
64
+ categories: categoriesClient,
50
65
  inventory: getInventoryClient(wixClient),
51
- // Keep cart for backward compatibility, but prefer WIX_CART_SERVICE
52
- cart: getCurrentCartClient(wixClient),
53
- categoryPrefixes: options?.categoryPrefixes ?? []
66
+ customizations: customizationsClient,
67
+ urls: options?.urls ?? { product: "/products/{slug}", category: null },
68
+ defaultCategory: options?.defaultCategory ?? null,
69
+ async getCategoryTree() {
70
+ if (cachedTree) return cachedTree;
71
+ const slugMap = /* @__PURE__ */ new Map();
72
+ const parentMap = /* @__PURE__ */ new Map();
73
+ const rootIds = /* @__PURE__ */ new Set();
74
+ const imageMap = /* @__PURE__ */ new Map();
75
+ try {
76
+ const processItems = (items) => {
77
+ for (const cat of items) {
78
+ if (!cat._id || !cat.slug) continue;
79
+ slugMap.set(cat._id, cat.slug);
80
+ if (cat.parentCategory?._id) {
81
+ parentMap.set(cat._id, cat.parentCategory._id);
82
+ } else {
83
+ rootIds.add(cat._id);
84
+ }
85
+ const imageUrl = cat.media?.mainMedia?.image?.url || cat.media?.mainMedia?.url;
86
+ if (imageUrl) {
87
+ imageMap.set(cat._id, imageUrl);
88
+ }
89
+ }
90
+ };
91
+ let result = await categoriesClient.queryCategories({ treeReference: { appNamespace: "@wix/stores" } }).eq("visible", true).limit(100).find();
92
+ processItems(result.items || []);
93
+ while (result.hasNext()) {
94
+ result = await result.next();
95
+ processItems(result.items || []);
96
+ }
97
+ } catch (error) {
98
+ console.error("[wix-stores] Failed to build category tree:", error);
99
+ }
100
+ cachedTree = { slugMap, parentMap, rootIds, imageMap };
101
+ return cachedTree;
102
+ },
103
+ async getCustomizations() {
104
+ if (cachedCustomizations) return cachedCustomizations;
105
+ try {
106
+ const result = await customizationsClient.queryCustomizations().eq("customizationType", "PRODUCT_OPTION").limit(100).find();
107
+ cachedCustomizations = result.items || [];
108
+ } catch (error) {
109
+ console.error("[wix-stores] Failed to load customizations:", error);
110
+ cachedCustomizations = [];
111
+ }
112
+ return cachedCustomizations;
113
+ }
54
114
  };
55
115
  registerService(WIX_STORES_SERVICE_MARKER, service);
56
116
  return service;
@@ -80,7 +140,8 @@ var ProductType$1 = /* @__PURE__ */ ((ProductType2) => {
80
140
  var QuickAddType = /* @__PURE__ */ ((QuickAddType2) => {
81
141
  QuickAddType2[QuickAddType2["SIMPLE"] = 0] = "SIMPLE";
82
142
  QuickAddType2[QuickAddType2["SINGLE_OPTION"] = 1] = "SINGLE_OPTION";
83
- QuickAddType2[QuickAddType2["NEEDS_CONFIGURATION"] = 2] = "NEEDS_CONFIGURATION";
143
+ QuickAddType2[QuickAddType2["COLOR_AND_TEXT_OPTIONS"] = 2] = "COLOR_AND_TEXT_OPTIONS";
144
+ QuickAddType2[QuickAddType2["NEEDS_CONFIGURATION"] = 3] = "NEEDS_CONFIGURATION";
84
145
  return QuickAddType2;
85
146
  })(QuickAddType || {});
86
147
  var OptionRenderType$1 = /* @__PURE__ */ ((OptionRenderType2) => {
@@ -136,33 +197,54 @@ function formatWixMediaUrl(_id, url, resize) {
136
197
  }
137
198
  return "";
138
199
  }
139
- function resolveProductPrefix(product, prefixConfig) {
140
- if (!prefixConfig?.length || !product.allCategoriesInfo?.categories) {
141
- return null;
142
- }
143
- const productCategoryIds = new Set(
144
- product.allCategoriesInfo.categories.map((c) => c._id).filter(Boolean)
145
- );
146
- for (const { categoryId, prefix } of prefixConfig) {
147
- if (productCategoryIds.has(categoryId)) {
148
- return prefix;
200
+ function findRootCategoryId(categoryId, tree) {
201
+ let current = categoryId;
202
+ for (let depth = 0; depth < 20; depth++) {
203
+ if (tree.rootIds.has(current)) {
204
+ return current;
149
205
  }
206
+ const parentId = tree.parentMap.get(current);
207
+ if (!parentId) return current;
208
+ current = parentId;
150
209
  }
151
- return null;
210
+ return current;
152
211
  }
153
- function resolveProductPrefixConfig(product, prefixConfig) {
154
- if (!prefixConfig?.length || !product.allCategoriesInfo?.categories) {
155
- return null;
212
+ function findRootCategorySlug(categoryId, tree) {
213
+ const rootId = findRootCategoryId(categoryId, tree);
214
+ return tree.slugMap.get(rootId) ?? "";
215
+ }
216
+ function findCategoryImage(categoryId, tree) {
217
+ let current = categoryId;
218
+ for (let depth = 0; depth < 20 && current; depth++) {
219
+ const image = tree.imageMap.get(current);
220
+ if (image) return image;
221
+ current = tree.parentMap.get(current);
156
222
  }
157
- const productCategoryIds = new Set(
158
- product.allCategoriesInfo.categories.map((c) => c._id).filter(Boolean)
159
- );
160
- for (const config of prefixConfig) {
161
- if (productCategoryIds.has(config.categoryId)) {
162
- return config;
163
- }
223
+ return "";
224
+ }
225
+ function buildProductUrl(urls, tree, slug, mainCategoryId) {
226
+ let url = urls.product;
227
+ url = url.replace("{slug}", slug);
228
+ if (url.includes("{category}")) {
229
+ const categorySlug = tree.slugMap.get(mainCategoryId);
230
+ url = url.replace("{category}", categorySlug);
164
231
  }
165
- return null;
232
+ if (url.includes("{prefix}")) {
233
+ const prefixSlug = findRootCategorySlug(mainCategoryId, tree);
234
+ url = url.replace("{prefix}", prefixSlug);
235
+ }
236
+ return url;
237
+ }
238
+ function buildCategoryUrl(urls, tree, categorySlug, categoryId) {
239
+ if (!urls.category) return null;
240
+ let url = urls.category;
241
+ url = url.replace("{category}", categorySlug);
242
+ if (url.includes("{prefix}")) {
243
+ const prefixSlug = findRootCategorySlug(categoryId, tree);
244
+ if (!prefixSlug) return null;
245
+ url = url.replace("{prefix}", prefixSlug);
246
+ }
247
+ return url.includes("{") ? null : url;
166
248
  }
167
249
  function mapAvailabilityStatus(status) {
168
250
  switch (status) {
@@ -198,7 +280,15 @@ function isValidPrice(amount) {
198
280
  function getQuickAddType(product) {
199
281
  const optionCount = product.options?.length ?? 0;
200
282
  const hasModifiers = (product.modifiers?.length ?? 0) > 0;
201
- if (hasModifiers || optionCount > 1) {
283
+ if (hasModifiers || optionCount > 2) {
284
+ return QuickAddType.NEEDS_CONFIGURATION;
285
+ }
286
+ if (optionCount === 2) {
287
+ const hasColor = product.options.some((o) => o.optionRenderType === "SWATCH_CHOICES");
288
+ const hasText = product.options.some((o) => o.optionRenderType === "TEXT_CHOICES");
289
+ if (hasColor && hasText) {
290
+ return QuickAddType.COLOR_AND_TEXT_OPTIONS;
291
+ }
202
292
  return QuickAddType.NEEDS_CONFIGURATION;
203
293
  }
204
294
  if (optionCount === 1) {
@@ -214,31 +304,85 @@ function mapChoiceType(choiceType) {
214
304
  }
215
305
  function mapQuickOption(option, variantsInfo) {
216
306
  if (!option) return null;
217
- const optionId = option._id;
218
307
  const choices = option.choicesSettings?.choices || [];
219
308
  return {
220
- _id: optionId,
309
+ _id: option._id || "",
221
310
  name: option.name || "",
222
311
  optionRenderType: mapOptionRenderType(option.optionRenderType),
223
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
224
- choices: choices.map((choice) => {
225
- return {
226
- choiceId: choice.choiceId,
227
- name: choice.name || "",
228
- choiceType: mapChoiceType(choice.choiceType),
229
- colorCode: choice.colorCode || "",
230
- inStock: choice.inStock,
231
- isSelected: false
232
- };
233
- })
312
+ choices: choices.map((choice) => ({
313
+ choiceId: choice.choiceId || "",
314
+ name: choice.name || "",
315
+ choiceType: mapChoiceType(choice.choiceType),
316
+ colorCode: choice.colorCode || "",
317
+ inStock: choice.inStock ?? true,
318
+ isSelected: false
319
+ }))
234
320
  };
235
321
  }
236
- const DEFAULT_PRODUCT_PAGE_PATH = "/products";
237
- function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH, prefixConfig) {
322
+ function mapQuickAddOptions(product) {
323
+ const quickAddType = getQuickAddType(product);
324
+ if (quickAddType === QuickAddType.COLOR_AND_TEXT_OPTIONS) {
325
+ const colorOption = product.options.find((o) => o.optionRenderType === "SWATCH_CHOICES");
326
+ const textOption = product.options.find((o) => o.optionRenderType === "TEXT_CHOICES");
327
+ const quickOption = mapQuickOption(colorOption, product.variantsInfo);
328
+ const secondQuickOption = mapQuickOption(textOption, product.variantsInfo);
329
+ if (quickOption?.choices) {
330
+ const firstInStock = quickOption.choices.find((c) => c.inStock);
331
+ if (firstInStock) {
332
+ firstInStock.isSelected = true;
333
+ }
334
+ }
335
+ if (secondQuickOption?.choices) {
336
+ for (const choice of secondQuickOption.choices) {
337
+ choice.inStock = false;
338
+ }
339
+ }
340
+ return { quickAddType, quickOption, secondQuickOption };
341
+ }
342
+ if (quickAddType === QuickAddType.SINGLE_OPTION) {
343
+ return {
344
+ quickAddType,
345
+ quickOption: mapQuickOption(product.options?.[0], product.variantsInfo),
346
+ secondQuickOption: null
347
+ };
348
+ }
349
+ return { quickAddType, quickOption: null, secondQuickOption: null };
350
+ }
351
+ function buildVariantStockMap(product) {
352
+ const stockMap = {};
353
+ const options = product.options;
354
+ const variants = product.variantsInfo?.variants;
355
+ if (!options || options.length !== 2 || !variants) return stockMap;
356
+ const colorOption = options.find((o) => o.optionRenderType === "SWATCH_CHOICES");
357
+ const textOption = options.find((o) => o.optionRenderType === "TEXT_CHOICES");
358
+ if (!colorOption || !textOption) return stockMap;
359
+ const colorOptionId = colorOption._id || "";
360
+ const textOptionId = textOption._id || "";
361
+ const colorChoices = colorOption.choicesSettings?.choices || [];
362
+ const textChoices = textOption.choicesSettings?.choices || [];
363
+ for (const colorChoice of colorChoices) {
364
+ const cId = colorChoice.choiceId || "";
365
+ stockMap[cId] = {};
366
+ for (const textChoice of textChoices) {
367
+ const tId = textChoice.choiceId || "";
368
+ const variant = variants.find(
369
+ (v) => v.choices?.some(
370
+ (c) => c.optionChoiceIds?.optionId === colorOptionId && c.optionChoiceIds?.choiceId === cId
371
+ ) && v.choices?.some(
372
+ (c) => c.optionChoiceIds?.optionId === textOptionId && c.optionChoiceIds?.choiceId === tId
373
+ )
374
+ );
375
+ stockMap[cId][tId] = variant?.inventoryStatus?.inStock ?? false;
376
+ }
377
+ }
378
+ return stockMap;
379
+ }
380
+ function mapProductToCard(product, urls, tree) {
238
381
  const mainMedia = product.media?.main;
239
382
  const slug = product.slug || "";
240
- const matchedPrefix = prefixConfig?.length ? resolveProductPrefixConfig(product, prefixConfig) : null;
241
- const productUrl = slug ? matchedPrefix ? `${productPagePath}/${matchedPrefix.prefix}/${slug}` : `${productPagePath}/${slug}` : "";
383
+ const mainCategoryId = product.mainCategoryId || "";
384
+ const productUrl = buildProductUrl(urls, tree, slug, mainCategoryId);
385
+ const categoryName = tree.slugMap.get(mainCategoryId);
242
386
  const firstVariant = product.variantsInfo?.variants?.[0];
243
387
  const variantPrice = firstVariant?.price;
244
388
  const actualAmount = variantPrice?.actualPrice?.amount || product.actualPriceRange?.minValue?.amount || "0";
@@ -251,7 +395,7 @@ function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH,
251
395
  name: product.name || "",
252
396
  slug,
253
397
  productUrl,
254
- categoryPrefix: matchedPrefix?.name ?? "",
398
+ categoryPrefix: categoryName,
255
399
  mainMedia: {
256
400
  url: mainMedia ? formatWixMediaUrl(mainMedia._id, mainMedia.url) : "",
257
401
  altText: mainMedia?.altText || product.name || "",
@@ -263,7 +407,6 @@ function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH,
263
407
  width: 300,
264
408
  height: 300
265
409
  },
266
- // Simplified price fields
267
410
  price: actualFormattedAmount,
268
411
  strikethroughPrice: hasDiscount ? compareAtFormattedAmount : "",
269
412
  hasDiscount,
@@ -282,11 +425,13 @@ function mapProductToCard(product, productPagePath = DEFAULT_PRODUCT_PAGE_PATH,
282
425
  },
283
426
  productType: mapProductType$1(product.productType),
284
427
  isAddingToCart: false,
285
- // Quick add behavior
286
- quickAddType: getQuickAddType(product),
287
- quickOption: getQuickAddType(product) === QuickAddType.SINGLE_OPTION ? mapQuickOption(product.options?.[0], product.variantsInfo) : null
428
+ ...mapQuickAddOptions(product)
288
429
  };
289
430
  }
431
+ function needsCategoryInfo(wixStores) {
432
+ const template = wixStores.urls.product;
433
+ return template.includes("{category}") || template.includes("{prefix}");
434
+ }
290
435
  const PRICE_BUCKET_BOUNDARIES = [
291
436
  0,
292
437
  20,
@@ -307,6 +452,31 @@ const PRICE_BUCKETS = PRICE_BUCKET_BOUNDARIES.slice(0, -1).map((from, i) => ({
307
452
  to: PRICE_BUCKET_BOUNDARIES[i + 1]
308
453
  }));
309
454
  PRICE_BUCKETS.push({ from: PRICE_BUCKET_BOUNDARIES[PRICE_BUCKET_BOUNDARIES.length - 1] });
455
+ function getAvailableProductOptions(aggResults, customizations) {
456
+ const optionNamesAgg = aggResults.find((a) => a.name === "optionNames")?.values;
457
+ const choiceNamesAgg = aggResults.find((a) => a.name === "choiceNames")?.values;
458
+ if (!optionNamesAgg?.results?.length || !choiceNamesAgg?.results?.length) {
459
+ return [];
460
+ }
461
+ const optionNames = new Set(optionNamesAgg.results.map((e) => e.value));
462
+ const choiceCounts = new Map(
463
+ choiceNamesAgg.results.map((e) => [e.value.toLowerCase(), e.count ?? 0])
464
+ );
465
+ return customizations.filter(
466
+ (c) => c.customizationType === "PRODUCT_OPTION" && c.name && optionNames.has(c.name) && (c.customizationRenderType === "TEXT_CHOICES" || c.customizationRenderType === "SWATCH_CHOICES")
467
+ ).map((c) => ({
468
+ optionId: c._id || "",
469
+ optionName: c.name || "",
470
+ optionRenderType: c.customizationRenderType,
471
+ // Sort by product count descending (most used choices first)
472
+ choices: (c.choicesSettings?.choices || []).filter((ch) => ch.name && choiceCounts.has(ch.name.toLowerCase())).map((ch) => ({
473
+ choiceId: ch._id || "",
474
+ choiceName: ch.name || "",
475
+ colorCode: ch.colorCode || "",
476
+ productCount: choiceCounts.get(ch.name.toLowerCase()) ?? 0
477
+ })).sort((a, b) => b.productCount - a.productCount)
478
+ })).filter((o) => o.choices.length > 0);
479
+ }
310
480
  const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX_STORES_SERVICE_MARKER).withHandler(
311
481
  async (input, wixStores) => {
312
482
  const { query, filters = {}, sortBy = "relevance", cursor, pageSize = 12 } = input;
@@ -339,6 +509,22 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
339
509
  }
340
510
  });
341
511
  }
512
+ if (filters.optionFilters && filters.optionFilters.length > 0) {
513
+ for (const optFilter of filters.optionFilters) {
514
+ if (optFilter.choiceNames.length > 0) {
515
+ filterConditions.push({
516
+ $and: [
517
+ { "options.name": { $hasSome: [optFilter.optionName] } },
518
+ {
519
+ "options.choicesSettings.choices.name": {
520
+ $hasSome: optFilter.choiceNames
521
+ }
522
+ }
523
+ ]
524
+ });
525
+ }
526
+ }
527
+ }
342
528
  const filter = filterConditions.length === 1 ? filterConditions[0] : { $and: filterConditions };
343
529
  const sort = [];
344
530
  switch (sortBy) {
@@ -392,23 +578,43 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
392
578
  name: "max-price",
393
579
  type: "SCALAR",
394
580
  scalar: { type: "MAX" }
581
+ },
582
+ // Option names for option-based filtering
583
+ {
584
+ fieldPath: "options.name",
585
+ name: "optionNames",
586
+ type: "VALUE",
587
+ value: {
588
+ limit: 20,
589
+ sortType: "VALUE",
590
+ sortDirection: "DESC"
591
+ }
592
+ },
593
+ // Choice names for option-based filtering
594
+ {
595
+ fieldPath: "options.choicesSettings.choices.name",
596
+ name: "choiceNames",
597
+ type: "VALUE",
598
+ value: {
599
+ limit: 50,
600
+ sortType: "COUNT",
601
+ sortDirection: "DESC"
602
+ }
395
603
  }
396
604
  ];
397
605
  const searchResult = await wixStores.products.searchProducts(
398
606
  {
399
607
  filter,
400
- // @ts-expect-error - Wix SDK types don't match actual API
401
608
  sort: sort.length > 0 ? sort : void 0,
402
609
  cursorPaging,
403
610
  search,
404
- // @ts-expect-error - Wix SDK types don't include aggregations
405
611
  aggregations
406
612
  },
407
613
  {
408
614
  fields: [
409
615
  "CURRENCY",
410
616
  "VARIANT_OPTION_CHOICE_NAMES",
411
- ...wixStores.categoryPrefixes.length > 0 ? ["ALL_CATEGORIES_INFO"] : []
617
+ ...needsCategoryInfo(wixStores) ? ["ALL_CATEGORIES_INFO"] : []
412
618
  ]
413
619
  }
414
620
  );
@@ -448,9 +654,11 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
448
654
  };
449
655
  });
450
656
  priceRanges.push(...bucketRanges);
451
- const prefixConfig = wixStores.categoryPrefixes;
657
+ const customizations = await wixStores.getCustomizations();
658
+ const optionFilters = getAvailableProductOptions(aggResults, customizations);
659
+ const tree = await wixStores.getCategoryTree();
452
660
  const mappedProducts = products.map(
453
- (p) => mapProductToCard(p, "/products", prefixConfig)
661
+ (p) => mapProductToCard(p, wixStores.urls, tree)
454
662
  );
455
663
  return {
456
664
  products: mappedProducts,
@@ -461,7 +669,8 @@ const searchProducts = makeJayQuery("wixStores.searchProducts").withServices(WIX
461
669
  minBound,
462
670
  maxBound,
463
671
  ranges: priceRanges
464
- }
672
+ },
673
+ optionFilters
465
674
  };
466
675
  } catch (error) {
467
676
  console.error("[wixStores.searchProducts] Search failed:", error);
@@ -476,11 +685,10 @@ const getProductBySlug = makeJayQuery("wixStores.getProductBySlug").withServices
476
685
  throw new ActionError("INVALID_INPUT", "Product slug is required");
477
686
  }
478
687
  try {
479
- const prefixConfig = wixStores.categoryPrefixes;
480
688
  const fields = [
481
689
  "MEDIA_ITEMS_INFO",
482
690
  "VARIANT_OPTION_CHOICE_NAMES",
483
- ...prefixConfig.length > 0 ? ["ALL_CATEGORIES_INFO"] : []
691
+ ...needsCategoryInfo(wixStores) ? ["ALL_CATEGORIES_INFO"] : []
484
692
  ];
485
693
  const result = await wixStores.products.getProductBySlug(slug, {
486
694
  fields: [...fields]
@@ -489,13 +697,27 @@ const getProductBySlug = makeJayQuery("wixStores.getProductBySlug").withServices
489
697
  if (!product) {
490
698
  return null;
491
699
  }
492
- return mapProductToCard(product, "/products", prefixConfig);
700
+ const tree = await wixStores.getCategoryTree();
701
+ return mapProductToCard(product, wixStores.urls, tree);
493
702
  } catch (error) {
494
703
  console.error("[wixStores.getProductBySlug] Failed to get product:", error);
495
704
  return null;
496
705
  }
497
706
  }
498
707
  );
708
+ const getVariantStock = makeJayQuery("wixStores.getVariantStock").withServices(WIX_STORES_SERVICE_MARKER).withHandler(
709
+ async (input, wixStores) => {
710
+ try {
711
+ const product = await wixStores.products.getProduct(input.productId, {
712
+ fields: ["VARIANT_OPTION_CHOICE_NAMES"]
713
+ });
714
+ return buildVariantStockMap(product);
715
+ } catch (error) {
716
+ console.error("[wixStores.getVariantStock] Failed:", error);
717
+ return {};
718
+ }
719
+ }
720
+ );
499
721
  const getCategories = makeJayQuery("wixStores.getCategories").withServices(WIX_STORES_SERVICE_MARKER).withCaching({ maxAge: 3600 }).withHandler(
500
722
  async (_input, wixStores) => {
501
723
  try {
@@ -515,11 +737,193 @@ const getCategories = makeJayQuery("wixStores.getCategories").withServices(WIX_S
515
737
  }
516
738
  );
517
739
  const PAGE_SIZE = 12;
740
+ function mapSortToAction(sort) {
741
+ switch (sort) {
742
+ case CurrentSort.priceAsc:
743
+ return "price_asc";
744
+ case CurrentSort.priceDesc:
745
+ return "price_desc";
746
+ case CurrentSort.newest:
747
+ return "newest";
748
+ case CurrentSort.nameAsc:
749
+ return "name_asc";
750
+ case CurrentSort.nameDesc:
751
+ return "name_desc";
752
+ default:
753
+ return "relevance";
754
+ }
755
+ }
756
+ function parseUrlFilters(url) {
757
+ try {
758
+ const params = new URL(url, "http://x").searchParams;
759
+ const optionSelections = /* @__PURE__ */ new Map();
760
+ const optParam = params.get("opt");
761
+ if (optParam) {
762
+ for (const segment of optParam.split(";")) {
763
+ const colonIdx = segment.indexOf(":");
764
+ if (colonIdx === -1)
765
+ continue;
766
+ const name = decodeURIComponent(segment.slice(0, colonIdx));
767
+ const choices = segment.slice(colonIdx + 1).split(",").map((c) => decodeURIComponent(c)).filter(Boolean);
768
+ if (choices.length > 0) {
769
+ optionSelections.set(name, new Set(choices));
770
+ }
771
+ }
772
+ }
773
+ return {
774
+ searchTerm: params.get("q") || "",
775
+ selectedCategorySlugs: params.get("cat")?.split(",").filter(Boolean) || [],
776
+ minPrice: params.has("min") ? Number(params.get("min")) : null,
777
+ maxPrice: params.has("max") ? Number(params.get("max")) : null,
778
+ inStockOnly: params.get("inStock") === "1",
779
+ sort: params.get("sort") || "relevance",
780
+ optionSelections
781
+ };
782
+ } catch {
783
+ return {
784
+ searchTerm: "",
785
+ selectedCategorySlugs: [],
786
+ minPrice: null,
787
+ maxPrice: null,
788
+ inStockOnly: false,
789
+ sort: "relevance",
790
+ optionSelections: /* @__PURE__ */ new Map()
791
+ };
792
+ }
793
+ }
794
+ function parseSortParam(sort) {
795
+ const sortMap = {
796
+ relevance: CurrentSort.relevance,
797
+ priceAsc: CurrentSort.priceAsc,
798
+ priceDesc: CurrentSort.priceDesc,
799
+ newest: CurrentSort.newest,
800
+ nameAsc: CurrentSort.nameAsc,
801
+ nameDesc: CurrentSort.nameDesc
802
+ };
803
+ return sortMap[sort] ?? CurrentSort.relevance;
804
+ }
805
+ function buildOptionFiltersViewState(baseOptionFilters, filteredResult, optionSelections) {
806
+ const filteredChoiceCounts = /* @__PURE__ */ new Map();
807
+ for (const opt of filteredResult.optionFilters || []) {
808
+ for (const ch of opt.choices) {
809
+ filteredChoiceCounts.set(ch.choiceName.toLowerCase(), ch.productCount);
810
+ }
811
+ }
812
+ return baseOptionFilters.map((opt) => ({
813
+ optionId: opt.optionId,
814
+ optionName: opt.optionName,
815
+ optionRenderType: opt.optionRenderType === "SWATCH_CHOICES" ? OptionRenderType$2.SWATCH_CHOICES : OptionRenderType$2.TEXT_CHOICES,
816
+ choices: opt.choices.map((ch) => {
817
+ const count = filteredChoiceCounts.get(ch.choiceName.toLowerCase()) ?? 0;
818
+ return {
819
+ choiceId: ch.choiceId,
820
+ choiceName: ch.choiceName,
821
+ colorCode: ch.colorCode,
822
+ productCount: count,
823
+ isSelected: optionSelections.get(opt.optionName)?.has(ch.choiceName) ?? false,
824
+ isDisabled: count === 0
825
+ };
826
+ })
827
+ }));
828
+ }
829
+ const EMPTY_CATEGORY_HEADER = {
830
+ name: "",
831
+ description: "",
832
+ imageUrl: "",
833
+ hasImage: false,
834
+ productCount: 0,
835
+ breadcrumbs: [],
836
+ seoData: { tags: [], settings: { preventAutoRedirect: false, keywords: [] } }
837
+ };
838
+ async function findCategoryBySlug(categoriesClient, slug) {
839
+ const result = await categoriesClient.queryCategories({ treeReference: { appNamespace: "@wix/stores" } }).eq("slug", slug).eq("visible", true).limit(1).find();
840
+ return result.items?.[0] ?? null;
841
+ }
842
+ async function loadCategoryDetails(categoriesClient, categoryId) {
843
+ try {
844
+ return await categoriesClient.getCategory(categoryId, { appNamespace: "@wix/stores" }, { fields: ["DESCRIPTION", "BREADCRUMBS_INFO"] });
845
+ } catch {
846
+ return null;
847
+ }
848
+ }
849
+ async function buildCategoryHeader(wixStoreService, category, categoryUrlTemplate) {
850
+ const details = await loadCategoryDetails(wixStoreService.categories, category._id);
851
+ const cat = details || category;
852
+ const imageUrl = cat.image || "";
853
+ const description = cat.description || "";
854
+ const categoryTree = await wixStoreService.getCategoryTree();
855
+ const breadcrumbs = (cat.breadcrumbsInfo?.breadcrumbs || []).map((b) => ({
856
+ categoryId: b.categoryId,
857
+ name: b.categoryName,
858
+ slug: b.categorySlug,
859
+ url: categoryUrlTemplate ? buildCategoryUrl(wixStoreService.urls, categoryTree, b.categorySlug, b.categoryId) : ""
860
+ }));
861
+ const seoData = cat.seoData ? {
862
+ tags: (cat.seoData.tags || []).map((tag, index) => ({
863
+ position: index.toString().padStart(2, "0"),
864
+ type: tag.type || "",
865
+ props: Object.entries(tag.props || {}).map(([key, value]) => ({
866
+ key,
867
+ value
868
+ })),
869
+ meta: Object.entries(tag.meta || {}).map(([key, value]) => ({
870
+ key,
871
+ value
872
+ })),
873
+ children: tag.children || ""
874
+ })),
875
+ settings: {
876
+ preventAutoRedirect: cat.seoData.settings?.preventAutoRedirect || false,
877
+ keywords: (cat.seoData.settings?.keywords || []).map((k) => ({
878
+ term: k.term || "",
879
+ isMain: k.isMain || false,
880
+ origin: k.origin || ""
881
+ }))
882
+ }
883
+ } : EMPTY_CATEGORY_HEADER.seoData;
884
+ let header = {
885
+ name: cat.name || "",
886
+ description,
887
+ imageUrl,
888
+ hasImage: !!imageUrl,
889
+ productCount: cat.itemCounter || 0,
890
+ breadcrumbs,
891
+ seoData
892
+ };
893
+ if ((!description || !imageUrl) && cat.parentCategory?._id) {
894
+ const parent = await loadCategoryDetails(wixStoreService.categories, cat.parentCategory._id);
895
+ if (parent) {
896
+ if (!header.description && parent.description) {
897
+ header = { ...header, description: parent.description };
898
+ }
899
+ if (!header.imageUrl) {
900
+ const parentImage = parent.image || "";
901
+ if (parentImage) {
902
+ header = { ...header, imageUrl: parentImage, hasImage: true };
903
+ }
904
+ }
905
+ }
906
+ }
907
+ return header;
908
+ }
518
909
  async function renderSlowlyChanging$2(props, wixStores) {
519
910
  const Pipeline = RenderPipeline.for();
520
- const categoryPrefix = props.category;
521
- const categoryConfig = categoryPrefix ? wixStores.categoryPrefixes.find((c) => c.prefix === categoryPrefix) : null;
522
- const baseCategoryId = categoryConfig?.categoryId ?? null;
911
+ const subcategorySlug = props.subcategory ?? null;
912
+ const categorySlug = props.category ?? null;
913
+ const defaultCategorySlug = wixStores.defaultCategory;
914
+ let activeCategory = null;
915
+ let baseCategoryId = null;
916
+ if (subcategorySlug) {
917
+ activeCategory = await findCategoryBySlug(wixStores.categories, subcategorySlug);
918
+ baseCategoryId = activeCategory?._id ?? null;
919
+ } else if (categorySlug) {
920
+ activeCategory = await findCategoryBySlug(wixStores.categories, categorySlug);
921
+ baseCategoryId = activeCategory?._id ?? null;
922
+ } else if (defaultCategorySlug) {
923
+ activeCategory = await findCategoryBySlug(wixStores.categories, defaultCategorySlug);
924
+ }
925
+ const tree = await wixStores.getCategoryTree();
926
+ const categoryHeader = activeCategory ? await buildCategoryHeader(wixStores, activeCategory, wixStores.urls.category) : EMPTY_CATEGORY_HEADER;
523
927
  return Pipeline.try(async () => {
524
928
  let query = wixStores.categories.queryCategories({
525
929
  treeReference: { appNamespace: "@wix/stores" }
@@ -527,17 +931,35 @@ async function renderSlowlyChanging$2(props, wixStores) {
527
931
  if (baseCategoryId) {
528
932
  query = query.eq("parentCategory.id", baseCategoryId);
529
933
  }
530
- const categoriesResult = await query.find();
531
- return categoriesResult.items || [];
934
+ const baseCategoryIds = baseCategoryId ? [baseCategoryId] : [];
935
+ const [categoriesResult, productsResult] = await Promise.all([
936
+ query.find(),
937
+ searchProducts({
938
+ query: "",
939
+ filters: {
940
+ categoryIds: baseCategoryIds.length > 0 ? baseCategoryIds : void 0
941
+ },
942
+ pageSize: PAGE_SIZE
943
+ })
944
+ ]);
945
+ return {
946
+ categories: categoriesResult.items || [],
947
+ productsResult
948
+ };
532
949
  }).recover((error) => {
533
- console.error("Failed to load categories:", error);
534
- return Pipeline.ok([]);
535
- }).toPhaseOutput((categories2) => {
950
+ console.error("Failed to load categories/products:", error);
951
+ return Pipeline.ok({
952
+ categories: [],
953
+ productsResult: null
954
+ });
955
+ }).toPhaseOutput(({ categories: categories2, productsResult }) => {
536
956
  const categoryInfos = categories2.map((cat) => ({
537
957
  categoryId: cat._id || "",
538
958
  categoryName: cat.name || "",
539
- categorySlug: cat.slug || ""
959
+ categorySlug: cat.slug || "",
960
+ categoryUrl: buildCategoryUrl(wixStores.urls, tree, cat.slug || "", cat._id || "") ?? ""
540
961
  }));
962
+ const baseOptionFilters = productsResult?.optionFilters || [];
541
963
  return {
542
964
  viewState: {
543
965
  searchFields: "name,description,sku",
@@ -547,24 +969,45 @@ async function renderSlowlyChanging$2(props, wixStores) {
547
969
  categoryFilter: {
548
970
  categories: categoryInfos
549
971
  }
550
- }
972
+ },
973
+ categoryHeader
551
974
  },
552
975
  carryForward: {
553
976
  searchFields: "name,description,sku",
554
977
  fuzzySearch: true,
555
978
  categories: categoryInfos,
556
- baseCategoryId
979
+ baseCategoryId,
980
+ preloadedResult: productsResult,
981
+ baseOptionFilters
557
982
  }
558
983
  };
559
984
  });
560
985
  }
561
986
  async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
562
987
  const Pipeline = RenderPipeline.for();
988
+ const urlFilters = parseUrlFilters(props.url);
989
+ const initialSort = parseSortParam(urlFilters.sort);
990
+ const initialCategoryIds = urlFilters.selectedCategorySlugs.map((slug) => slowCarryForward.categories.find((c) => c.categorySlug === slug)?.categoryId).filter(Boolean);
991
+ const initialOptionFilters = [];
992
+ for (const [optionName, choiceNames] of urlFilters.optionSelections) {
993
+ initialOptionFilters.push({ optionName, choiceNames: [...choiceNames] });
994
+ }
995
+ const hasActiveFilters = !!urlFilters.searchTerm || initialCategoryIds.length > 0 || urlFilters.minPrice !== null || urlFilters.maxPrice !== null || urlFilters.inStockOnly || initialSort !== CurrentSort.relevance || initialOptionFilters.length > 0;
563
996
  return Pipeline.try(async () => {
564
- const baseCategoryIds = slowCarryForward.baseCategoryId ? [slowCarryForward.baseCategoryId] : [];
997
+ if (!hasActiveFilters && slowCarryForward.preloadedResult) {
998
+ return slowCarryForward.preloadedResult;
999
+ }
1000
+ const baseCategoryIds = slowCarryForward.baseCategoryId ? [slowCarryForward.baseCategoryId, ...initialCategoryIds] : initialCategoryIds;
565
1001
  const result = await searchProducts({
566
- query: "",
567
- filters: baseCategoryIds.length > 0 ? { categoryIds: baseCategoryIds } : void 0,
1002
+ query: urlFilters.searchTerm || "",
1003
+ filters: {
1004
+ categoryIds: baseCategoryIds.length > 0 ? baseCategoryIds : void 0,
1005
+ minPrice: urlFilters.minPrice ?? void 0,
1006
+ maxPrice: urlFilters.maxPrice ?? void 0,
1007
+ inStockOnly: urlFilters.inStockOnly || void 0,
1008
+ optionFilters: initialOptionFilters.length > 0 ? initialOptionFilters : void 0
1009
+ },
1010
+ sortBy: initialSort !== CurrentSort.relevance ? mapSortToAction(initialSort) : void 0,
568
1011
  pageSize: PAGE_SIZE
569
1012
  });
570
1013
  return result;
@@ -582,13 +1025,14 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
582
1025
  {
583
1026
  rangeId: "all",
584
1027
  label: "Show all",
585
- minValue: null,
586
- maxValue: null,
1028
+ minValue: 0,
1029
+ maxValue: 1e3,
587
1030
  productCount: 0,
588
1031
  isSelected: true
589
1032
  }
590
1033
  ]
591
- }
1034
+ },
1035
+ optionFilters: []
592
1036
  });
593
1037
  }).toPhaseOutput((result) => {
594
1038
  const priceAgg = result.priceAggregation || {
@@ -598,8 +1042,8 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
598
1042
  {
599
1043
  rangeId: "all",
600
1044
  label: "Show all",
601
- minValue: null,
602
- maxValue: null,
1045
+ minValue: 0,
1046
+ maxValue: 1e3,
603
1047
  productCount: result.totalCount,
604
1048
  isSelected: true
605
1049
  }
@@ -607,33 +1051,37 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
607
1051
  };
608
1052
  return {
609
1053
  viewState: {
610
- searchExpression: "",
1054
+ searchExpression: urlFilters.searchTerm,
611
1055
  isSearching: false,
612
- hasSearched: false,
1056
+ hasSearched: !!urlFilters.searchTerm,
613
1057
  searchResults: result.products,
614
1058
  resultCount: result.products.length,
615
1059
  hasResults: result.products.length > 0,
616
1060
  hasSuggestions: false,
617
1061
  suggestions: [],
618
1062
  filters: {
619
- inStockOnly: false,
1063
+ inStockOnly: urlFilters.inStockOnly,
620
1064
  priceRange: {
621
- // Initialize sliders to full range (bounds)
622
- minPrice: priceAgg.minBound,
623
- maxPrice: priceAgg.maxBound,
1065
+ minPrice: urlFilters.minPrice ?? priceAgg.minBound,
1066
+ maxPrice: urlFilters.maxPrice ?? priceAgg.maxBound,
624
1067
  minBound: priceAgg.minBound,
625
1068
  maxBound: priceAgg.maxBound,
626
- ranges: priceAgg.ranges
1069
+ ranges: priceAgg.ranges.map((r) => ({
1070
+ ...r,
1071
+ minValue: r.minValue ?? 0,
1072
+ maxValue: r.maxValue ?? 0
1073
+ }))
627
1074
  },
628
1075
  categoryFilter: {
629
1076
  categories: slowCarryForward.categories.map((cat) => ({
630
1077
  categoryId: cat.categoryId,
631
- isSelected: false
1078
+ isSelected: initialCategoryIds.includes(cat.categoryId)
632
1079
  }))
633
- }
1080
+ },
1081
+ optionFilters: buildOptionFiltersViewState(slowCarryForward.baseOptionFilters, result, urlFilters.optionSelections)
634
1082
  },
635
1083
  sortBy: {
636
- currentSort: CurrentSort.relevance
1084
+ currentSort: initialSort
637
1085
  },
638
1086
  hasMore: result.hasMore,
639
1087
  loadedCount: result.products.length,
@@ -643,12 +1091,35 @@ async function renderFastChanging$1(props, slowCarryForward, _wixStores) {
643
1091
  searchFields: slowCarryForward.searchFields,
644
1092
  fuzzySearch: slowCarryForward.fuzzySearch,
645
1093
  categories: slowCarryForward.categories,
646
- baseCategoryId: slowCarryForward.baseCategoryId
1094
+ baseCategoryId: slowCarryForward.baseCategoryId,
1095
+ baseOptionFilters: slowCarryForward.baseOptionFilters
647
1096
  }
648
1097
  };
649
1098
  });
650
1099
  }
651
- const productSearch = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withSlowlyRender(renderSlowlyChanging$2).withFastRender(renderFastChanging$1);
1100
+ async function* loadSearchParams([wixStores]) {
1101
+ try {
1102
+ const result = await wixStores.categories.queryCategories({
1103
+ treeReference: { appNamespace: "@wix/stores" }
1104
+ }).eq("visible", true).limit(100).find();
1105
+ const categories2 = result.items || [];
1106
+ yield categories2.filter((cat) => cat.slug && (cat.itemCounter ?? 0) > 0).map((cat) => ({
1107
+ category: cat.slug
1108
+ }));
1109
+ for (const cat of categories2) {
1110
+ if (!cat.slug || !cat.parentCategory?._id || (cat.itemCounter ?? 0) === 0)
1111
+ continue;
1112
+ const parent = categories2.find((c) => c._id === cat.parentCategory?._id);
1113
+ if (parent?.slug) {
1114
+ yield [{ category: parent.slug, subcategory: cat.slug }];
1115
+ }
1116
+ }
1117
+ } catch (error) {
1118
+ console.error("Failed to load category params:", error);
1119
+ yield [];
1120
+ }
1121
+ }
1122
+ const productSearch = makeJayStackComponent().withProps().withServices(WIX_STORES_SERVICE_MARKER).withLoadParams(loadSearchParams).withSlowlyRender(renderSlowlyChanging$2).withFastRender(renderFastChanging$1);
652
1123
  var ProductType = /* @__PURE__ */ ((ProductType2) => {
653
1124
  ProductType2[ProductType2["PHYSICAL"] = 0] = "PHYSICAL";
654
1125
  ProductType2[ProductType2["DIGITAL"] = 1] = "DIGITAL";
@@ -691,28 +1162,21 @@ var MediaType = /* @__PURE__ */ ((MediaType2) => {
691
1162
  return MediaType2;
692
1163
  })(MediaType || {});
693
1164
  async function* loadProductParams([wixStores]) {
694
- const prefixConfig = wixStores.categoryPrefixes;
695
- const hasPrefixes = prefixConfig.length > 0;
696
- const fields = hasPrefixes ? ["ALL_CATEGORIES_INFO"] : [];
1165
+ const template = wixStores.urls.product;
1166
+ const needsCategories = template.includes("{category}") || template.includes("{prefix}");
1167
+ const fields = needsCategories ? ["ALL_CATEGORIES_INFO"] : [];
697
1168
  try {
698
1169
  let result = await wixStores.products.queryProducts({ fields: [...fields] }).find();
699
- yield result.items.map((product) => mapProductToParams(product, prefixConfig));
1170
+ yield result.items.map((product) => ({ slug: product.slug ?? "" })).filter((p) => p.slug);
700
1171
  while (result.hasNext()) {
701
1172
  result = await result.next();
702
- yield result.items.map((product) => mapProductToParams(product, prefixConfig));
1173
+ yield result.items.map((product) => ({ slug: product.slug ?? "" })).filter((p) => p.slug);
703
1174
  }
704
1175
  } catch (error) {
705
1176
  console.error("Failed to load product slugs:", error);
706
1177
  yield [];
707
1178
  }
708
1179
  }
709
- function mapProductToParams(product, prefixConfig) {
710
- const prefix = resolveProductPrefix(product, prefixConfig);
711
- return {
712
- slug: product.slug ?? "",
713
- ...prefix ? { category: prefix } : {}
714
- };
715
- }
716
1180
  function mapProductType(productType) {
717
1181
  return productType === "DIGITAL" ? ProductType.DIGITAL : ProductType.PHYSICAL;
718
1182
  }
@@ -848,8 +1312,8 @@ function mapVariants(variantsInfo) {
848
1312
  }
849
1313
  async function renderSlowlyChanging$1(props, wixStores) {
850
1314
  const Pipeline = RenderPipeline.for();
851
- const prefixConfig = wixStores.categoryPrefixes;
852
- const hasPrefixes = prefixConfig.length > 0;
1315
+ const template = wixStores.urls.product;
1316
+ const needsCategories = template.includes("{category}") || template.includes("{prefix}");
853
1317
  return Pipeline.try(async () => {
854
1318
  const fields = [
855
1319
  "INFO_SECTION",
@@ -857,17 +1321,11 @@ async function renderSlowlyChanging$1(props, wixStores) {
857
1321
  "MEDIA_ITEMS_INFO",
858
1322
  "PLAIN_DESCRIPTION",
859
1323
  "CURRENCY",
860
- ...hasPrefixes ? ["ALL_CATEGORIES_INFO"] : []
1324
+ ...needsCategories ? ["ALL_CATEGORIES_INFO"] : []
861
1325
  ];
862
1326
  const response = await wixStores.products.getProductBySlug(props.slug, {
863
1327
  fields: [...fields]
864
1328
  });
865
- if (props.category && hasPrefixes) {
866
- const actualPrefix = resolveProductPrefix(response.product, prefixConfig);
867
- if (actualPrefix !== props.category) {
868
- throw new Error("Category prefix mismatch");
869
- }
870
- }
871
1329
  return response;
872
1330
  }).recover((error) => {
873
1331
  console.log("product page error", error);
@@ -960,37 +1418,38 @@ const categoryList = makeJayStackComponent().withProps().withServices(WIX_STORES
960
1418
  const WIX_STORES_CONTEXT = createJayContext();
961
1419
  function loadWixStoresConfig() {
962
1420
  const configPath = path.join(process.cwd(), "config", ".wix-stores.yaml");
1421
+ const defaults = {
1422
+ urls: { product: "/products/{slug}", category: null },
1423
+ defaultCategory: null
1424
+ };
963
1425
  if (!fs.existsSync(configPath)) {
964
- return { categoryPrefixes: [] };
1426
+ return defaults;
965
1427
  }
966
1428
  const fileContents = fs.readFileSync(configPath, "utf8");
967
1429
  const raw = yaml.load(fileContents);
968
1430
  if (!raw) {
969
- return { categoryPrefixes: [] };
1431
+ return defaults;
970
1432
  }
971
- const prefixes = [];
972
- if (Array.isArray(raw.categoryPrefixes)) {
973
- for (const entry of raw.categoryPrefixes) {
974
- if (typeof entry.categoryId === "string" && typeof entry.prefix === "string") {
975
- prefixes.push({
976
- categoryId: entry.categoryId.trim(),
977
- prefix: entry.prefix.trim(),
978
- name: typeof entry.name === "string" ? entry.name.trim() : entry.prefix.trim()
979
- });
980
- }
981
- }
982
- }
983
- return { categoryPrefixes: prefixes };
1433
+ const urls = raw.urls;
1434
+ return {
1435
+ urls: {
1436
+ product: typeof urls?.product === "string" ? urls.product : defaults.urls.product,
1437
+ category: typeof urls?.category === "string" ? urls.category : null
1438
+ },
1439
+ defaultCategory: typeof raw.defaultCategory === "string" ? raw.defaultCategory : null
1440
+ };
984
1441
  }
985
1442
  const init = makeJayInit().withServer(async () => {
986
1443
  console.log("[wix-stores] Initializing Wix Stores service...");
987
1444
  const wixClient = getService(WIX_CLIENT_SERVICE);
988
1445
  const storesConfig = loadWixStoresConfig();
989
1446
  provideWixStoresService(wixClient, {
990
- categoryPrefixes: storesConfig.categoryPrefixes
1447
+ urls: storesConfig.urls,
1448
+ defaultCategory: storesConfig.defaultCategory
991
1449
  });
992
- if (storesConfig.categoryPrefixes.length > 0) {
993
- console.log(`[wix-stores] Category prefixes configured: ${storesConfig.categoryPrefixes.map((p) => p.prefix).join(", ")}`);
1450
+ console.log(`[wix-stores] URL templates: product="${storesConfig.urls.product}", category="${storesConfig.urls.category ?? "none"}"`);
1451
+ if (storesConfig.defaultCategory) {
1452
+ console.log(`[wix-stores] Default category: ${storesConfig.defaultCategory}`);
994
1453
  }
995
1454
  console.log("[wix-stores] Server initialization complete");
996
1455
  return {
@@ -1001,20 +1460,22 @@ const init = makeJayInit().withServer(async () => {
1001
1460
  const CONFIG_FILE_NAME = ".wix-stores.yaml";
1002
1461
  const CONFIG_TEMPLATE = `# Wix Stores Configuration
1003
1462
  #
1004
- # Category Prefixes (optional):
1005
- # Maps root Wix categories to URL prefix slugs.
1006
- # Products under a root category get URLs like /products/{prefix}/{product-slug}
1007
- # Each prefix gets its own search/listing page and product page templates.
1463
+ # URL templates for link generation.
1464
+ # Placeholders: {slug} (product), {category} (sub-category), {prefix} (root category)
1008
1465
  #
1009
- # To find category IDs, use: jay-stack action wix-stores/getCategories
1466
+ # urls:
1467
+ # product: "/products/{slug}" # simple (default)
1468
+ # product: "/products/{category}/{slug}" # with categories
1469
+ # product: "/products/{prefix}/{category}/{slug}" # with prefixes + categories
1470
+ # category: "/products/{prefix}/{category}" # category deep-link pages
1010
1471
  #
1011
- # categoryPrefixes:
1012
- # - categoryId: "<root-category-id>"
1013
- # prefix: "<url-prefix>"
1014
- # name: "<display-name>"
1015
- # - categoryId: "<another-root-category-id>"
1016
- # prefix: "<another-url-prefix>"
1017
- # name: "<another-display-name>"
1472
+ # Fallback category for pages without category context:
1473
+ # defaultCategory: "all-products"
1474
+ #
1475
+ # To see available categories: jay-stack setup wix-stores (generates category tree reference)
1476
+
1477
+ urls:
1478
+ product: "/products/{slug}"
1018
1479
  `;
1019
1480
  async function setupWixStores(ctx) {
1020
1481
  if (ctx.initError) {
@@ -1041,8 +1502,7 @@ async function setupWixStores(ctx) {
1041
1502
  configCreated.push(`config/${CONFIG_FILE_NAME}`);
1042
1503
  }
1043
1504
  const service = getService(WIX_STORES_SERVICE_MARKER);
1044
- const prefixCount = service.categoryPrefixes.length;
1045
- const message = prefixCount > 0 ? `Wix Stores configured with ${prefixCount} category prefix(es): ${service.categoryPrefixes.map((p) => p.prefix).join(", ")}` : "Wix Stores service verified";
1505
+ const message = `Wix Stores configured (product URL: ${service.urls.product})`;
1046
1506
  return {
1047
1507
  status: "configured",
1048
1508
  message,
@@ -1091,22 +1551,13 @@ async function generateWixStoresReferences(ctx) {
1091
1551
  roots.push(node);
1092
1552
  }
1093
1553
  }
1094
- const prefixConfig = storesService.categoryPrefixes;
1095
- const configuredPrefixes = prefixConfig.map((p) => ({
1096
- categoryId: p.categoryId,
1097
- prefix: p.prefix,
1098
- name: p.name,
1099
- categoryName: nodeMap.get(p.categoryId)?.name ?? "unknown"
1100
- }));
1101
1554
  const categoriesPath = path.join(ctx.referencesDir, "categories.yaml");
1102
1555
  fs.writeFileSync(
1103
1556
  categoriesPath,
1104
1557
  yaml.dump(
1105
1558
  {
1106
- _generated: (/* @__PURE__ */ new Date()).toISOString(),
1107
- _description: "Wix Stores category tree for agent discovery. Shows category hierarchy, IDs, product counts, and configured URL prefixes.",
1559
+ _description: "Wix Stores category tree for agent discovery. Shows category hierarchy, IDs, slugs, product counts, and parent-child relationships.",
1108
1560
  totalCategories: allCategories.length,
1109
- configuredPrefixes: configuredPrefixes.length > 0 ? configuredPrefixes : void 0,
1110
1561
  categoryTree: roots
1111
1562
  },
1112
1563
  { indent: 2, lineWidth: 120, noRefs: true }
@@ -1123,12 +1574,18 @@ export {
1123
1574
  WIX_CART_SERVICE,
1124
1575
  WIX_STORES_CONTEXT,
1125
1576
  WIX_STORES_SERVICE_MARKER,
1577
+ buildCategoryUrl,
1578
+ buildProductUrl,
1126
1579
  cartIndicator,
1127
1580
  cartPage,
1128
1581
  categoryList,
1582
+ findCategoryImage,
1583
+ findRootCategoryId,
1584
+ findRootCategorySlug,
1129
1585
  generateWixStoresReferences,
1130
1586
  getCategories,
1131
1587
  getProductBySlug,
1588
+ getVariantStock,
1132
1589
  init,
1133
1590
  productPage,
1134
1591
  productSearch,