@o2vend/theme-cli 1.0.32 → 1.0.34

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.
@@ -106,23 +106,31 @@ class DevServer {
106
106
 
107
107
  // Process CSS files through Liquid to support theme variables
108
108
  // This handles CSS files that contain Liquid syntax like {{ settings.color_primary }}
109
- this.app.get('/assets/:filename.css', async (req, res, next) => {
109
+ // Supports both root assets and subdirectories: /assets/file.css and /assets/css/file.css
110
+ this.app.get('/assets/*.css', async (req, res, next) => {
110
111
  try {
111
- // Get filename from params
112
- const filename = req.params.filename + '.css';
113
- const cssPath = path.join(this.themePath, 'assets', filename);
112
+ // Extract the relative path from /assets/ (remove leading /assets/)
113
+ const relativePath = req.path.replace(/^\/assets\//, '');
114
+ const cssPath = path.join(this.themePath, 'assets', relativePath);
115
+
116
+ // Normalize path to prevent directory traversal
117
+ const normalizedPath = path.normalize(cssPath);
118
+ const assetsDir = path.normalize(path.join(this.themePath, 'assets'));
119
+ if (!normalizedPath.startsWith(assetsDir)) {
120
+ return res.status(403).send('Invalid path');
121
+ }
114
122
 
115
- if (!fs.existsSync(cssPath)) {
123
+ if (!fs.existsSync(normalizedPath)) {
116
124
  return next();
117
125
  }
118
126
 
119
- const cssContent = fs.readFileSync(cssPath, 'utf8');
127
+ const cssContent = fs.readFileSync(normalizedPath, 'utf8');
120
128
 
121
129
  // Check if CSS contains Liquid variable syntax ({{ }})
122
130
  // Only process if it contains actual Liquid variables, not just comments
123
131
  if (cssContent.includes('{{') && cssContent.includes('settings.')) {
124
132
  try {
125
- // Load settings from theme config
133
+ // Load settings from theme config (already extracts 'current' section)
126
134
  const settings = this.loadThemeSettings();
127
135
 
128
136
  // Process through Liquid
@@ -132,14 +140,14 @@ class DevServer {
132
140
  res.setHeader('Cache-Control', 'no-cache');
133
141
  return res.send(processedCss);
134
142
  } catch (liquidError) {
135
- console.error(`[CSS] Liquid processing error for ${filename}:`, liquidError.message);
143
+ console.error(`[CSS] Liquid processing error for ${relativePath}:`, liquidError.message);
136
144
  // Fall through to serve static file
137
145
  }
138
146
  }
139
147
 
140
148
  // Serve static file if no Liquid processing needed or if processing failed
141
149
  res.setHeader('Content-Type', 'text/css; charset=utf-8');
142
- res.sendFile(cssPath);
150
+ res.sendFile(normalizedPath);
143
151
  } catch (error) {
144
152
  console.error(`[CSS] Error processing ${req.path}:`, error.message);
145
153
  next();
@@ -444,6 +452,7 @@ class DevServer {
444
452
 
445
453
  const html = await renderWithLayout(this.liquid, 'templates/index', context, this.themePath);
446
454
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
455
+ res.setHeader('Cache-Control', 'no-cache');
447
456
  res.send(html);
448
457
  } catch (error) {
449
458
  next(error);
@@ -453,7 +462,73 @@ class DevServer {
453
462
  // Products listing page
454
463
  this.app.get('/products', async (req, res, next) => {
455
464
  try {
456
- const context = await this.buildContext(req, 'products');
465
+ // Check for category filter in query parameters (handle both 'category' and 'Category')
466
+ const categoryFilter = req.query.category || req.query.Category;
467
+
468
+ let context = await this.buildContext(req, 'products');
469
+
470
+ // If category filter is present, filter products by category
471
+ if (categoryFilter) {
472
+ console.log(`[PRODUCTS PAGE] Filtering by category: ${categoryFilter}`);
473
+
474
+ // Find category by name or handle (case-insensitive)
475
+ const category = context.categories?.find(c =>
476
+ (c.name && c.name.toLowerCase() === categoryFilter.toLowerCase()) ||
477
+ (c.handle && c.handle.toLowerCase() === categoryFilter.toLowerCase())
478
+ );
479
+
480
+ if (category) {
481
+ console.log(`[PRODUCTS PAGE] Found category: ${category.name} (ID: ${category.id})`);
482
+
483
+ // Filter products by categoryId and enrich them
484
+ const filteredProducts = context.products
485
+ .filter(p =>
486
+ p.categoryId === category.id ||
487
+ String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
488
+ )
489
+ .map(p => this.enrichProductData(p)); // Enrich each product
490
+
491
+ // CRITICAL: Set both context.products and context.collection.products to the SAME enriched array
492
+ // This ensures they always reference the same objects (like widgets do)
493
+ context.products = filteredProducts;
494
+ context.collection = context.collection || {};
495
+ context.collection.products = context.products; // Same reference as context.products
496
+ context.collection.title = category.name || 'Products';
497
+ context.collection.handle = category.handle || categoryFilter;
498
+ context.collection.description = category.description;
499
+ context.collection.totalProducts = filteredProducts.length;
500
+
501
+ console.log(`[PRODUCTS PAGE] Filtered products: ${filteredProducts.length} products in category`);
502
+ console.log(`[PRODUCTS PAGE] ✅ collection.products === context.products: ${context.collection.products === context.products}`);
503
+
504
+ console.log(`[PRODUCTS PAGE] ✅ Category filtered: ${filteredProducts.length} enriched products in collection`);
505
+ } else {
506
+ console.warn(`[PRODUCTS PAGE] Category not found: ${categoryFilter}, showing all products`);
507
+ }
508
+ }
509
+
510
+ // Ensure all products have proper data: URL, name/title, price, images
511
+ // Only enrich if not already enriched (check if first product has url field)
512
+ if (context.products && context.products.length > 0 && !context.products[0].url && !context.products[0].link) {
513
+ console.log(`[PRODUCTS PAGE] Products need enrichment - enriching now...`);
514
+ context.products = this.enrichProductsData(context.products);
515
+ } else if (context.products && context.products.length > 0) {
516
+ // Products already enriched
517
+ console.log(`[PRODUCTS PAGE] Products already enriched, first product has URL: ${context.products[0].url || context.products[0].link}`);
518
+ }
519
+
520
+ // Debug: Log sample product data to verify enrichment
521
+ if (context.products.length > 0) {
522
+ const sample = context.products[0];
523
+ console.log(`[PRODUCTS PAGE] Sample product data:`, {
524
+ title: sample.title || sample.name,
525
+ url: sample.url || sample.link,
526
+ price: sample.price || sample.sellingPrice,
527
+ stock: sample.stock,
528
+ inStock: sample.inStock,
529
+ hasImage: !!(sample.imageUrl || sample.thumbnailImage1)
530
+ });
531
+ }
457
532
 
458
533
  // DEBUG: Log context data for /products page
459
534
  console.log(`[PRODUCTS PAGE] Context summary:`);
@@ -468,9 +543,34 @@ class DevServer {
468
543
  if (!context.products || context.products.length === 0) {
469
544
  console.warn('[PRODUCTS PAGE] ⚠️ No products in context, attempting to reload...');
470
545
  try {
471
- const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0 });
546
+ const queryParams = {};
547
+ if (categoryFilter) {
548
+ // Find category and filter by categoryId
549
+ const category = context.categories?.find(c =>
550
+ (c.name && c.name.toLowerCase() === categoryFilter.toLowerCase()) ||
551
+ (c.handle && c.handle.toLowerCase() === categoryFilter.toLowerCase())
552
+ );
553
+ if (category) {
554
+ queryParams.categoryId = category.id;
555
+ }
556
+ }
557
+ const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0, ...queryParams });
472
558
  context.products = productsResponse.products || productsResponse.data?.products || [];
559
+
560
+ // Ensure URLs and all product data are set
561
+ context.products = this.enrichProductsData(context.products);
562
+
473
563
  console.log(`[PRODUCTS PAGE] ✅ Reloaded ${context.products.length} products`);
564
+ if (context.products.length > 0) {
565
+ const sample = context.products[0];
566
+ console.log(`[PRODUCTS PAGE] Sample reloaded product:`, {
567
+ title: sample.title || sample.name,
568
+ url: sample.url || sample.link,
569
+ price: sample.price || sample.sellingPrice,
570
+ stock: sample.stock,
571
+ inStock: sample.inStock
572
+ });
573
+ }
474
574
  } catch (error) {
475
575
  console.error('[PRODUCTS PAGE] ❌ Failed to load products:', error.message);
476
576
  }
@@ -479,10 +579,14 @@ class DevServer {
479
579
  if (context.products.length > 0) {
480
580
  const sampleProduct = context.products[0];
481
581
  const hasStock = 'stock' in sampleProduct || 'quantity' in sampleProduct;
582
+ const hasImage = sampleProduct.thumbnailImage1 || sampleProduct.imageUrl || (sampleProduct.images && sampleProduct.images.length > 0);
583
+ const hasUrl = sampleProduct.url || sampleProduct.link;
482
584
  // Use nullish coalescing to show 0 values correctly
483
585
  const stockValue = sampleProduct.stock ?? sampleProduct.quantity ?? 'N/A';
484
- console.log(`[PRODUCTS PAGE DEBUG] Sample product - Title: ${sampleProduct.title || sampleProduct.name}, Has stock: ${hasStock}, Stock: ${stockValue}`);
485
- console.log(`[PRODUCTS PAGE DEBUG] Product stock details: stock=${sampleProduct.stock}, quantity=${sampleProduct.quantity}, inStock=${sampleProduct.inStock}`);
586
+ console.log(`[PRODUCTS PAGE DEBUG] Sample product - Title: ${sampleProduct.title || sampleProduct.name}`);
587
+ console.log(`[PRODUCTS PAGE DEBUG] - Has stock: ${hasStock}, Stock: ${stockValue}`);
588
+ console.log(`[PRODUCTS PAGE DEBUG] - Has image: ${hasImage}, Has URL: ${hasUrl}`);
589
+ console.log(`[PRODUCTS PAGE DEBUG] - URL: ${sampleProduct.url || sampleProduct.link || 'missing'}`);
486
590
 
487
591
  // Check a few more products to see stock distribution
488
592
  const stockCounts = context.products.slice(0, 5).map(p => p.stock ?? 0);
@@ -524,16 +628,78 @@ class DevServer {
524
628
  }
525
629
 
526
630
  // Ensure collection.products is set for products template
631
+ // CRITICAL: Always sync collection.products with context.products AFTER enrichment
632
+ // This ensures collection.products always has the same enriched objects as context.products
527
633
  if (context.products && context.products.length > 0) {
634
+ // Always reassign collection.products to match context.products (they should be the same)
528
635
  context.collection = context.collection || {};
529
- context.collection.products = context.products;
530
- context.collection.title = 'All Products';
531
- context.collection.handle = 'all';
532
- context.collection.totalProducts = context.products.length;
636
+ context.collection.products = context.products; // Use the same enriched array
637
+
638
+ // Update collection metadata if not set
639
+ if (!context.collection.title) {
640
+ context.collection.title = categoryFilter ? `${categoryFilter} Products` : 'All Products';
641
+ }
642
+ if (!context.collection.handle) {
643
+ context.collection.handle = categoryFilter || 'all';
644
+ }
645
+ if (!context.collection.totalProducts) {
646
+ context.collection.totalProducts = context.products.length;
647
+ }
648
+
649
+ // Debug: Verify collection.products has same reference as context.products
650
+ const sameReference = context.collection.products === context.products;
651
+ console.log(`[PRODUCTS PAGE] collection.products sync: same reference=${sameReference}, length=${context.collection.products?.length || 0}`);
652
+
653
+ // Debug: Log sample product from collection to verify enrichment
654
+ if (context.collection.products && context.collection.products.length > 0) {
655
+ const sample = context.collection.products[0];
656
+ console.log(`[PRODUCTS PAGE] ✅ Sample collection product before render:`, {
657
+ id: sample.id,
658
+ title: sample.title || sample.name,
659
+ url: sample.url || sample.link || 'MISSING',
660
+ price: sample.price || sample.sellingPrice || 'MISSING',
661
+ prices: sample.prices ? { price: sample.prices.price, mrp: sample.prices.mrp } : 'MISSING',
662
+ stock: sample.stock !== undefined ? sample.stock : 'MISSING',
663
+ stockQuantity: sample.stockQuantity !== undefined ? sample.stockQuantity : 'MISSING',
664
+ inStock: sample.inStock !== undefined ? sample.inStock : 'MISSING',
665
+ available: sample.available !== undefined ? sample.available : 'MISSING',
666
+ hasImage: !!(sample.imageUrl || sample.thumbnailImage1),
667
+ imageUrl: sample.imageUrl || sample.thumbnailImage1?.url || 'MISSING',
668
+ hasThumbnailImage1: !!sample.thumbnailImage1,
669
+ hasImagesArray: !!(sample.images && sample.images.length > 0)
670
+ });
671
+ } else {
672
+ console.warn(`[PRODUCTS PAGE] ⚠️ collection.products is empty or undefined!`);
673
+ console.warn(`[PRODUCTS PAGE] context.products.length: ${context.products?.length || 0}`);
674
+ console.warn(`[PRODUCTS PAGE] context.collection:`, context.collection ? Object.keys(context.collection) : 'undefined');
675
+ }
676
+ } else {
677
+ console.warn(`[PRODUCTS PAGE] ⚠️ No products in context!`);
678
+ }
679
+
680
+ // Debug: Log final context before rendering
681
+ console.log(`[PRODUCTS PAGE] Final context before rendering:`);
682
+ console.log(` - Products count: ${context.products?.length || 0}`);
683
+ if (context.products && context.products.length > 0) {
684
+ const firstProduct = context.products[0];
685
+ console.log(` - First product:`, {
686
+ id: firstProduct.id,
687
+ title: firstProduct.title || firstProduct.name,
688
+ url: firstProduct.url || firstProduct.link,
689
+ price: firstProduct.price || firstProduct.sellingPrice,
690
+ stock: firstProduct.stock,
691
+ inStock: firstProduct.inStock,
692
+ hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage1),
693
+ imageUrl: firstProduct.imageUrl || firstProduct.thumbnailImage1?.url
694
+ });
695
+ }
696
+ if (context.collection) {
697
+ console.log(` - Collection: ${context.collection.title || context.collection.name}, Products: ${context.collection.products?.length || 0}`);
533
698
  }
534
699
 
535
700
  const html = await renderWithLayout(this.liquid, 'templates/products', context, this.themePath);
536
701
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
702
+ res.setHeader('Cache-Control', 'no-cache');
537
703
  res.send(html);
538
704
  } catch (error) {
539
705
  // If products template doesn't exist, try collections template as fallback
@@ -542,6 +708,7 @@ class DevServer {
542
708
  const context = await this.buildContext(req, 'collection');
543
709
  const html = await renderWithLayout(this.liquid, 'templates/collection', context, this.themePath);
544
710
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
711
+ res.setHeader('Cache-Control', 'no-cache');
545
712
  res.send(html);
546
713
  } catch (fallbackError) {
547
714
  next(error);
@@ -555,7 +722,46 @@ class DevServer {
555
722
  // Product detail page
556
723
  this.app.get('/products/:handle', async (req, res, next) => {
557
724
  try {
558
- const context = await this.buildContext(req, 'product', { productHandle: req.params.handle });
725
+ const handle = req.params.handle;
726
+ console.log(`[PRODUCT PAGE] Looking up product with handle: ${handle}`);
727
+
728
+ const context = await this.buildContext(req, 'product', { productHandle: handle });
729
+
730
+ // Check if product was found
731
+ if (!context.product) {
732
+ console.warn(`[PRODUCT PAGE] Product not found with handle: ${handle}`);
733
+ // Try to find product by ID if handle looks like an ID
734
+ if (handle.match(/^\d+$/) || handle.startsWith('product-')) {
735
+ const productId = handle.replace(/^product-/, '');
736
+ console.log(`[PRODUCT PAGE] Trying to find product by ID: ${productId}`);
737
+ const allProducts = context.products || [];
738
+ const productById = allProducts.find(p =>
739
+ String(p.id) === productId ||
740
+ String(p.id) === handle ||
741
+ (p.handle && p.handle.includes(productId))
742
+ );
743
+ if (productById) {
744
+ context.product = productById;
745
+ console.log(`[PRODUCT PAGE] Found product by ID: ${productById.title || productById.name}`);
746
+ }
747
+ }
748
+
749
+ // If still not found, return 404
750
+ if (!context.product) {
751
+ return res.status(404).send(`
752
+ <!DOCTYPE html>
753
+ <html>
754
+ <head><title>Product Not Found</title></head>
755
+ <body>
756
+ <h1>Product Not Found</h1>
757
+ <p>Product with handle "${handle}" was not found.</p>
758
+ <p><a href="/products">Back to Products</a></p>
759
+ </body>
760
+ </html>
761
+ `);
762
+ }
763
+ }
764
+
559
765
  // Try multiple template names: product-detail, product, product-page
560
766
  let html;
561
767
  const templateOptions = ['templates/product-detail', 'templates/product', 'templates/product-page'];
@@ -573,8 +779,10 @@ class DevServer {
573
779
  throw new Error(`Product template not found. Tried: ${templateOptions.join(', ')}`);
574
780
  }
575
781
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
782
+ res.setHeader('Cache-Control', 'no-cache');
576
783
  res.send(html);
577
784
  } catch (error) {
785
+ console.error(`[PRODUCT PAGE] Error rendering product page:`, error);
578
786
  next(error);
579
787
  }
580
788
  });
@@ -600,6 +808,7 @@ class DevServer {
600
808
  throw new Error(`Product template not found. Tried: ${templateOptions.join(', ')}`);
601
809
  }
602
810
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
811
+ res.setHeader('Cache-Control', 'no-cache');
603
812
  res.send(html);
604
813
  } catch (error) {
605
814
  next(error);
@@ -626,6 +835,7 @@ class DevServer {
626
835
  throw new Error(`Product template not found. Tried: ${templateOptions.join(', ')}`);
627
836
  }
628
837
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
838
+ res.setHeader('Cache-Control', 'no-cache');
629
839
  res.send(html);
630
840
  } catch (error) {
631
841
  next(error);
@@ -652,6 +862,7 @@ class DevServer {
652
862
  }
653
863
  }
654
864
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
865
+ res.setHeader('Cache-Control', 'no-cache');
655
866
  res.send(html);
656
867
  } catch (error) {
657
868
  next(error);
@@ -670,10 +881,30 @@ class DevServer {
670
881
  if (category) {
671
882
  context.category = category;
672
883
  context.collection = category;
884
+
885
+ // Filter products by this category and enrich them
886
+ const filteredProducts = (context.products || [])
887
+ .filter(p =>
888
+ p.categoryId === category.id ||
889
+ String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
890
+ )
891
+ .map(p => this.enrichProductData(p));
892
+
893
+ context.collection.products = filteredProducts;
894
+ context.collection.totalProducts = filteredProducts.length;
895
+ context.products = filteredProducts; // Also set in products array for consistency
896
+
897
+ console.log(`[COLLECTION PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
898
+ } else {
899
+ // No category found, use all products with enrichment
900
+ context.products = this.enrichProductsData(context.products || []);
901
+ context.collection = context.collection || {};
902
+ context.collection.products = context.products;
673
903
  }
674
904
 
905
+ // Prioritize products template to show product list, not categories list
675
906
  let html;
676
- const templateOptions = ['templates/categories', 'templates/category', 'templates/collection'];
907
+ const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
677
908
  for (const template of templateOptions) {
678
909
  try {
679
910
  html = await renderWithLayout(this.liquid, template, context, this.themePath);
@@ -688,6 +919,7 @@ class DevServer {
688
919
  throw new Error(`Category/Collection template not found. Tried: ${templateOptions.join(', ')}`);
689
920
  }
690
921
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
922
+ res.setHeader('Cache-Control', 'no-cache');
691
923
  res.send(html);
692
924
  } catch (error) {
693
925
  next(error);
@@ -714,6 +946,7 @@ class DevServer {
714
946
  throw new Error(`Categories template not found. Tried: ${templateOptions.join(', ')}`);
715
947
  }
716
948
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
949
+ res.setHeader('Cache-Control', 'no-cache');
717
950
  res.send(html);
718
951
  } catch (error) {
719
952
  next(error);
@@ -730,10 +963,30 @@ class DevServer {
730
963
  if (category) {
731
964
  context.category = category;
732
965
  context.collection = category;
966
+
967
+ // Filter products by this category and enrich them
968
+ const filteredProducts = (context.products || [])
969
+ .filter(p =>
970
+ p.categoryId === category.id ||
971
+ String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
972
+ )
973
+ .map(p => this.enrichProductData(p));
974
+
975
+ context.collection.products = filteredProducts;
976
+ context.collection.totalProducts = filteredProducts.length;
977
+ context.products = filteredProducts; // Also set in products array for consistency
978
+
979
+ console.log(`[CATEGORY PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
980
+ } else {
981
+ // No category found, use all products with enrichment
982
+ context.products = this.enrichProductsData(context.products || []);
983
+ context.collection = context.collection || {};
984
+ context.collection.products = context.products;
733
985
  }
734
986
 
987
+ // Prioritize products template to show product list, not categories list
735
988
  let html;
736
- const templateOptions = ['templates/categories', 'templates/category', 'templates/collection'];
989
+ const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
737
990
  for (const template of templateOptions) {
738
991
  try {
739
992
  html = await renderWithLayout(this.liquid, template, context, this.themePath);
@@ -748,6 +1001,7 @@ class DevServer {
748
1001
  throw new Error(`Category template not found. Tried: ${templateOptions.join(', ')}`);
749
1002
  }
750
1003
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1004
+ res.setHeader('Cache-Control', 'no-cache');
751
1005
  res.send(html);
752
1006
  } catch (error) {
753
1007
  next(error);
@@ -775,6 +1029,7 @@ class DevServer {
775
1029
  html = this.createSimpleBrandsPage(context);
776
1030
  }
777
1031
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1032
+ res.setHeader('Cache-Control', 'no-cache');
778
1033
  res.send(html);
779
1034
  } catch (error) {
780
1035
  next(error);
@@ -813,6 +1068,7 @@ class DevServer {
813
1068
  html = this.createSimpleBrandPage(context, brand);
814
1069
  }
815
1070
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1071
+ res.setHeader('Cache-Control', 'no-cache');
816
1072
  res.send(html);
817
1073
  } catch (error) {
818
1074
  next(error);
@@ -825,6 +1081,7 @@ class DevServer {
825
1081
  const context = await this.buildContext(req, 'cart');
826
1082
  const html = await renderWithLayout(this.liquid, 'templates/cart', context, this.themePath);
827
1083
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1084
+ res.setHeader('Cache-Control', 'no-cache');
828
1085
  res.send(html);
829
1086
  } catch (error) {
830
1087
  next(error);
@@ -837,6 +1094,7 @@ class DevServer {
837
1094
  const context = await this.buildContext(req, 'search', { query: req.query.q });
838
1095
  const html = await renderWithLayout(this.liquid, 'templates/search', context, this.themePath);
839
1096
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1097
+ res.setHeader('Cache-Control', 'no-cache');
840
1098
  res.send(html);
841
1099
  } catch (error) {
842
1100
  next(error);
@@ -890,12 +1148,14 @@ class DevServer {
890
1148
  try {
891
1149
  const html = await renderWithLayout(this.liquid, 'templates/page', context, this.themePath);
892
1150
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1151
+ res.setHeader('Cache-Control', 'no-cache');
893
1152
  res.send(html);
894
1153
  } catch (templateError) {
895
1154
  // If no page template, render raw HTML content within layout
896
1155
  console.log(`[PAGE] No page template found, rendering raw HTML for ${req.params.handle}`);
897
1156
  const html = this.renderLandingPage(context);
898
1157
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1158
+ res.setHeader('Cache-Control', 'no-cache');
899
1159
  res.send(html);
900
1160
  }
901
1161
  } catch (error) {
@@ -934,6 +1194,7 @@ class DevServer {
934
1194
 
935
1195
  const html = await renderWithLayout(this.liquid, 'templates/page', context, this.themePath);
936
1196
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1197
+ res.setHeader('Cache-Control', 'no-cache');
937
1198
  res.send(html);
938
1199
  } catch (error) {
939
1200
  next(error);
@@ -950,6 +1211,7 @@ class DevServer {
950
1211
 
951
1212
  const html = this.generateDevDashboard({ templates, sections, widgets, snippets });
952
1213
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1214
+ res.setHeader('Cache-Control', 'no-cache');
953
1215
  res.send(html);
954
1216
  } catch (error) {
955
1217
  next(error);
@@ -974,6 +1236,7 @@ class DevServer {
974
1236
  backUrl: '/dev'
975
1237
  });
976
1238
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1239
+ res.setHeader('Cache-Control', 'no-cache');
977
1240
  res.send(html);
978
1241
  } catch (error) {
979
1242
  next(error);
@@ -1033,6 +1296,7 @@ class DevServer {
1033
1296
  backUrl: '/dev'
1034
1297
  });
1035
1298
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1299
+ res.setHeader('Cache-Control', 'no-cache');
1036
1300
  res.send(html);
1037
1301
  } catch (error) {
1038
1302
  next(error);
@@ -1081,6 +1345,7 @@ class DevServer {
1081
1345
  backUrl: '/dev'
1082
1346
  });
1083
1347
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1348
+ res.setHeader('Cache-Control', 'no-cache');
1084
1349
  res.send(html);
1085
1350
  } catch (error) {
1086
1351
  next(error);
@@ -1118,12 +1383,235 @@ class DevServer {
1118
1383
  const context = await this.buildContext(req, pageType);
1119
1384
  const html = await renderWithLayout(this.liquid, `templates/${templateName}`, context, this.themePath);
1120
1385
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
1386
+ res.setHeader('Cache-Control', 'no-cache');
1121
1387
  res.send(html);
1122
1388
  } catch (error) {
1123
1389
  next(error);
1124
1390
  }
1125
1391
  });
1126
1392
 
1393
+ // Catch-all route for category/collection handles at root level (e.g., /toys-and-games)
1394
+ // This handles links from widgets that use category handles directly
1395
+ this.app.get('/:handle', async (req, res, next) => {
1396
+ try {
1397
+ const handle = req.params.handle;
1398
+
1399
+ // Skip if it's a known route path or static file extension
1400
+ const knownPaths = ['assets', 'images', 'api', 'dev', 'favicon.ico', 'products', 'collections', 'categories', 'brands', 'cart', 'search', 'page', 'pages'];
1401
+ const staticExtensions = ['.css', '.js', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp', '.woff', '.woff2', '.ttf', '.eot', '.ico'];
1402
+ const hasStaticExtension = staticExtensions.some(ext => handle.toLowerCase().endsWith(ext));
1403
+
1404
+ if (knownPaths.includes(handle) || hasStaticExtension) {
1405
+ return next();
1406
+ }
1407
+
1408
+ // Build context to get categories, brands, and products
1409
+ const context = await this.buildContext(req, 'collection', { collectionHandle: handle });
1410
+
1411
+ // Try to find category by handle (case-insensitive)
1412
+ const category = context.categories?.find(c =>
1413
+ (c.handle && c.handle.toLowerCase() === handle.toLowerCase()) ||
1414
+ (c.name && c.name.toLowerCase().replace(/\s+/g, '-') === handle.toLowerCase())
1415
+ );
1416
+
1417
+ if (category) {
1418
+ // Found a category - render products list (NOT categories list)
1419
+ context.category = category;
1420
+ context.collection = category;
1421
+
1422
+ console.log(`[ROOT COLLECTION] Found category: ${category.name} (ID: ${category.id})`);
1423
+ console.log(`[ROOT COLLECTION] Total products in context: ${context.products?.length || 0}`);
1424
+
1425
+ // Filter products by this category and enrich them
1426
+ const filteredProducts = (context.products || [])
1427
+ .filter(p => {
1428
+ const matches = p.categoryId === category.id ||
1429
+ String(p.categoryId).toLowerCase() === String(category.id).toLowerCase();
1430
+ if (matches) {
1431
+ console.log(`[ROOT COLLECTION] Product matches: ${p.title || p.name || p.id}, categoryId: ${p.categoryId}`);
1432
+ }
1433
+ return matches;
1434
+ })
1435
+ .map(p => this.enrichProductData(p)); // Enrich each product
1436
+
1437
+ console.log(`[ROOT COLLECTION] Filtered products count: ${filteredProducts.length}`);
1438
+
1439
+ // Log sample enriched products
1440
+ if (filteredProducts.length > 0) {
1441
+ filteredProducts.slice(0, 3).forEach((product, index) => {
1442
+ console.log(`[ROOT COLLECTION] Product ${index} AFTER enrichment:`, {
1443
+ id: product.id,
1444
+ title: product.title || product.name,
1445
+ url: product.url || product.link,
1446
+ price: product.price || product.sellingPrice,
1447
+ stock: product.stock,
1448
+ inStock: product.inStock,
1449
+ hasImage: !!(product.imageUrl || product.thumbnailImage1),
1450
+ imageUrl: product.imageUrl || product.thumbnailImage1?.url
1451
+ });
1452
+ });
1453
+ }
1454
+
1455
+ // Set products in multiple places to ensure templates can access them
1456
+ context.collection.products = filteredProducts;
1457
+ context.collection.totalProducts = filteredProducts.length;
1458
+ context.products = filteredProducts; // Also set in products array for consistency
1459
+
1460
+ // Also set in collection object for template compatibility
1461
+ if (!context.collection.products) {
1462
+ context.collection.products = filteredProducts;
1463
+ }
1464
+
1465
+ console.log(`[ROOT COLLECTION] Final context setup:`);
1466
+ console.log(` - context.products.length: ${context.products?.length || 0}`);
1467
+ console.log(` - context.collection.products.length: ${context.collection.products?.length || 0}`);
1468
+ console.log(` - context.collection.totalProducts: ${context.collection.totalProducts || 0}`);
1469
+
1470
+ if (filteredProducts.length > 0) {
1471
+ const sample = filteredProducts[0];
1472
+ console.log(`[ROOT COLLECTION] Sample enriched product:`, {
1473
+ id: sample.id,
1474
+ title: sample.title || sample.name,
1475
+ url: sample.url || sample.link,
1476
+ price: sample.price || sample.sellingPrice,
1477
+ stock: sample.stock,
1478
+ inStock: sample.inStock,
1479
+ hasImage: !!(sample.imageUrl || sample.thumbnailImage1),
1480
+ imageUrl: sample.imageUrl || sample.thumbnailImage1?.url,
1481
+ allKeys: Object.keys(sample).slice(0, 20)
1482
+ });
1483
+ } else {
1484
+ console.warn(`[ROOT COLLECTION] ⚠️ No products found for category ${category.name}!`);
1485
+ console.warn(`[ROOT COLLECTION] Available products in context: ${context.products?.length || 0}`);
1486
+ if (context.products && context.products.length > 0) {
1487
+ console.warn(`[ROOT COLLECTION] Sample product categoryIds:`, context.products.slice(0, 3).map(p => ({
1488
+ id: p.id,
1489
+ title: p.title || p.name,
1490
+ categoryId: p.categoryId
1491
+ })));
1492
+ }
1493
+ }
1494
+
1495
+ // Debug: Log final context before rendering
1496
+ console.log(`[ROOT COLLECTION] Final context before rendering:`);
1497
+ console.log(` - Products count: ${context.products?.length || 0}`);
1498
+ if (context.products && context.products.length > 0) {
1499
+ const firstProduct = context.products[0];
1500
+ console.log(` - First product:`, {
1501
+ id: firstProduct.id,
1502
+ title: firstProduct.title || firstProduct.name,
1503
+ url: firstProduct.url || firstProduct.link,
1504
+ price: firstProduct.price || firstProduct.sellingPrice,
1505
+ stock: firstProduct.stock,
1506
+ inStock: firstProduct.inStock,
1507
+ hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage1)
1508
+ });
1509
+ }
1510
+
1511
+ // Try to render products template first (to show product list, not categories list)
1512
+ let html;
1513
+ const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
1514
+ for (const template of templateOptions) {
1515
+ try {
1516
+ html = await renderWithLayout(this.liquid, template, context, this.themePath);
1517
+ break;
1518
+ } catch (error) {
1519
+ if (!error.message?.includes('Template not found')) {
1520
+ throw error;
1521
+ }
1522
+ }
1523
+ }
1524
+
1525
+ if (html) {
1526
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
1527
+ res.setHeader('Cache-Control', 'no-cache');
1528
+ return res.send(html);
1529
+ }
1530
+ }
1531
+
1532
+ // Not a category - try to find brand by handle
1533
+ const brand = context.brands?.find(b =>
1534
+ (b.handle && b.handle.toLowerCase() === handle.toLowerCase()) ||
1535
+ (b.name && b.name.toLowerCase().replace(/\s+/g, '-') === handle.toLowerCase())
1536
+ );
1537
+
1538
+ if (brand) {
1539
+ // Found a brand - render products list
1540
+ context.brand = brand;
1541
+
1542
+ // Filter products by this brand and enrich them
1543
+ const filteredProducts = (context.products || [])
1544
+ .filter(p =>
1545
+ p.brandId === brand.id ||
1546
+ String(p.brandId).toLowerCase() === String(brand.id).toLowerCase() ||
1547
+ (p.brand && p.brand.toLowerCase() === brand.name.toLowerCase())
1548
+ )
1549
+ .map(p => this.enrichProductData(p));
1550
+
1551
+ context.brand.products = filteredProducts;
1552
+ context.products = filteredProducts;
1553
+
1554
+ console.log(`[ROOT BRAND] Brand: ${brand.name}, Products: ${filteredProducts.length}`);
1555
+
1556
+ // Try to render products template
1557
+ let html;
1558
+ const templateOptions = ['templates/products', 'templates/brand', 'templates/collection', 'templates/brands'];
1559
+ for (const template of templateOptions) {
1560
+ try {
1561
+ html = await renderWithLayout(this.liquid, template, context, this.themePath);
1562
+ break;
1563
+ } catch (error) {
1564
+ if (!error.message?.includes('Template not found')) {
1565
+ throw error;
1566
+ }
1567
+ }
1568
+ }
1569
+
1570
+ if (html) {
1571
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
1572
+ res.setHeader('Cache-Control', 'no-cache');
1573
+ return res.send(html);
1574
+ }
1575
+ }
1576
+
1577
+ // Not a category - check if it's a product handle
1578
+ const product = context.products?.find(p =>
1579
+ (p.handle && p.handle.toLowerCase() === handle.toLowerCase()) ||
1580
+ (p.slug && p.slug.toLowerCase() === handle.toLowerCase())
1581
+ );
1582
+
1583
+ if (product) {
1584
+ // Found a product - render product detail page
1585
+ const productContext = await this.buildContext(req, 'product', { productHandle: product.handle || product.slug || handle });
1586
+
1587
+ let html;
1588
+ const templateOptions = ['templates/product-detail', 'templates/product', 'templates/product-page'];
1589
+ for (const template of templateOptions) {
1590
+ try {
1591
+ html = await renderWithLayout(this.liquid, template, productContext, this.themePath);
1592
+ break;
1593
+ } catch (error) {
1594
+ if (!error.message?.includes('Template not found')) {
1595
+ throw error;
1596
+ }
1597
+ }
1598
+ }
1599
+
1600
+ if (html) {
1601
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
1602
+ res.setHeader('Cache-Control', 'no-cache');
1603
+ return res.send(html);
1604
+ }
1605
+ }
1606
+
1607
+ // Not found - pass to next middleware (will eventually 404)
1608
+ next();
1609
+ } catch (error) {
1610
+ // If there's an error, pass to next middleware
1611
+ next(error);
1612
+ }
1613
+ });
1614
+
1127
1615
  // Favicon handler
1128
1616
  this.app.get('/favicon.ico', (req, res) => {
1129
1617
  res.status(204).end(); // No Content
@@ -1267,10 +1755,13 @@ class DevServer {
1267
1755
 
1268
1756
  try {
1269
1757
  // Load settings from settings_data.json
1758
+ // Extract 'current' section to match webstore behavior
1270
1759
  const settingsPath = path.join(this.themePath, 'config', 'settings_data.json');
1271
1760
  if (fs.existsSync(settingsPath)) {
1272
1761
  const settingsData = fs.readFileSync(settingsPath, 'utf8');
1273
- context.settings = JSON.parse(settingsData);
1762
+ const parsedData = JSON.parse(settingsData);
1763
+ // Extract 'current' section like loadThemeSettings() does
1764
+ context.settings = parsedData.current || parsedData.presets?.default || {};
1274
1765
  }
1275
1766
  } catch (error) {
1276
1767
  console.warn('[CONTEXT] Failed to load settings:', error.message);
@@ -1350,6 +1841,31 @@ class DevServer {
1350
1841
  // Load products
1351
1842
  const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0 });
1352
1843
  context.products = productsResponse.products || productsResponse.data?.products || [];
1844
+
1845
+ // Enrich all products using the reusable enrichment function
1846
+ // This ensures consistency with products used in widgets and pages
1847
+ context.products = this.enrichProductsData(context.products);
1848
+
1849
+ // Debug first few products to verify enrichment
1850
+ if (context.products.length > 0) {
1851
+ context.products.slice(0, 3).forEach((product, index) => {
1852
+ console.log(`[CONTEXT] Product ${index + 1} after enrichment:`, {
1853
+ id: product.id,
1854
+ handle: product.handle || 'none',
1855
+ url: product.url || product.link || 'MISSING',
1856
+ title: product.title || product.name || 'MISSING',
1857
+ price: product.price || product.sellingPrice || 'MISSING',
1858
+ hasPrices: !!product.prices,
1859
+ stock: product.stock !== undefined ? product.stock : 'MISSING',
1860
+ stockQuantity: product.stockQuantity !== undefined ? product.stockQuantity : 'MISSING',
1861
+ inStock: product.inStock !== undefined ? product.inStock : 'MISSING',
1862
+ available: product.available !== undefined ? product.available : 'MISSING',
1863
+ hasImage: !!(product.imageUrl || product.thumbnailImage1),
1864
+ imageUrl: product.imageUrl || product.thumbnailImage1?.url || 'MISSING'
1865
+ });
1866
+ });
1867
+ }
1868
+
1353
1869
  console.log(`[CONTEXT] Loaded ${context.products.length} products`);
1354
1870
 
1355
1871
  // DEBUG: Verify products have quantity/stock field
@@ -1376,6 +1892,18 @@ class DevServer {
1376
1892
  try {
1377
1893
  const categoriesResponse = await this.apiClient.getCategories({ limit: 50, offset: 0 });
1378
1894
  context.categories = categoriesResponse.categories || categoriesResponse.data?.categories || categoriesResponse || [];
1895
+
1896
+ // Ensure all categories have proper URL fields for navigation
1897
+ context.categories = context.categories.map(category => {
1898
+ // Set URL to category handle (e.g., /accessories)
1899
+ if (!category.url && !category.link) {
1900
+ const handle = category.handle || category.name?.toLowerCase().replace(/\s+/g, '-') || category.id;
1901
+ category.url = `/${handle}`;
1902
+ category.link = `/${handle}`;
1903
+ }
1904
+ return category;
1905
+ });
1906
+
1379
1907
  context.collections = context.categories; // Also set collections for compatibility
1380
1908
  console.log(`[CONTEXT] Loaded ${context.categories.length} categories`);
1381
1909
  } catch (error) {
@@ -1446,40 +1974,57 @@ class DevServer {
1446
1974
  if (pageType === 'product' && extra.productHandle) {
1447
1975
  // Load single product data
1448
1976
  try {
1977
+ const searchHandle = extra.productHandle.toLowerCase().trim();
1978
+ console.log(`[CONTEXT] Searching for product with handle: ${searchHandle}`);
1979
+
1449
1980
  // First try to find in already loaded products
1450
- let product = context.products?.find(p =>
1451
- (p.handle && p.handle.toLowerCase() === extra.productHandle.toLowerCase()) ||
1452
- (p.slug && p.slug.toLowerCase() === extra.productHandle.toLowerCase()) ||
1453
- (p.id && String(p.id).toLowerCase() === extra.productHandle.toLowerCase())
1454
- );
1981
+ // Check multiple fields: handle, slug, id, and also check if handle contains the ID
1982
+ let product = context.products?.find(p => {
1983
+ const pHandle = (p.handle || '').toLowerCase().trim();
1984
+ const pSlug = (p.slug || '').toLowerCase().trim();
1985
+ const pId = String(p.id || '').toLowerCase().trim();
1986
+ const pIdStr = String(p.id || '');
1987
+
1988
+ return pHandle === searchHandle ||
1989
+ pSlug === searchHandle ||
1990
+ pId === searchHandle ||
1991
+ (searchHandle.startsWith('product-') && pId === searchHandle.replace('product-', '')) ||
1992
+ (pHandle && searchHandle.includes(pIdStr)) ||
1993
+ (pSlug && searchHandle.includes(pIdStr));
1994
+ });
1455
1995
 
1456
1996
  // If not found, try loading more products
1457
1997
  if (!product) {
1998
+ console.log(`[CONTEXT] Product not found in initial list, loading more products...`);
1458
1999
  const productsResponse = await this.apiClient.getProducts({ limit: 100 });
1459
2000
  const allProducts = productsResponse.products || productsResponse.data?.products || [];
1460
- product = allProducts.find(p =>
1461
- (p.handle && p.handle.toLowerCase() === extra.productHandle.toLowerCase()) ||
1462
- (p.slug && p.slug.toLowerCase() === extra.productHandle.toLowerCase()) ||
1463
- (p.id && String(p.id).toLowerCase() === extra.productHandle.toLowerCase())
1464
- );
2001
+ product = allProducts.find(p => {
2002
+ const pHandle = (p.handle || '').toLowerCase().trim();
2003
+ const pSlug = (p.slug || '').toLowerCase().trim();
2004
+ const pId = String(p.id || '').toLowerCase().trim();
2005
+ const pIdStr = String(p.id || '');
2006
+
2007
+ return pHandle === searchHandle ||
2008
+ pSlug === searchHandle ||
2009
+ pId === searchHandle ||
2010
+ (searchHandle.startsWith('product-') && pId === searchHandle.replace('product-', '')) ||
2011
+ (pHandle && searchHandle.includes(pIdStr)) ||
2012
+ (pSlug && searchHandle.includes(pIdStr));
2013
+ });
1465
2014
  }
1466
2015
 
1467
2016
  if (product) {
1468
2017
  context.product = product;
1469
- console.log(`[CONTEXT] Found product: ${product.title || product.name} (handle: ${product.handle || product.slug})`);
2018
+ console.log(`[CONTEXT] Found product: ${product.title || product.name} (handle: ${product.handle || product.slug || product.id})`);
1470
2019
  } else {
1471
- console.warn(`[CONTEXT] Product not found with handle/slug/id: ${extra.productHandle}`);
1472
- // Fallback to first product if handle not found
1473
- if (context.products && context.products.length > 0) {
1474
- context.product = context.products[0];
1475
- console.log(`[CONTEXT] Using fallback product: ${context.product.title || context.product.name}`);
1476
- }
2020
+ console.warn(`[CONTEXT] ⚠️ Product not found with handle/slug/id: ${extra.productHandle}`);
2021
+ console.warn(`[CONTEXT] Available product handles: ${(context.products || []).slice(0, 5).map(p => p.handle || p.slug || p.id).join(', ')}`);
2022
+ // Don't use fallback - let the route handler deal with 404
1477
2023
  }
1478
2024
  } catch (error) {
1479
- console.warn('[CONTEXT] Failed to load product:', error.message);
1480
- if (context.products && context.products.length > 0) {
1481
- context.product = context.products[0];
1482
- }
2025
+ console.error('[CONTEXT] Failed to load product:', error.message);
2026
+ console.error('[CONTEXT] Error stack:', error.stack);
2027
+ // Don't use fallback on error - let route handle it
1483
2028
  }
1484
2029
  }
1485
2030
  } catch (error) {
@@ -1589,18 +2134,35 @@ class DevServer {
1589
2134
  if (type.includes('product') || type === 'featuredproducts' || type === 'bestsellerproducts' || type === 'newproducts' || type === 'simpleproduct' || type === 'singleproduct') {
1590
2135
  const widgetProducts = this.getProductsForWidget(widget, products);
1591
2136
 
2137
+ // Products are already enriched by getProductsForWidget, but ensure they're properly formatted
2138
+ const enrichedProducts = this.enrichProductsData(widgetProducts);
2139
+
1592
2140
  // Set products at multiple levels for template compatibility
1593
- widget.data.products = widgetProducts;
1594
- widget.data.Products = widgetProducts;
1595
- widget.products = widgetProducts;
2141
+ widget.data.products = enrichedProducts;
2142
+ widget.data.Products = enrichedProducts;
2143
+ widget.products = enrichedProducts;
1596
2144
 
1597
2145
  // Also set in content for some template variations
1598
2146
  widget.data.content = widget.data.content || {};
1599
- widget.data.content.products = widgetProducts;
1600
- widget.data.content.Products = widgetProducts;
2147
+ widget.data.content.products = enrichedProducts;
2148
+ widget.data.content.Products = enrichedProducts;
1601
2149
 
1602
2150
  enrichedCount++;
1603
- console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${widgetProducts.length} products`);
2151
+ console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${enrichedProducts.length} enriched products`);
2152
+
2153
+ // Debug: Log sample product data
2154
+ if (enrichedProducts.length > 0) {
2155
+ const sample = enrichedProducts[0];
2156
+ console.log(`[ENRICH] Sample widget product:`, {
2157
+ id: sample.id,
2158
+ title: sample.title || sample.name,
2159
+ url: sample.url || sample.link,
2160
+ price: sample.price || sample.sellingPrice,
2161
+ stock: sample.stock,
2162
+ inStock: sample.inStock,
2163
+ hasImage: !!(sample.imageUrl || sample.thumbnailImage1)
2164
+ });
2165
+ }
1604
2166
  }
1605
2167
 
1606
2168
  // Enrich CategoryList and CategoryListCarousel widgets with categories
@@ -1637,13 +2199,18 @@ class DevServer {
1637
2199
 
1638
2200
  // Enrich RecentlyViewed widgets with products (use first 6 products as mock recent views)
1639
2201
  if (type === 'recentlyviewed' || type === 'recently-viewed') {
1640
- const recentProducts = products.slice(0, 6);
2202
+ const recentProducts = this.enrichProductsData(products.slice(0, 6));
1641
2203
  widget.data.products = recentProducts;
1642
2204
  widget.data.Products = recentProducts;
1643
2205
  widget.products = recentProducts;
1644
2206
 
2207
+ // Also set in content
2208
+ widget.data.content = widget.data.content || {};
2209
+ widget.data.content.products = recentProducts;
2210
+ widget.data.content.Products = recentProducts;
2211
+
1645
2212
  enrichedCount++;
1646
- console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${recentProducts.length} recently viewed products`);
2213
+ console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${recentProducts.length} enriched recently viewed products`);
1647
2214
  }
1648
2215
  });
1649
2216
  });
@@ -1651,37 +2218,268 @@ class DevServer {
1651
2218
  console.log(`[CONTEXT] ✅ Enriched ${enrichedCount} widgets with data`);
1652
2219
  }
1653
2220
 
2221
+ /**
2222
+ * Enrich a single product with all required data (URL, name/title, price, images, stock)
2223
+ * This ensures consistency between widgets and pages
2224
+ * @param {Object} product - Product object to enrich
2225
+ * @returns {Object} Enriched product (creates a copy to avoid mutation)
2226
+ */
2227
+ enrichProductData(product) {
2228
+ // Create a copy to avoid mutating the original
2229
+ const enriched = { ...product };
2230
+
2231
+ // CRITICAL: Ensure productId exists - it's the most reliable identifier
2232
+ if (!enriched.productId && enriched.id) {
2233
+ // Try to extract numeric ID from string ID (e.g., "product-1" -> 1)
2234
+ const numericMatch = String(enriched.id).match(/\d+/);
2235
+ if (numericMatch) {
2236
+ enriched.productId = parseInt(numericMatch[0], 10);
2237
+ } else {
2238
+ // Use the ID as-is if it's already numeric
2239
+ enriched.productId = enriched.id;
2240
+ }
2241
+ } else if (!enriched.productId && !enriched.id) {
2242
+ console.warn(`[ENRICH] ⚠️ Product missing both id and productId:`, {
2243
+ keys: Object.keys(enriched).slice(0, 15),
2244
+ title: enriched.title || enriched.name
2245
+ });
2246
+ }
2247
+
2248
+ // DEBUG: Log incoming product to diagnose missing fields
2249
+ if (!product.id && !product.handle && !product.slug && !product.productId) {
2250
+ console.warn(`[ENRICH] ⚠️ Product missing all identifiers (id/handle/slug/productId):`, {
2251
+ keys: Object.keys(product).slice(0, 15),
2252
+ title: product.title || product.name
2253
+ });
2254
+ }
2255
+
2256
+ // Ensure URL field exists - CRITICAL for product navigation
2257
+ // Treat '#' as missing URL and regenerate it
2258
+ const hasValidUrl = enriched.url && enriched.url !== '#' && enriched.url !== '';
2259
+ const hasValidLink = enriched.link && enriched.link !== '#' && enriched.link !== '';
2260
+
2261
+ if (!hasValidUrl && !hasValidLink) {
2262
+ const handle = enriched.handle || enriched.slug || enriched.id;
2263
+ if (handle && handle !== '#' && handle !== '') {
2264
+ enriched.url = `/products/${handle}`;
2265
+ enriched.link = `/products/${handle}`;
2266
+ } else {
2267
+ // Last resort: use numeric ID if available
2268
+ const numericId = enriched.productId || enriched.id;
2269
+ if (numericId) {
2270
+ const fallbackHandle = `product-${numericId}`.toLowerCase();
2271
+ enriched.url = `/products/${fallbackHandle}`;
2272
+ enriched.link = `/products/${fallbackHandle}`;
2273
+ enriched.handle = enriched.handle || fallbackHandle; // Also set handle if missing
2274
+ enriched.slug = enriched.slug || fallbackHandle; // Also set slug if missing
2275
+ } else {
2276
+ enriched.url = '#';
2277
+ enriched.link = '#';
2278
+ console.warn(`[ENRICH] ⚠️ Product has no id/handle/slug/productId, URL set to '#'`, {
2279
+ productKeys: Object.keys(enriched).slice(0, 10)
2280
+ });
2281
+ }
2282
+ }
2283
+ } else if (!hasValidUrl && hasValidLink) {
2284
+ enriched.url = enriched.link;
2285
+ } else if (hasValidUrl && !hasValidLink) {
2286
+ enriched.link = enriched.url;
2287
+ } else if (enriched.url === '#' || enriched.link === '#') {
2288
+ // If one is valid but the other is '#', update the '#' one
2289
+ if (enriched.url === '#') enriched.url = enriched.link;
2290
+ if (enriched.link === '#') enriched.link = enriched.url;
2291
+ }
2292
+
2293
+ // Ensure handle/slug exist if missing (for template fallback logic)
2294
+ // Also normalize them (remove '#' or empty strings)
2295
+ if (!enriched.handle || enriched.handle === '#' || enriched.handle === '') {
2296
+ if (enriched.slug && enriched.slug !== '#' && enriched.slug !== '') {
2297
+ enriched.handle = enriched.slug;
2298
+ } else if (enriched.id && enriched.id !== '#') {
2299
+ enriched.handle = String(enriched.id).toLowerCase();
2300
+ } else if (enriched.productId) {
2301
+ enriched.handle = `product-${enriched.productId}`.toLowerCase();
2302
+ } else if (!enriched.handle) {
2303
+ // Generate from URL if it was set above
2304
+ if (enriched.url && enriched.url !== '#') {
2305
+ enriched.handle = enriched.url.replace(/^\/products\//, '').toLowerCase();
2306
+ }
2307
+ }
2308
+ }
2309
+
2310
+ if (!enriched.slug || enriched.slug === '#' || enriched.slug === '') {
2311
+ if (enriched.handle && enriched.handle !== '#' && enriched.handle !== '') {
2312
+ enriched.slug = enriched.handle;
2313
+ } else if (enriched.id && enriched.id !== '#') {
2314
+ enriched.slug = String(enriched.id).toLowerCase();
2315
+ } else if (enriched.productId) {
2316
+ enriched.slug = `product-${enriched.productId}`.toLowerCase();
2317
+ } else if (!enriched.slug) {
2318
+ // Generate from URL if it was set above
2319
+ if (enriched.url && enriched.url !== '#') {
2320
+ enriched.slug = enriched.url.replace(/^\/products\//, '').toLowerCase();
2321
+ }
2322
+ }
2323
+ }
2324
+
2325
+ // Ensure name/title exists - CRITICAL for product display
2326
+ if (!enriched.name && !enriched.title) {
2327
+ enriched.name = `Product ${enriched.id || 'Unknown'}`;
2328
+ enriched.title = enriched.name;
2329
+ } else if (!enriched.name) {
2330
+ enriched.name = enriched.title || `Product ${enriched.id || 'Unknown'}`;
2331
+ } else if (!enriched.title) {
2332
+ enriched.title = enriched.name;
2333
+ }
2334
+
2335
+ // Ensure both variations and variants exist (product-card uses variations)
2336
+ if (enriched.variants && !enriched.variations) {
2337
+ enriched.variations = enriched.variants;
2338
+ } else if (enriched.variations && !enriched.variants) {
2339
+ enriched.variants = enriched.variations;
2340
+ }
2341
+
2342
+ // Ensure price exists - CRITICAL for product display
2343
+ if (!enriched.price && !enriched.sellingPrice) {
2344
+ // Try to get from variants/variations first
2345
+ const variantsOrVariations = enriched.variants || enriched.variations;
2346
+ if (variantsOrVariations && variantsOrVariations.length > 0) {
2347
+ enriched.price = variantsOrVariations[0].price || variantsOrVariations[0].sellingPrice || 0;
2348
+ enriched.sellingPrice = enriched.price;
2349
+ } else {
2350
+ enriched.price = 0;
2351
+ enriched.sellingPrice = 0;
2352
+ }
2353
+ } else if (!enriched.price) {
2354
+ enriched.price = enriched.sellingPrice;
2355
+ } else if (!enriched.sellingPrice) {
2356
+ enriched.sellingPrice = enriched.price;
2357
+ }
2358
+
2359
+ // Ensure prices object exists for product-card compatibility - CRITICAL
2360
+ // Make sure price is NOT 0 unless it's actually 0 (check if undefined/null)
2361
+ const finalPrice = (enriched.price !== undefined && enriched.price !== null)
2362
+ ? enriched.price
2363
+ : ((enriched.sellingPrice !== undefined && enriched.sellingPrice !== null)
2364
+ ? enriched.sellingPrice
2365
+ : 0);
2366
+
2367
+ if (!enriched.prices) {
2368
+ const mrpValue = enriched.compareAtPrice || enriched.comparePrice || enriched.mrp || 0;
2369
+ enriched.prices = {
2370
+ price: finalPrice,
2371
+ mrp: mrpValue,
2372
+ currency: enriched.currency || 'USD'
2373
+ };
2374
+ } else {
2375
+ // Update prices object if it exists but is missing fields
2376
+ if (enriched.prices.price === undefined || enriched.prices.price === null) {
2377
+ enriched.prices.price = finalPrice;
2378
+ }
2379
+ if (enriched.prices.mrp === undefined || enriched.prices.mrp === null) {
2380
+ enriched.prices.mrp = enriched.compareAtPrice || enriched.comparePrice || enriched.mrp || 0;
2381
+ }
2382
+ if (!enriched.prices.currency) {
2383
+ enriched.prices.currency = enriched.currency || 'USD';
2384
+ }
2385
+ }
2386
+
2387
+ // Ensure price/sellingPrice fields also exist (not just prices object)
2388
+ if (enriched.price === undefined || enriched.price === null) {
2389
+ enriched.price = finalPrice;
2390
+ }
2391
+ if (enriched.sellingPrice === undefined || enriched.sellingPrice === null) {
2392
+ enriched.sellingPrice = finalPrice;
2393
+ }
2394
+
2395
+ // Ensure image exists - CRITICAL for product display
2396
+ if (!enriched.imageUrl && !enriched.thumbnailImage1 && (!enriched.images || enriched.images.length === 0)) {
2397
+ // Fallback to picsum placeholder
2398
+ const imageId = enriched.id ? String(enriched.id).replace(/\D/g, '') || '1' : '1';
2399
+ enriched.imageUrl = `https://picsum.photos/seed/${imageId}/800/800`;
2400
+ enriched.thumbnailImage1 = {
2401
+ url: enriched.imageUrl,
2402
+ altText: enriched.title || enriched.name || 'Product image'
2403
+ };
2404
+ }
2405
+
2406
+ // Ensure stock/availability data exists - CRITICAL to prevent incorrect "SOLD OUT" display
2407
+ if (enriched.stock === undefined && enriched.stockQuantity === undefined) {
2408
+ // Calculate from variants/variations if available
2409
+ const variantsOrVariations = enriched.variants || enriched.variations;
2410
+ if (variantsOrVariations && variantsOrVariations.length > 0) {
2411
+ const totalStock = variantsOrVariations.reduce((sum, v) => sum + (v.stock || v.quantity || 0), 0);
2412
+ enriched.stock = totalStock;
2413
+ enriched.stockQuantity = totalStock;
2414
+ enriched.inStock = totalStock > 0;
2415
+ enriched.available = totalStock > 0;
2416
+ } else {
2417
+ // Default to in stock if no stock info (prevents false "SOLD OUT" display)
2418
+ enriched.stock = 10;
2419
+ enriched.stockQuantity = 10;
2420
+ enriched.inStock = true;
2421
+ enriched.available = true;
2422
+ }
2423
+ } else {
2424
+ // Ensure inStock/available flags are set based on stock value
2425
+ // CRITICAL: Use nullish coalescing to handle 0 values correctly
2426
+ const stockValue = enriched.stock !== undefined ? enriched.stock : (enriched.stockQuantity !== undefined ? enriched.stockQuantity : 0);
2427
+ enriched.inStock = stockValue > 0;
2428
+ enriched.available = stockValue > 0;
2429
+
2430
+ // Also ensure stockQuantity is set if only stock is set (and vice versa)
2431
+ if (enriched.stock !== undefined && enriched.stockQuantity === undefined) {
2432
+ enriched.stockQuantity = enriched.stock;
2433
+ } else if (enriched.stockQuantity !== undefined && enriched.stock === undefined) {
2434
+ enriched.stock = enriched.stockQuantity;
2435
+ }
2436
+ }
2437
+
2438
+ return enriched;
2439
+ }
2440
+
2441
+ /**
2442
+ * Enrich multiple products with all required data
2443
+ * @param {Array} products - Array of products to enrich
2444
+ * @returns {Array} Array of enriched products
2445
+ */
2446
+ enrichProductsData(products) {
2447
+ if (!Array.isArray(products)) return [];
2448
+ return products.map(product => this.enrichProductData(product));
2449
+ }
2450
+
1654
2451
  /**
1655
2452
  * Get products for a specific widget based on its settings
1656
2453
  */
1657
2454
  getProductsForWidget(widget, products) {
1658
2455
  // Get the limit from widget settings, default to 12
1659
- const limit = widget.settings?.limit || widget.data?.content?.Limit || 12;
2456
+ const limit = widget.settings?.limit || widget.data?.content?.Limit || widget.settings?.numberOfProducts || widget.settings?.NumberOfProducts || 12;
1660
2457
 
1661
2458
  // Get products based on widget type
1662
2459
  const type = (widget.type || '').toLowerCase();
1663
2460
 
2461
+ let widgetProducts = [];
2462
+
1664
2463
  if (type.includes('featured') || type === 'featuredproducts') {
1665
2464
  // Featured products - filter by featured tag or just take first N
1666
2465
  const featured = products.filter(p => p.tags?.includes('featured') || p.featured);
1667
- return featured.length > 0 ? featured.slice(0, limit) : products.slice(0, limit);
1668
- }
1669
-
1670
- if (type.includes('bestseller') || type === 'bestsellerproducts') {
2466
+ widgetProducts = featured.length > 0 ? featured.slice(0, limit) : products.slice(0, limit);
2467
+ } else if (type.includes('bestseller') || type === 'bestsellerproducts') {
1671
2468
  // Best seller products - sort by sales or just take random N
1672
- return products.slice(0, limit);
1673
- }
1674
-
1675
- if (type.includes('new') || type === 'newproducts') {
2469
+ widgetProducts = products.slice(0, limit);
2470
+ } else if (type.includes('new') || type === 'newproducts') {
1676
2471
  // New products - sort by created date
1677
2472
  const sorted = [...products].sort((a, b) =>
1678
2473
  new Date(b.createdAt || 0) - new Date(a.createdAt || 0)
1679
2474
  );
1680
- return sorted.slice(0, limit);
2475
+ widgetProducts = sorted.slice(0, limit);
2476
+ } else {
2477
+ // Default: return first N products
2478
+ widgetProducts = products.slice(0, limit);
1681
2479
  }
1682
2480
 
1683
- // Default: return first N products
1684
- return products.slice(0, limit);
2481
+ // Enrich all products with required data before returning
2482
+ return this.enrichProductsData(widgetProducts);
1685
2483
  }
1686
2484
 
1687
2485
  /**
@@ -2586,14 +3384,36 @@ class DevServer {
2586
3384
  */
2587
3385
  async stop() {
2588
3386
  return new Promise((resolve) => {
3387
+ let resolved = false;
3388
+ const safeResolve = () => {
3389
+ if (!resolved) {
3390
+ resolved = true;
3391
+ console.log(chalk.yellow('\n👋 Server stopped'));
3392
+ resolve();
3393
+ }
3394
+ };
3395
+
3396
+ // Increase max listeners to prevent warning if multiple shutdown attempts
3397
+ if (this.server && this.server.setMaxListeners) {
3398
+ this.server.setMaxListeners(20);
3399
+ }
3400
+
2589
3401
  // Stop file watcher
2590
3402
  if (this.fileWatcher && typeof this.fileWatcher.close === 'function') {
2591
- this.fileWatcher.close();
3403
+ try {
3404
+ this.fileWatcher.close();
3405
+ } catch (error) {
3406
+ // Ignore errors when closing watcher
3407
+ }
2592
3408
  }
2593
3409
 
2594
3410
  // Close Socket.IO
2595
3411
  if (this.io && typeof this.io.close === 'function') {
2596
- this.io.close();
3412
+ try {
3413
+ this.io.close();
3414
+ } catch (error) {
3415
+ // Ignore errors when closing Socket.IO
3416
+ }
2597
3417
  }
2598
3418
 
2599
3419
  // Stop mock API
@@ -2601,14 +3421,37 @@ class DevServer {
2601
3421
  this.mockApi.stop().catch(() => {});
2602
3422
  }
2603
3423
 
2604
- // Close HTTP server
3424
+ // Close HTTP server - check if it's actually listening/running
2605
3425
  if (this.server) {
2606
- this.server.close(() => {
2607
- console.log(chalk.yellow('\n👋 Server stopped'));
2608
- resolve();
3426
+ // Check if server is listening before trying to close
3427
+ const isListening = this.server.listening || false;
3428
+
3429
+ if (!isListening) {
3430
+ // Server is already closed/not running
3431
+ safeResolve();
3432
+ return;
3433
+ }
3434
+
3435
+ // Remove all existing listeners to prevent memory leak warning
3436
+ this.server.removeAllListeners('close');
3437
+
3438
+ // Close the server - use callback instead of event listener to avoid duplicate calls
3439
+ this.server.close((err) => {
3440
+ if (err) {
3441
+ // Ignore ERR_SERVER_NOT_RUNNING errors as they're harmless
3442
+ if (err.code !== 'ERR_SERVER_NOT_RUNNING') {
3443
+ console.error(chalk.red('Error closing server:'), err.message);
3444
+ }
3445
+ }
3446
+ safeResolve();
2609
3447
  });
3448
+
3449
+ // Fallback timeout in case close callback doesn't fire
3450
+ setTimeout(() => {
3451
+ safeResolve();
3452
+ }, 2000);
2610
3453
  } else {
2611
- resolve();
3454
+ safeResolve();
2612
3455
  }
2613
3456
  });
2614
3457
  }