@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.
- package/lib/lib/dev-server.js +568 -157
- package/lib/lib/liquid-engine.js +83 -11
- package/package.json +1 -1
package/lib/lib/dev-server.js
CHANGED
|
@@ -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
|
|
485
|
-
p
|
|
486
|
-
|
|
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
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
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 || [])
|
|
809
|
-
p
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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 || [])
|
|
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/
|
|
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 || [])
|
|
904
|
-
p
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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 || [])
|
|
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/
|
|
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
|
|
1418
|
+
// Found a category - render products list (NOT categories list)
|
|
1367
1419
|
context.category = category;
|
|
1368
1420
|
context.collection = category;
|
|
1369
1421
|
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
//
|
|
1377
|
-
filteredProducts.
|
|
1378
|
-
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
-
|
|
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
|
|
1556
|
+
// Try to render products template
|
|
1392
1557
|
let html;
|
|
1393
|
-
const templateOptions = ['templates/
|
|
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
|
-
//
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
product.
|
|
1699
|
-
product.
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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 =
|
|
1981
|
-
widget.data.Products =
|
|
1982
|
-
widget.products =
|
|
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 =
|
|
1987
|
-
widget.data.content.Products =
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2071
|
-
return
|
|
2481
|
+
// Enrich all products with required data before returning
|
|
2482
|
+
return this.enrichProductsData(widgetProducts);
|
|
2072
2483
|
}
|
|
2073
2484
|
|
|
2074
2485
|
/**
|
package/lib/lib/liquid-engine.js
CHANGED
|
@@ -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
|
|
286
|
+
// Parse arguments - handle 'snippets/product-card', product: product format
|
|
286
287
|
const args = tagToken.args.trim();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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'),
|
|
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'),
|
|
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'),
|
|
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'),
|
|
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'),
|
|
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
|
|