@o2vend/theme-cli 1.0.33 → 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.
@@ -462,8 +462,8 @@ class DevServer {
462
462
  // Products listing page
463
463
  this.app.get('/products', async (req, res, next) => {
464
464
  try {
465
- // Check for category filter in query parameters
466
- const categoryFilter = req.query.category;
465
+ // Check for category filter in query parameters (handle both 'category' and 'Category')
466
+ const categoryFilter = req.query.category || req.query.Category;
467
467
 
468
468
  let context = await this.buildContext(req, 'products');
469
469
 
@@ -480,35 +480,55 @@ class DevServer {
480
480
  if (category) {
481
481
  console.log(`[PRODUCTS PAGE] Found category: ${category.name} (ID: ${category.id})`);
482
482
 
483
- // Filter products by categoryId
484
- const filteredProducts = context.products.filter(p =>
485
- p.categoryId === category.id ||
486
- String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
487
- );
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
488
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)
489
493
  context.products = filteredProducts;
490
- console.log(`[PRODUCTS PAGE] Filtered products: ${filteredProducts.length} products in category`);
491
-
492
- // Update collection context
493
494
  context.collection = context.collection || {};
495
+ context.collection.products = context.products; // Same reference as context.products
494
496
  context.collection.title = category.name || 'Products';
495
497
  context.collection.handle = category.handle || categoryFilter;
496
498
  context.collection.description = category.description;
497
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`);
498
505
  } else {
499
506
  console.warn(`[PRODUCTS PAGE] Category not found: ${categoryFilter}, showing all products`);
500
507
  }
501
508
  }
502
509
 
503
- // Ensure all products have proper URL field if missing
504
- context.products = (context.products || []).map(product => {
505
- if (!product.url && !product.link) {
506
- const handle = product.handle || product.slug || product.id;
507
- product.url = `/products/${handle}`;
508
- product.link = `/products/${handle}`;
509
- }
510
- return product;
511
- });
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
+ }
512
532
 
513
533
  // DEBUG: Log context data for /products page
514
534
  console.log(`[PRODUCTS PAGE] Context summary:`);
@@ -537,17 +557,20 @@ class DevServer {
537
557
  const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0, ...queryParams });
538
558
  context.products = productsResponse.products || productsResponse.data?.products || [];
539
559
 
540
- // Ensure URLs are set
541
- context.products = context.products.map(product => {
542
- if (!product.url && !product.link) {
543
- const handle = product.handle || product.slug || product.id;
544
- product.url = `/products/${handle}`;
545
- product.link = `/products/${handle}`;
546
- }
547
- return product;
548
- });
560
+ // Ensure URLs and all product data are set
561
+ context.products = this.enrichProductsData(context.products);
549
562
 
550
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
+ }
551
574
  } catch (error) {
552
575
  console.error('[PRODUCTS PAGE] ❌ Failed to load products:', error.message);
553
576
  }
@@ -605,9 +628,14 @@ class DevServer {
605
628
  }
606
629
 
607
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
608
633
  if (context.products && context.products.length > 0) {
634
+ // Always reassign collection.products to match context.products (they should be the same)
609
635
  context.collection = context.collection || {};
610
- context.collection.products = context.products;
636
+ context.collection.products = context.products; // Use the same enriched array
637
+
638
+ // Update collection metadata if not set
611
639
  if (!context.collection.title) {
612
640
  context.collection.title = categoryFilter ? `${categoryFilter} Products` : 'All Products';
613
641
  }
@@ -617,6 +645,56 @@ class DevServer {
617
645
  if (!context.collection.totalProducts) {
618
646
  context.collection.totalProducts = context.products.length;
619
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}`);
620
698
  }
621
699
 
622
700
  const html = await renderWithLayout(this.liquid, 'templates/products', context, this.themePath);
@@ -804,20 +882,13 @@ class DevServer {
804
882
  context.category = category;
805
883
  context.collection = category;
806
884
 
807
- // Filter products by this category
808
- const filteredProducts = (context.products || []).filter(p =>
809
- p.categoryId === category.id ||
810
- String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
811
- );
812
-
813
- // Ensure all products have proper URL field if missing
814
- filteredProducts.forEach(product => {
815
- if (!product.url && !product.link) {
816
- const handle = product.handle || product.slug || product.id;
817
- product.url = `/products/${handle}`;
818
- product.link = `/products/${handle}`;
819
- }
820
- });
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));
821
892
 
822
893
  context.collection.products = filteredProducts;
823
894
  context.collection.totalProducts = filteredProducts.length;
@@ -825,21 +896,15 @@ class DevServer {
825
896
 
826
897
  console.log(`[COLLECTION PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
827
898
  } else {
828
- // No category found, use all products
829
- context.products = (context.products || []).map(product => {
830
- if (!product.url && !product.link) {
831
- const handle = product.handle || product.slug || product.id;
832
- product.url = `/products/${handle}`;
833
- product.link = `/products/${handle}`;
834
- }
835
- return product;
836
- });
899
+ // No category found, use all products with enrichment
900
+ context.products = this.enrichProductsData(context.products || []);
837
901
  context.collection = context.collection || {};
838
902
  context.collection.products = context.products;
839
903
  }
840
904
 
905
+ // Prioritize products template to show product list, not categories list
841
906
  let html;
842
- const templateOptions = ['templates/categories', 'templates/category', 'templates/collection'];
907
+ const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
843
908
  for (const template of templateOptions) {
844
909
  try {
845
910
  html = await renderWithLayout(this.liquid, template, context, this.themePath);
@@ -899,20 +964,13 @@ class DevServer {
899
964
  context.category = category;
900
965
  context.collection = category;
901
966
 
902
- // Filter products by this category
903
- const filteredProducts = (context.products || []).filter(p =>
904
- p.categoryId === category.id ||
905
- String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
906
- );
907
-
908
- // Ensure all products have proper URL field if missing
909
- filteredProducts.forEach(product => {
910
- if (!product.url && !product.link) {
911
- const handle = product.handle || product.slug || product.id;
912
- product.url = `/products/${handle}`;
913
- product.link = `/products/${handle}`;
914
- }
915
- });
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));
916
974
 
917
975
  context.collection.products = filteredProducts;
918
976
  context.collection.totalProducts = filteredProducts.length;
@@ -920,21 +978,15 @@ class DevServer {
920
978
 
921
979
  console.log(`[CATEGORY PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
922
980
  } else {
923
- // No category found, use all products
924
- context.products = (context.products || []).map(product => {
925
- if (!product.url && !product.link) {
926
- const handle = product.handle || product.slug || product.id;
927
- product.url = `/products/${handle}`;
928
- product.link = `/products/${handle}`;
929
- }
930
- return product;
931
- });
981
+ // No category found, use all products with enrichment
982
+ context.products = this.enrichProductsData(context.products || []);
932
983
  context.collection = context.collection || {};
933
984
  context.collection.products = context.products;
934
985
  }
935
986
 
987
+ // Prioritize products template to show product list, not categories list
936
988
  let html;
937
- const templateOptions = ['templates/categories', 'templates/category', 'templates/collection'];
989
+ const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
938
990
  for (const template of templateOptions) {
939
991
  try {
940
992
  html = await renderWithLayout(this.liquid, template, context, this.themePath);
@@ -1353,7 +1405,7 @@ class DevServer {
1353
1405
  return next();
1354
1406
  }
1355
1407
 
1356
- // Build context to get categories and products
1408
+ // Build context to get categories, brands, and products
1357
1409
  const context = await this.buildContext(req, 'collection', { collectionHandle: handle });
1358
1410
 
1359
1411
  // Try to find category by handle (case-insensitive)
@@ -1363,34 +1415,147 @@ class DevServer {
1363
1415
  );
1364
1416
 
1365
1417
  if (category) {
1366
- // Found a category - render collection page
1418
+ // Found a category - render products list (NOT categories list)
1367
1419
  context.category = category;
1368
1420
  context.collection = category;
1369
1421
 
1370
- // Filter products by this category
1371
- const filteredProducts = (context.products || []).filter(p =>
1372
- p.categoryId === category.id ||
1373
- String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
1374
- );
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}`);
1375
1424
 
1376
- // Ensure all products have proper URL field if missing
1377
- filteredProducts.forEach(product => {
1378
- if (!product.url && !product.link) {
1379
- const productHandle = product.handle || product.slug || product.id;
1380
- product.url = `/products/${productHandle}`;
1381
- product.link = `/products/${productHandle}`;
1382
- }
1383
- });
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
+ }
1384
1454
 
1455
+ // Set products in multiple places to ensure templates can access them
1385
1456
  context.collection.products = filteredProducts;
1386
1457
  context.collection.totalProducts = filteredProducts.length;
1387
1458
  context.products = filteredProducts; // Also set in products array for consistency
1388
1459
 
1389
- console.log(`[ROOT COLLECTION] Category: ${category.name}, Products: ${filteredProducts.length}`);
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}`);
1390
1555
 
1391
- // Try to render collection template
1556
+ // Try to render products template
1392
1557
  let html;
1393
- const templateOptions = ['templates/categories', 'templates/category', 'templates/collection', 'templates/products'];
1558
+ const templateOptions = ['templates/products', 'templates/brand', 'templates/collection', 'templates/brands'];
1394
1559
  for (const template of templateOptions) {
1395
1560
  try {
1396
1561
  html = await renderWithLayout(this.liquid, template, context, this.themePath);
@@ -1677,48 +1842,29 @@ class DevServer {
1677
1842
  const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0 });
1678
1843
  context.products = productsResponse.products || productsResponse.data?.products || [];
1679
1844
 
1680
- // Ensure all products have proper URL, images, and stock fields
1681
- context.products = context.products.map((product, index) => {
1682
- // Ensure URL field exists
1683
- if (!product.url && !product.link) {
1684
- const handle = product.handle || product.slug || product.id;
1685
- product.url = `/products/${handle}`;
1686
- product.link = `/products/${handle}`;
1687
-
1688
- // Debug first few products to verify URL generation
1689
- if (index < 3) {
1690
- console.log(`[CONTEXT] Product ${index + 1} URL: ${product.url} (handle: ${product.handle || 'none'}, slug: ${product.slug || 'none'}, id: ${product.id || 'none'})`);
1691
- }
1692
- }
1693
- // Ensure stock fields exist (they should from mock-data, but double-check)
1694
- if (product.stock === undefined && product.stockQuantity === undefined) {
1695
- // Calculate from variants if available
1696
- if (product.variants && product.variants.length > 0) {
1697
- const totalStock = product.variants.reduce((sum, v) => sum + (v.stock || 0), 0);
1698
- product.stock = totalStock;
1699
- product.stockQuantity = totalStock;
1700
- product.inStock = totalStock > 0;
1701
- product.available = totalStock > 0;
1702
- } else {
1703
- // Default to in stock if no stock info
1704
- product.stock = 10;
1705
- product.stockQuantity = 10;
1706
- product.inStock = true;
1707
- product.available = true;
1708
- }
1709
- }
1710
- // Ensure image fields exist (they should from mock-data, but double-check)
1711
- if (!product.thumbnailImage1 && !product.imageUrl && (!product.images || product.images.length === 0)) {
1712
- // Fallback to picsum placeholder
1713
- const imageId = product.id ? String(product.id).replace(/\D/g, '') || '1' : '1';
1714
- product.imageUrl = `https://picsum.photos/seed/${imageId}/800/800`;
1715
- product.thumbnailImage1 = {
1716
- url: product.imageUrl,
1717
- altText: product.title || product.name || 'Product image'
1718
- };
1719
- }
1720
- return product;
1721
- });
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
+ }
1722
1868
 
1723
1869
  console.log(`[CONTEXT] Loaded ${context.products.length} products`);
1724
1870
 
@@ -1746,6 +1892,18 @@ class DevServer {
1746
1892
  try {
1747
1893
  const categoriesResponse = await this.apiClient.getCategories({ limit: 50, offset: 0 });
1748
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
+
1749
1907
  context.collections = context.categories; // Also set collections for compatibility
1750
1908
  console.log(`[CONTEXT] Loaded ${context.categories.length} categories`);
1751
1909
  } catch (error) {
@@ -1976,18 +2134,35 @@ class DevServer {
1976
2134
  if (type.includes('product') || type === 'featuredproducts' || type === 'bestsellerproducts' || type === 'newproducts' || type === 'simpleproduct' || type === 'singleproduct') {
1977
2135
  const widgetProducts = this.getProductsForWidget(widget, products);
1978
2136
 
2137
+ // Products are already enriched by getProductsForWidget, but ensure they're properly formatted
2138
+ const enrichedProducts = this.enrichProductsData(widgetProducts);
2139
+
1979
2140
  // Set products at multiple levels for template compatibility
1980
- widget.data.products = widgetProducts;
1981
- widget.data.Products = widgetProducts;
1982
- widget.products = widgetProducts;
2141
+ widget.data.products = enrichedProducts;
2142
+ widget.data.Products = enrichedProducts;
2143
+ widget.products = enrichedProducts;
1983
2144
 
1984
2145
  // Also set in content for some template variations
1985
2146
  widget.data.content = widget.data.content || {};
1986
- widget.data.content.products = widgetProducts;
1987
- widget.data.content.Products = widgetProducts;
2147
+ widget.data.content.products = enrichedProducts;
2148
+ widget.data.content.Products = enrichedProducts;
1988
2149
 
1989
2150
  enrichedCount++;
1990
- 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
+ }
1991
2166
  }
1992
2167
 
1993
2168
  // Enrich CategoryList and CategoryListCarousel widgets with categories
@@ -2024,13 +2199,18 @@ class DevServer {
2024
2199
 
2025
2200
  // Enrich RecentlyViewed widgets with products (use first 6 products as mock recent views)
2026
2201
  if (type === 'recentlyviewed' || type === 'recently-viewed') {
2027
- const recentProducts = products.slice(0, 6);
2202
+ const recentProducts = this.enrichProductsData(products.slice(0, 6));
2028
2203
  widget.data.products = recentProducts;
2029
2204
  widget.data.Products = recentProducts;
2030
2205
  widget.products = recentProducts;
2031
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
+
2032
2212
  enrichedCount++;
2033
- 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`);
2034
2214
  }
2035
2215
  });
2036
2216
  });
@@ -2038,37 +2218,268 @@ class DevServer {
2038
2218
  console.log(`[CONTEXT] ✅ Enriched ${enrichedCount} widgets with data`);
2039
2219
  }
2040
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
+
2041
2451
  /**
2042
2452
  * Get products for a specific widget based on its settings
2043
2453
  */
2044
2454
  getProductsForWidget(widget, products) {
2045
2455
  // Get the limit from widget settings, default to 12
2046
- 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;
2047
2457
 
2048
2458
  // Get products based on widget type
2049
2459
  const type = (widget.type || '').toLowerCase();
2050
2460
 
2461
+ let widgetProducts = [];
2462
+
2051
2463
  if (type.includes('featured') || type === 'featuredproducts') {
2052
2464
  // Featured products - filter by featured tag or just take first N
2053
2465
  const featured = products.filter(p => p.tags?.includes('featured') || p.featured);
2054
- return featured.length > 0 ? featured.slice(0, limit) : products.slice(0, limit);
2055
- }
2056
-
2057
- 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') {
2058
2468
  // Best seller products - sort by sales or just take random N
2059
- return products.slice(0, limit);
2060
- }
2061
-
2062
- if (type.includes('new') || type === 'newproducts') {
2469
+ widgetProducts = products.slice(0, limit);
2470
+ } else if (type.includes('new') || type === 'newproducts') {
2063
2471
  // New products - sort by created date
2064
2472
  const sorted = [...products].sort((a, b) =>
2065
2473
  new Date(b.createdAt || 0) - new Date(a.createdAt || 0)
2066
2474
  );
2067
- 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);
2068
2479
  }
2069
2480
 
2070
- // Default: return first N products
2071
- return products.slice(0, limit);
2481
+ // Enrich all products with required data before returning
2482
+ return this.enrichProductsData(widgetProducts);
2072
2483
  }
2073
2484
 
2074
2485
  /**
@@ -280,13 +280,25 @@ function registerCustomTags(liquid, themePath) {
280
280
  };
281
281
 
282
282
  // Override the include tag to handle widget and snippet paths
283
+ // CRITICAL: Include tag must parse hash arguments like 'product: product' to match production behavior
283
284
  liquid.registerTag('include', {
284
285
  parse: function(tagToken, remainTokens) {
285
- // Parse arguments - handle both 'snippets/pagination' and 'snippets/pagination', pagination formats
286
+ // Parse arguments - handle 'snippets/product-card', product: product format
286
287
  const args = tagToken.args.trim();
287
- // Extract file path (first argument before comma if present)
288
- const fileMatch = args.match(/^['"]([^'"]+)['"]/);
289
- this.file = fileMatch ? fileMatch[1] : args.split(',')[0].trim().replace(/^['"]|['"]$/g, '');
288
+
289
+ // Split by comma to separate file path from hash parameters
290
+ const commaIndex = args.indexOf(',');
291
+ if (commaIndex > 0) {
292
+ this.fileArg = args.substring(0, commaIndex).trim();
293
+ this.hashArgs = args.substring(commaIndex + 1).trim();
294
+ } else {
295
+ this.fileArg = args;
296
+ this.hashArgs = '';
297
+ }
298
+
299
+ // Extract file path - remove quotes if present
300
+ const fileMatch = this.fileArg.match(/^['"]([^'"]+)['"]/);
301
+ this.file = fileMatch ? fileMatch[1] : this.fileArg.replace(/^['"]|['"]$/g, '');
290
302
  },
291
303
  render: async function(scope, hash) {
292
304
  let filePath = this.file;
@@ -298,7 +310,7 @@ function registerCustomTags(liquid, themePath) {
298
310
  // Clean up file path
299
311
  filePath = filePath.trim().replace(/^['"]|['"]$/g, '');
300
312
 
301
- // Get full context for snippets/widgets
313
+ // Get full context for snippets/widgets - merge parent scope (for shop, settings, etc.)
302
314
  const scopeContexts = Array.isArray(scope?.contexts) ? scope.contexts : [];
303
315
  const primaryScope = scopeContexts.length > 0
304
316
  ? scopeContexts[0]
@@ -309,12 +321,37 @@ function registerCustomTags(liquid, themePath) {
309
321
  ? { ...currentRenderingContext, ...primaryScope }
310
322
  : primaryScope;
311
323
 
324
+ // CRITICAL: Parse hash arguments if provided (e.g., "product: product")
325
+ // Hash params override parent scope values, matching LiquidJS include behavior
326
+ const includeContext = { ...fullContext };
327
+ if (this.hashArgs) {
328
+ const hashPairs = this.hashArgs.split(',').map(pair => pair.trim());
329
+ for (const pair of hashPairs) {
330
+ const colonIndex = pair.indexOf(':');
331
+ if (colonIndex > 0) {
332
+ const key = pair.substring(0, colonIndex).trim();
333
+ const valueVar = pair.substring(colonIndex + 1).trim();
334
+ if (key && valueVar) {
335
+ // Use scope.get() to resolve the value variable (handles loop variables correctly!)
336
+ try {
337
+ const value = await scope.get(valueVar.split('.'));
338
+ if (value !== undefined && value !== null) {
339
+ includeContext[key] = value;
340
+ }
341
+ } catch (error) {
342
+ // Silently skip unresolved hash params
343
+ }
344
+ }
345
+ }
346
+ }
347
+ }
348
+
312
349
  // Handle widget paths
313
350
  if (filePath.startsWith('widgets/')) {
314
351
  const widgetName = filePath.replace(/^widgets\//, '').replace(/\.liquid$/, '');
315
352
  const widgetPath = path.join(themePath, 'widgets', `${widgetName}.liquid`);
316
353
  if (fs.existsSync(widgetPath)) {
317
- return await liquid.parseAndRender(fs.readFileSync(widgetPath, 'utf8'), fullContext);
354
+ return await liquid.parseAndRender(fs.readFileSync(widgetPath, 'utf8'), includeContext);
318
355
  }
319
356
  }
320
357
 
@@ -323,7 +360,7 @@ function registerCustomTags(liquid, themePath) {
323
360
  const snippetName = filePath.replace(/^snippets\//, '').replace(/\.liquid$/, '');
324
361
  const snippetPath = path.join(themePath, 'snippets', `${snippetName}.liquid`);
325
362
  if (fs.existsSync(snippetPath)) {
326
- return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'), fullContext);
363
+ return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'), includeContext);
327
364
  } else {
328
365
  console.warn(`[INCLUDE] Snippet not found: ${snippetPath}`);
329
366
  }
@@ -332,19 +369,19 @@ function registerCustomTags(liquid, themePath) {
332
369
  // Try to resolve file in snippets directory (default location)
333
370
  const snippetPath = path.join(themePath, 'snippets', `${filePath}.liquid`);
334
371
  if (fs.existsSync(snippetPath)) {
335
- return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'), fullContext);
372
+ return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'), includeContext);
336
373
  }
337
374
 
338
375
  // Try direct path
339
376
  const resolvedPath = path.join(themePath, filePath);
340
377
  if (fs.existsSync(resolvedPath)) {
341
- return await liquid.parseAndRender(fs.readFileSync(resolvedPath, 'utf8'), fullContext);
378
+ return await liquid.parseAndRender(fs.readFileSync(resolvedPath, 'utf8'), includeContext);
342
379
  }
343
380
 
344
381
  // Try with .liquid extension
345
382
  const resolvedPathWithExt = `${resolvedPath}.liquid`;
346
383
  if (fs.existsSync(resolvedPathWithExt)) {
347
- return await liquid.parseAndRender(fs.readFileSync(resolvedPathWithExt, 'utf8'), fullContext);
384
+ return await liquid.parseAndRender(fs.readFileSync(resolvedPathWithExt, 'utf8'), includeContext);
348
385
  }
349
386
 
350
387
  console.warn(`[INCLUDE] File not found: ${filePath} (tried: ${snippetPath}, ${resolvedPath}, ${resolvedPathWithExt})`);
@@ -686,9 +723,44 @@ async function renderWithLayout(liquid, templatePath, context, themePath) {
686
723
  categories: context.categories?.length || 0,
687
724
  brands: context.brands?.length || 0,
688
725
  menus: context.menus?.length || 0,
689
- cart: context.cart?.itemCount || 0
726
+ cart: context.cart?.itemCount || 0,
727
+ collection: context.collection ? {
728
+ title: context.collection.title || context.collection.name,
729
+ products: context.collection.products?.length || 0
730
+ } : null
690
731
  };
691
732
  console.log(`[RENDER] ${templatePath} - Context:`, JSON.stringify(contextSummary));
733
+
734
+ // Detailed product logging
735
+ if (context.products && context.products.length > 0) {
736
+ const sampleProduct = context.products[0];
737
+ console.log(`[RENDER] ${templatePath} - Sample product data:`, {
738
+ id: sampleProduct.id,
739
+ title: sampleProduct.title || sampleProduct.name,
740
+ url: sampleProduct.url || sampleProduct.link,
741
+ price: sampleProduct.price || sampleProduct.sellingPrice,
742
+ stock: sampleProduct.stock,
743
+ inStock: sampleProduct.inStock,
744
+ hasImage: !!(sampleProduct.imageUrl || sampleProduct.thumbnailImage1),
745
+ imageUrl: sampleProduct.imageUrl || sampleProduct.thumbnailImage1?.url,
746
+ categoryId: sampleProduct.categoryId
747
+ });
748
+ } else {
749
+ console.warn(`[RENDER] ${templatePath} - ⚠️ No products in context!`);
750
+ }
751
+
752
+ // Check collection products
753
+ if (context.collection && context.collection.products) {
754
+ console.log(`[RENDER] ${templatePath} - Collection products: ${context.collection.products.length}`);
755
+ if (context.collection.products.length > 0) {
756
+ const sample = context.collection.products[0];
757
+ console.log(`[RENDER] ${templatePath} - Sample collection product:`, {
758
+ id: sample.id,
759
+ title: sample.title || sample.name,
760
+ url: sample.url || sample.link
761
+ });
762
+ }
763
+ }
692
764
 
693
765
  let templateContent = fs.readFileSync(fullTemplatePath, 'utf-8');
694
766
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o2vend/theme-cli",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
4
4
  "description": "O2VEND Theme Development CLI - Standalone tool for local theme development",
5
5
  "bin": {
6
6
  "o2vend": "./bin/o2vend"