@o2vend/theme-cli 1.0.35 → 1.0.36

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.
@@ -26,6 +26,7 @@ class DevServer {
26
26
  this.mode = options.mode || 'mock'; // 'mock' or 'real'
27
27
  this.mockApiPort = options.mockApiPort || 3001;
28
28
  this.openBrowser = options.open !== false;
29
+ this.debug = options.debug || process.env.O2VEND_DEBUG === 'true' || process.env.DEBUG === 'true';
29
30
 
30
31
  this.app = express();
31
32
  this.server = null;
@@ -423,7 +424,7 @@ class DevServer {
423
424
  // Log context data for debugging
424
425
  const widgetSections = Object.keys(context.widgets || {});
425
426
  const widgetCounts = widgetSections.map(s => `${s}:${context.widgets[s]?.length || 0}`).join(', ');
426
- console.log(`[HOME] Products: ${context.products?.length || 0}, Widgets: [${widgetCounts}], Menus: ${context.menus?.length || 0}`);
427
+ console.log(`[HOME] Products: ${context.products?.length || 0}, Widgets: [${widgetCounts}], Menus: ${Object.keys(context.menus || {}).length}`);
427
428
 
428
429
  // Ensure products are available
429
430
  if (!context.products || context.products.length === 0) {
@@ -453,18 +454,15 @@ class DevServer {
453
454
  if (this.mode === 'real') {
454
455
  const opts = { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} };
455
456
  context.widgets = await webstoreApi.fetchWidgetsBySections('home', webstoreApi.DEFAULT_SECTIONS, opts) || { hero: [], content: [], footer: [] };
456
- console.log(`[HOME] Reloaded widgets: ${Object.keys(context.widgets).join(', ')} (real)`);
457
+ console.log(`[HOME] Reloaded widgets (real)`);
457
458
  } else if (this.widgetService) {
458
- const widgets = await this.widgetService.getWidgetsBySections();
459
- context.widgets = widgets || { hero: [], content: [], footer: [] };
460
- console.log(`[HOME] Reloaded widgets: ${Object.keys(context.widgets).join(', ')}`);
459
+ context.widgets = await this.widgetService.fetchPageWidgets('home');
461
460
  } else {
462
- console.error('[HOME] Widget service not available');
463
- context.widgets = { hero: [], content: [], footer: [] };
461
+ context.widgets = { header: [], hero: [], content: [], footer: [] };
464
462
  }
465
463
  } catch (error) {
466
464
  console.error('[HOME] Failed to reload widgets:', error.message);
467
- context.widgets = { hero: [], content: [], footer: [] };
465
+ context.widgets = { header: [], hero: [], content: [], footer: [] };
468
466
  }
469
467
  }
470
468
 
@@ -541,7 +539,7 @@ class DevServer {
541
539
  price: sample.price || sample.sellingPrice,
542
540
  stock: sample.stock,
543
541
  inStock: sample.inStock,
544
- hasImage: !!(sample.imageUrl || sample.thumbnailImage1)
542
+ hasImage: !!(sample.imageUrl || sample.thumbnailImage)
545
543
  });
546
544
  }
547
545
 
@@ -550,7 +548,7 @@ class DevServer {
550
548
  console.log(` - Products count: ${context.products?.length || 0}`);
551
549
  console.log(` - Widgets sections: ${Object.keys(context.widgets || {}).join(', ') || 'none'}`);
552
550
  console.log(` - Widgets counts: ${Object.keys(context.widgets || {}).map(s => `${s}:${context.widgets[s]?.length || 0}`).join(', ') || 'none'}`);
553
- console.log(` - Menus count: ${context.menus?.length || 0}`);
551
+ console.log(` - Menus count: ${Object.keys(context.menus || {}).length}`);
554
552
  console.log(` - Categories count: ${context.categories?.length || 0}`);
555
553
  console.log(` - Brands count: ${context.brands?.length || 0}`);
556
554
 
@@ -597,7 +595,7 @@ class DevServer {
597
595
  if (context.products.length > 0) {
598
596
  const sampleProduct = context.products[0];
599
597
  const hasStock = 'stock' in sampleProduct || 'quantity' in sampleProduct;
600
- const hasImage = sampleProduct.thumbnailImage1 || sampleProduct.imageUrl || (sampleProduct.images && sampleProduct.images.length > 0);
598
+ const hasImage = sampleProduct.thumbnailImage || sampleProduct.imageUrl || (sampleProduct.images && sampleProduct.images.length > 0);
601
599
  const hasUrl = sampleProduct.url || sampleProduct.link;
602
600
  // Use nullish coalescing to show 0 values correctly
603
601
  const stockValue = sampleProduct.stock ?? sampleProduct.quantity ?? 'N/A';
@@ -612,46 +610,25 @@ class DevServer {
612
610
  }
613
611
  }
614
612
 
615
- // Ensure widgets are available (they should be from buildContext, but double-check)
616
613
  if (!context.widgets || Object.keys(context.widgets).length === 0) {
617
- console.warn('[PRODUCTS PAGE] ⚠️ No widgets in context, attempting to reload...');
614
+ console.warn('[PRODUCTS PAGE] No widgets in context, attempting reload...');
618
615
  try {
619
616
  if (this.mode === 'real') {
620
617
  const opts = { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} };
621
- context.widgets = await webstoreApi.fetchWidgetsBySections('products', webstoreApi.DEFAULT_SECTIONS, opts) || { hero: [], content: [], footer: [], header: [] };
622
- console.log(`[PRODUCTS PAGE] ✅ Reloaded widgets: ${Object.keys(context.widgets).join(', ')} (real)`);
618
+ context.widgets = await webstoreApi.fetchWidgetsBySections('products', webstoreApi.DEFAULT_SECTIONS, opts) || { header: [], hero: [], content: [], footer: [] };
623
619
  } else if (this.widgetService) {
624
- const widgets = await this.widgetService.getWidgetsBySections();
625
- context.widgets = widgets || { hero: [], content: [], footer: [], header: [] };
626
- console.log(`[PRODUCTS PAGE] ✅ Reloaded widgets: ${Object.keys(context.widgets).join(', ')}`);
620
+ context.widgets = await this.widgetService.fetchPageWidgets('products');
627
621
  } else {
628
- context.widgets = { hero: [], content: [], footer: [], header: [] };
622
+ context.widgets = { header: [], hero: [], content: [], footer: [] };
629
623
  }
630
624
  } catch (error) {
631
- console.error('[PRODUCTS PAGE] Failed to reload widgets:', error.message);
632
- context.widgets = { hero: [], content: [], footer: [], header: [] };
625
+ console.error('[PRODUCTS PAGE] Failed to reload widgets:', error.message);
626
+ context.widgets = { header: [], hero: [], content: [], footer: [] };
633
627
  }
634
628
  }
635
629
 
636
- // Ensure menus are available
637
- if (!context.menus || context.menus.length === 0) {
638
- console.warn('[PRODUCTS PAGE] ⚠️ No menus in context, attempting to reload...');
639
- try {
640
- if (this.mode === 'real') {
641
- context.menus = await webstoreApi.fetchMenus({ headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} });
642
- context.menus = Array.isArray(context.menus) ? context.menus : [];
643
- console.log(`[PRODUCTS PAGE] ✅ Reloaded ${context.menus.length} menus (real)`);
644
- } else {
645
- const axios = require('axios');
646
- const menusResponse = await axios.get(`http://localhost:${this.mockApiPort}/webstoreapi/menus`);
647
- const menusData = menusResponse.data;
648
- context.menus = Array.isArray(menusData) ? menusData : (menusData.data?.menus || menusData.menus || []);
649
- console.log(`[PRODUCTS PAGE] ✅ Reloaded ${context.menus.length} menus`);
650
- }
651
- } catch (error) {
652
- console.error('[PRODUCTS PAGE] ❌ Failed to reload menus:', error.message);
653
- context.menus = [];
654
- }
630
+ if (!context.menus || Object.keys(context.menus).length === 0) {
631
+ console.warn('[PRODUCTS PAGE] No menus in context');
655
632
  }
656
633
 
657
634
  // Ensure collection.products is set for products template
@@ -690,9 +667,9 @@ class DevServer {
690
667
  stockQuantity: sample.stockQuantity !== undefined ? sample.stockQuantity : 'MISSING',
691
668
  inStock: sample.inStock !== undefined ? sample.inStock : 'MISSING',
692
669
  available: sample.available !== undefined ? sample.available : 'MISSING',
693
- hasImage: !!(sample.imageUrl || sample.thumbnailImage1),
694
- imageUrl: sample.imageUrl || sample.thumbnailImage1?.url || 'MISSING',
695
- hasThumbnailImage1: !!sample.thumbnailImage1,
670
+ hasImage: !!(sample.imageUrl || sample.thumbnailImage),
671
+ imageUrl: sample.imageUrl || sample.thumbnailImage?.url || 'MISSING',
672
+ hasThumbnailImage: !!sample.thumbnailImage,
696
673
  hasImagesArray: !!(sample.images && sample.images.length > 0)
697
674
  });
698
675
  } else {
@@ -714,10 +691,10 @@ class DevServer {
714
691
  title: firstProduct.title || firstProduct.name,
715
692
  url: firstProduct.url || firstProduct.link,
716
693
  price: firstProduct.price || firstProduct.sellingPrice,
717
- stock: firstProduct.stock,
694
+ stockQuantity: firstProduct.stockQuantity,
718
695
  inStock: firstProduct.inStock,
719
- hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage1),
720
- imageUrl: firstProduct.imageUrl || firstProduct.thumbnailImage1?.url
696
+ hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage),
697
+ imageUrl: firstProduct.imageUrl || firstProduct.thumbnailImage
721
698
  });
722
699
  }
723
700
  if (context.collection) {
@@ -869,31 +846,8 @@ class DevServer {
869
846
  }
870
847
  });
871
848
 
872
- // Collections listing page (uses categories.liquid template per webstore)
873
- this.app.get('/collections', async (req, res, next) => {
874
- try {
875
- const context = await this.buildContext(req, 'collections');
876
- // Try categories.liquid first (as per webstore), then collections.liquid, then collection.liquid
877
- let html;
878
- try {
879
- html = await renderWithLayout(this.liquid, 'templates/categories', context, this.themePath);
880
- } catch (error) {
881
- if (error.message && error.message.includes('Template not found')) {
882
- try {
883
- html = await renderWithLayout(this.liquid, 'templates/collections', context, this.themePath);
884
- } catch (error2) {
885
- html = await renderWithLayout(this.liquid, 'templates/collection', context, this.themePath);
886
- }
887
- } else {
888
- throw error;
889
- }
890
- }
891
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
892
- res.setHeader('Cache-Control', 'no-cache');
893
- res.send(html);
894
- } catch (error) {
895
- next(error);
896
- }
849
+ this.app.get('/collections', (req, res) => {
850
+ res.redirect(301, '/categories');
897
851
  });
898
852
 
899
853
  // Collection detail page - Try categories first, then collection
@@ -1209,6 +1163,55 @@ class DevServer {
1209
1163
  }
1210
1164
  });
1211
1165
 
1166
+ this.app.get('/dev/context', async (req, res, next) => {
1167
+ try {
1168
+ const pageType = req.query.page || 'home';
1169
+ const context = await this.buildContext(req, pageType);
1170
+ const debugOutput = {
1171
+ mode: this.mode,
1172
+ pageType,
1173
+ shopKeys: Object.keys(context.shop || {}),
1174
+ shop: context.shop,
1175
+ store: context.store,
1176
+ menusKeys: Object.keys(context.menus || {}),
1177
+ mainMenu: context.mainMenu ? { name: context.mainMenu.name, itemCount: context.mainMenu.items?.length } : null,
1178
+ widgetSections: Object.fromEntries(
1179
+ Object.entries(context.widgets || {}).map(([k, v]) =>
1180
+ [k, Array.isArray(v) ? v.map(w => ({ type: w.type, id: w.id, template_path: w.template_path })) : v]
1181
+ )
1182
+ ),
1183
+ productsCount: (context.products || []).length,
1184
+ categoriesCount: (context.categories || []).length,
1185
+ brandsCount: (context.brands || []).length,
1186
+ cart: context.cart,
1187
+ customer: context.customer,
1188
+ request: context.request,
1189
+ deliveryZone: context.deliveryZone,
1190
+ analytics: context.analytics,
1191
+ settingsKeys: Object.keys(context.settings || {}),
1192
+ sampleProduct: (context.products || [])[0] ? {
1193
+ id: context.products[0].id,
1194
+ slug: context.products[0].slug,
1195
+ url: context.products[0].url,
1196
+ stockQuantity: context.products[0].stockQuantity,
1197
+ inStock: context.products[0].inStock,
1198
+ available: context.products[0].available,
1199
+ prices: context.products[0].prices,
1200
+ thumbnailImage: context.products[0].thumbnailImage
1201
+ } : null,
1202
+ sampleCategory: (context.categories || [])[0] ? {
1203
+ id: context.categories[0].id,
1204
+ slug: context.categories[0].slug,
1205
+ handle: context.categories[0].handle,
1206
+ thumbnailImage: context.categories[0].thumbnailImage
1207
+ } : null
1208
+ };
1209
+ res.json(debugOutput);
1210
+ } catch (error) {
1211
+ next(error);
1212
+ }
1213
+ });
1214
+
1212
1215
  // Development Dashboard - List all theme components
1213
1216
  this.app.get('/dev', async (req, res, next) => {
1214
1217
  try {
@@ -1447,8 +1450,8 @@ class DevServer {
1447
1450
  price: product.price || product.sellingPrice,
1448
1451
  stock: product.stock,
1449
1452
  inStock: product.inStock,
1450
- hasImage: !!(product.imageUrl || product.thumbnailImage1),
1451
- imageUrl: product.imageUrl || product.thumbnailImage1?.url
1453
+ hasImage: !!(product.imageUrl || product.thumbnailImage),
1454
+ imageUrl: product.imageUrl || product.thumbnailImage?.url
1452
1455
  });
1453
1456
  });
1454
1457
  }
@@ -1477,8 +1480,8 @@ class DevServer {
1477
1480
  price: sample.price || sample.sellingPrice,
1478
1481
  stock: sample.stock,
1479
1482
  inStock: sample.inStock,
1480
- hasImage: !!(sample.imageUrl || sample.thumbnailImage1),
1481
- imageUrl: sample.imageUrl || sample.thumbnailImage1?.url,
1483
+ hasImage: !!(sample.imageUrl || sample.thumbnailImage),
1484
+ imageUrl: sample.imageUrl || sample.thumbnailImage?.url,
1482
1485
  allKeys: Object.keys(sample).slice(0, 20)
1483
1486
  });
1484
1487
  } else {
@@ -1503,9 +1506,9 @@ class DevServer {
1503
1506
  title: firstProduct.title || firstProduct.name,
1504
1507
  url: firstProduct.url || firstProduct.link,
1505
1508
  price: firstProduct.price || firstProduct.sellingPrice,
1506
- stock: firstProduct.stock,
1509
+ stockQuantity: firstProduct.stockQuantity,
1507
1510
  inStock: firstProduct.inStock,
1508
- hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage1)
1511
+ hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage)
1509
1512
  });
1510
1513
  }
1511
1514
 
@@ -1601,10 +1604,31 @@ class DevServer {
1601
1604
  }
1602
1605
  }
1603
1606
 
1604
- // Not found - pass to next middleware (will eventually 404)
1605
- next();
1607
+ // Not a category, brand, or product -- try as a CMS page
1608
+ {
1609
+ const pageContext = await this.buildContext(req, 'page', { pageHandle: handle });
1610
+ pageContext.page = { title: handle.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()), handle: handle };
1611
+ let html;
1612
+ const pageTemplates = ['templates/page', 'templates/pages'];
1613
+ for (const template of pageTemplates) {
1614
+ try {
1615
+ html = await renderWithLayout(this.liquid, template, pageContext, this.themePath);
1616
+ break;
1617
+ } catch (error) {
1618
+ if (!error.message?.includes('Template not found')) throw error;
1619
+ }
1620
+ }
1621
+ if (html) {
1622
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
1623
+ res.setHeader('Cache-Control', 'no-cache');
1624
+ return res.send(html);
1625
+ }
1626
+ }
1627
+
1628
+ // Not found - 404
1629
+ res.status(404).send(`<!DOCTYPE html><html><head><title>Page Not Found</title></head><body><h1>Page Not Found</h1><p>The page "${handle}" was not found.</p><p><a href="/">Back to Home</a></p></body></html>`);
1606
1630
  } catch (error) {
1607
- // If there's an error, pass to next middleware
1631
+ console.error(`[CATCH-ALL] Error handling /${req.params?.handle}:`, error.message);
1608
1632
  next(error);
1609
1633
  }
1610
1634
  });
@@ -1688,13 +1712,10 @@ class DevServer {
1688
1712
  * - If template expects 'quantity' instead of 'stock', consider adding both fields
1689
1713
  *
1690
1714
  * 2. WIDGETS LOADING:
1691
- * - Widgets are loaded via widgetService.getWidgetsBySections()
1715
+ * - Widgets are loaded via widgetService.fetchPageWidgets(pageId)
1692
1716
  * - Widgets are organized by section (header, hero, content, footer)
1693
- * - Widgets should be available for ALL page types (home, products, collection, etc.)
1694
- * - If widgets are missing, check:
1695
- * a) widgetService is initialized (should be created in setupServices)
1696
- * b) Mock API is returning widgets correctly
1697
- * c) Widget service is properly organizing widgets by section
1717
+ * - Widgets are filtered by pageId to match production behavior
1718
+ * - pageId is derived from pageType: home->home, product->product, collection->category, etc.
1698
1719
  *
1699
1720
  * 3. MENUS LOADING:
1700
1721
  * - Menus are loaded from /webstoreapi/menus endpoint
@@ -1709,8 +1730,10 @@ class DevServer {
1709
1730
  * @returns {Promise<Object>} Template context
1710
1731
  */
1711
1732
  async buildContext(req, pageType, extra = {}) {
1733
+ const hostname = `${this.host}:${this.port}`;
1712
1734
  const context = {
1713
1735
  shop: {},
1736
+ store: {},
1714
1737
  tenant: {},
1715
1738
  page: {
1716
1739
  type: pageType,
@@ -1720,29 +1743,40 @@ class DevServer {
1720
1743
  collections: [],
1721
1744
  categories: [],
1722
1745
  brands: [],
1723
- cart: {
1746
+ cart: {
1724
1747
  id: 'mock-cart-1',
1725
- items: [],
1726
- total: 0,
1727
- itemCount: 0,
1728
- currency: 'USD',
1729
- subtotal: 0,
1730
- tax: 0,
1731
- shipping: 0
1732
- },
1733
- // Mock authenticated customer for dev mode (allows add-to-cart without login)
1734
- customer: {
1735
- id: 'mock-customer-1',
1736
- isAuthenticated: true,
1737
- firstName: 'Test',
1738
- lastName: 'User',
1739
- email: 'test@example.com',
1740
- phone: '+1234567890',
1741
- name: 'Test User'
1748
+ items: [],
1749
+ total: 0,
1750
+ subTotal: 0,
1751
+ taxAmount: 0,
1752
+ shippingAmount: 0,
1753
+ itemCount: 0
1742
1754
  },
1755
+ customer: null,
1743
1756
  widgets: {},
1744
1757
  settings: {},
1745
- menus: [],
1758
+ menus: {},
1759
+ mainMenu: null,
1760
+ request: {
1761
+ path: req.path,
1762
+ url: req.url,
1763
+ host: hostname
1764
+ },
1765
+ deliveryZone: {
1766
+ enabled: false,
1767
+ mode: 0,
1768
+ current: null,
1769
+ zipcodeSearchEnabled: false,
1770
+ defaultZipcode: null,
1771
+ showModal: false
1772
+ },
1773
+ analytics: {
1774
+ enabled: false,
1775
+ baseUrl: null,
1776
+ siteId: null,
1777
+ visitorId: null
1778
+ },
1779
+ content_for_header: '',
1746
1780
  ...extra
1747
1781
  };
1748
1782
 
@@ -1770,63 +1804,54 @@ class DevServer {
1770
1804
  // Use mock API (API client points to mock API via proxy)
1771
1805
  try {
1772
1806
  const storeInfo = await this.apiClient.getStoreInfo(true);
1773
- context.shop = storeInfo || {
1774
- name: 'My Store',
1775
- description: 'O2VEND Store',
1776
- domain: 'localhost:3000',
1777
- email: 'store@example.com'
1807
+ const si = storeInfo || {};
1808
+ const storeSettings = si.settings || {};
1809
+ context.shop = {
1810
+ name: si.name || 'My Store',
1811
+ domain: si.domain || hostname,
1812
+ currency: storeSettings.currency || 'USD',
1813
+ currencySymbol: storeSettings.currencySymbol || '$',
1814
+ timezone: storeSettings.timezone || si.timezone || 'America/New_York',
1815
+ language: storeSettings.language || si.language || 'en',
1816
+ logo: si.logoUrl || storeSettings.logo || '/assets/logo.png',
1817
+ favicon: si.favouriteIconUrl || storeSettings.favicon || '/favicon.ico',
1818
+ id: si.id || 1,
1819
+ identifier: si.identifier || 'store',
1820
+ companyName: si.companyName || si.name || 'My Store',
1821
+ companyAddress: si.companyAddress || '',
1822
+ companyPhoneNumber: si.companyPhoneNumber || si.phone || '',
1823
+ companyEmail: si.companyEmail || si.email || '',
1824
+ settings: storeSettings
1825
+ };
1826
+ context.store = {
1827
+ id: context.shop.id,
1828
+ identifier: context.shop.identifier,
1829
+ name: context.shop.name,
1830
+ settings: storeSettings,
1831
+ companyName: context.shop.companyName,
1832
+ companyAddress: context.shop.companyAddress,
1833
+ companyPhoneNumber: context.shop.companyPhoneNumber,
1834
+ companyEmail: context.shop.companyEmail
1835
+ };
1836
+ context.tenant = {
1837
+ id: si.id || 1,
1838
+ name: context.shop.name,
1839
+ settings: storeSettings
1778
1840
  };
1779
1841
 
1780
- // Get widgets for sections from mock API
1781
- // NOTE: Widgets should be loaded for ALL page types (home, products, collection, etc.)
1782
- // The widget service filters widgets by section, so all sections should be available
1842
+ // Load widgets filtered by page type (matches production behavior)
1843
+ const pageIdMap = { home: 'home', product: 'product', collection: 'category', category: 'category', products: 'products', categories: 'categories', brand: 'brand', checkout: 'checkout', page: 'home' };
1844
+ const widgetPageId = pageIdMap[pageType] || 'home';
1783
1845
  if (this.widgetService) {
1784
1846
  try {
1785
- console.log(`[CONTEXT] Loading widgets for page type: ${pageType}`);
1786
- const widgets = await this.widgetService.getWidgetsBySections();
1787
- context.widgets = widgets || {
1788
- hero: [],
1789
- products: [],
1790
- footer: [],
1791
- content: [],
1792
- header: []
1793
- };
1794
- // Log widget counts by section
1795
- const widgetCounts = Object.keys(context.widgets).map(section =>
1796
- `${section}: ${context.widgets[section]?.length || 0}`
1797
- ).join(', ');
1798
- console.log(`[CONTEXT] ✅ Widgets loaded: ${widgetCounts}`);
1799
-
1800
- // DEBUG: Log detailed widget information
1801
- Object.keys(context.widgets).forEach(section => {
1802
- if (context.widgets[section] && context.widgets[section].length > 0) {
1803
- console.log(`[CONTEXT DEBUG] Section '${section}' has ${context.widgets[section].length} widget(s):`);
1804
- context.widgets[section].slice(0, 3).forEach((w, idx) => {
1805
- console.log(` - Widget ${idx + 1}: ${w.type || 'Unknown'} (ID: ${w.id || 'no-id'})`);
1806
- });
1807
- }
1808
- });
1847
+ console.log(`[CONTEXT] Loading widgets for page type: ${pageType} (pageId: ${widgetPageId})`);
1848
+ context.widgets = await this.widgetService.fetchPageWidgets(widgetPageId);
1809
1849
  } catch (error) {
1810
- console.error('[CONTEXT] Failed to load widgets:', error.message);
1811
- console.error('[CONTEXT] Error stack:', error.stack);
1812
- context.widgets = {
1813
- hero: [],
1814
- products: [],
1815
- footer: [],
1816
- content: [],
1817
- header: []
1818
- };
1850
+ console.error('[CONTEXT] Failed to load widgets:', error.message);
1851
+ context.widgets = { header: [], hero: [], content: [], footer: [] };
1819
1852
  }
1820
1853
  } else {
1821
- console.warn('[CONTEXT] ⚠️ Widget service not available, using empty widgets');
1822
- console.warn('[CONTEXT] This may happen if widgetService was not initialized properly');
1823
- context.widgets = {
1824
- hero: [],
1825
- products: [],
1826
- footer: [],
1827
- content: [],
1828
- header: []
1829
- };
1854
+ context.widgets = { header: [], hero: [], content: [], footer: [] };
1830
1855
  }
1831
1856
 
1832
1857
  // Always load products, categories, brands, menus, and cart for navigation and widgets
@@ -1853,32 +1878,17 @@ class DevServer {
1853
1878
  stockQuantity: product.stockQuantity !== undefined ? product.stockQuantity : 'MISSING',
1854
1879
  inStock: product.inStock !== undefined ? product.inStock : 'MISSING',
1855
1880
  available: product.available !== undefined ? product.available : 'MISSING',
1856
- hasImage: !!(product.imageUrl || product.thumbnailImage1),
1857
- imageUrl: product.imageUrl || product.thumbnailImage1?.url || 'MISSING'
1881
+ hasImage: !!(product.imageUrl || product.thumbnailImage),
1882
+ imageUrl: product.imageUrl || product.thumbnailImage?.url || 'MISSING'
1858
1883
  });
1859
1884
  });
1860
1885
  }
1861
1886
 
1862
1887
  console.log(`[CONTEXT] Loaded ${context.products.length} products`);
1863
1888
 
1864
- // DEBUG: Verify products have quantity/stock field
1865
1889
  if (context.products.length > 0) {
1866
- const firstProduct = context.products[0];
1867
- const hasStock = 'stock' in firstProduct || 'quantity' in firstProduct;
1868
- // Use nullish coalescing to show 0 values correctly (0 || 'N/A' would show N/A incorrectly)
1869
- const stockValue = firstProduct.stock ?? firstProduct.quantity ?? 'N/A';
1870
- console.log(`[CONTEXT DEBUG] First product sample - Has stock field: ${hasStock}, Stock value: ${stockValue}`);
1871
- console.log(`[CONTEXT DEBUG] First product keys: ${Object.keys(firstProduct).join(', ')}`);
1872
- console.log(`[CONTEXT DEBUG] First product stock details: stock=${firstProduct.stock}, quantity=${firstProduct.quantity}, inStock=${firstProduct.inStock}`);
1873
-
1874
- // Check if variants have stock
1875
- if (firstProduct.variants && firstProduct.variants.length > 0) {
1876
- const firstVariant = firstProduct.variants[0];
1877
- const variantHasStock = 'stock' in firstVariant || 'quantity' in firstVariant;
1878
- const variantStockValue = firstVariant.stock ?? firstVariant.quantity ?? 'N/A';
1879
- console.log(`[CONTEXT DEBUG] First variant sample - Has stock field: ${variantHasStock}, Stock value: ${variantStockValue}`);
1880
- console.log(`[CONTEXT DEBUG] First variant stock details: stock=${firstVariant.stock}, quantity=${firstVariant.quantity}, inStock=${firstVariant.inStock}, available=${firstVariant.available}`);
1881
- }
1890
+ const fp = context.products[0];
1891
+ console.log(`[CONTEXT] First product: id=${fp.id}, slug=${fp.slug}, stockQuantity=${fp.stockQuantity}, inStock=${fp.inStock}`);
1882
1892
  }
1883
1893
 
1884
1894
  // Load categories for navigation
@@ -1886,13 +1896,12 @@ class DevServer {
1886
1896
  const categoriesResponse = await this.apiClient.getCategories({ limit: 50, offset: 0 });
1887
1897
  context.categories = categoriesResponse.categories || categoriesResponse.data?.categories || categoriesResponse || [];
1888
1898
 
1889
- // Ensure all categories have proper URL fields for navigation
1890
1899
  context.categories = context.categories.map(category => {
1891
- // Set URL to category handle (e.g., /accessories)
1892
- if (!category.url && !category.link) {
1893
- const handle = category.handle || category.name?.toLowerCase().replace(/\s+/g, '-') || category.id;
1894
- category.url = `/${handle}`;
1895
- category.link = `/${handle}`;
1900
+ if (!category.slug) {
1901
+ category.slug = category.handle || category.name?.toLowerCase().replace(/\s+/g, '-') || String(category.id);
1902
+ }
1903
+ if (!category.handle) {
1904
+ category.handle = category.slug;
1896
1905
  }
1897
1906
  return category;
1898
1907
  });
@@ -1915,39 +1924,41 @@ class DevServer {
1915
1924
  context.brands = [];
1916
1925
  }
1917
1926
 
1918
- // Load menus (via webstoreapi) - use axios from apiClient
1919
- // NOTE: Menus are loaded for navigation in header/footer sections
1920
- // They should be available in context for all page types
1921
1927
  try {
1922
1928
  const axios = require('axios');
1923
- console.log(`[CONTEXT] Loading menus from http://localhost:${this.mockApiPort}/webstoreapi/menus`);
1924
1929
  const menusResponse = await axios.get(`http://localhost:${this.mockApiPort}/webstoreapi/menus`);
1925
- const menusData = menusResponse.data;
1926
- context.menus = Array.isArray(menusData) ? menusData : (menusData.data?.menus || menusData.menus || []);
1927
- console.log(`[CONTEXT] ✅ Loaded ${context.menus.length} menus`);
1928
-
1929
- // DEBUG: Log menu details
1930
- if (context.menus.length > 0) {
1931
- context.menus.forEach((menu, idx) => {
1932
- const itemCount = menu.items?.length || 0;
1933
- console.log(`[CONTEXT DEBUG] Menu ${idx + 1}: ${menu.name || menu.id || 'unnamed'} (${itemCount} items)`);
1934
- });
1935
- } else {
1936
- console.warn('[CONTEXT DEBUG] ⚠️ No menus loaded - menus array is empty');
1930
+ const rawMenus = menusResponse.data;
1931
+ const allMenus = Array.isArray(rawMenus) ? rawMenus : (rawMenus.data?.menus || rawMenus.menus || []);
1932
+
1933
+ const menusData = {};
1934
+ allMenus.forEach(menu => {
1935
+ if (menu && menu.name) {
1936
+ const normalizedName = menu.name.toLowerCase()
1937
+ .replace(/\s+menu$/i, '')
1938
+ .replace(/\s+/g, '_')
1939
+ .replace(/[^a-z0-9_]/g, '');
1940
+ menusData[normalizedName] = menu;
1941
+ }
1942
+ });
1943
+ menusData.all = allMenus;
1944
+ context.menus = menusData;
1945
+
1946
+ const mainMenuData = allMenus.find(menu => menu && menu.type === 'Main Menu');
1947
+ if (mainMenuData) {
1948
+ if (mainMenuData.items && Array.isArray(mainMenuData.items)) {
1949
+ mainMenuData.items.sort((a, b) => (a?.displayOrder || 0) - (b?.displayOrder || 0));
1950
+ }
1951
+ context.mainMenu = mainMenuData;
1952
+ } else if (allMenus.length > 0) {
1953
+ context.mainMenu = allMenus[0];
1937
1954
  }
1938
- } catch (error) {
1939
- console.error('[CONTEXT] ❌ Failed to load menus:', error.message);
1940
- console.error('[CONTEXT] Error details:', error.response?.status, error.response?.data);
1941
- context.menus = [];
1942
- }
1943
1955
 
1944
- // Set mainMenu for mega-menu fallback (header when no HeaderMenu widget)
1945
- let mainMenu = null;
1946
- if (context.menus && context.menus.length > 0) {
1947
- const main = context.menus.find((m) => (m.type || '').toLowerCase().trim() === 'main menu');
1948
- mainMenu = main || context.menus[0];
1956
+ console.log(`[CONTEXT] Loaded ${allMenus.length} menus`);
1957
+ } catch (error) {
1958
+ console.error('[CONTEXT] Failed to load menus:', error.message);
1959
+ context.menus = {};
1960
+ context.mainMenu = null;
1949
1961
  }
1950
- context.mainMenu = mainMenu;
1951
1962
 
1952
1963
  // Load cart data
1953
1964
  try {
@@ -2030,18 +2041,27 @@ class DevServer {
2030
2041
  }
2031
2042
  } catch (error) {
2032
2043
  console.warn('[CONTEXT] Failed to load from mock API:', error.message);
2033
- // Fallback to default values
2034
2044
  context.shop = {
2035
2045
  name: 'My Store',
2036
- description: 'O2VEND Store',
2037
- domain: 'localhost:3000',
2038
- email: 'store@example.com'
2046
+ domain: hostname,
2047
+ currency: 'USD',
2048
+ timezone: 'America/New_York',
2049
+ language: 'en',
2050
+ logo: '/assets/logo.png',
2051
+ favicon: '/favicon.ico',
2052
+ id: 1,
2053
+ identifier: 'store',
2054
+ companyName: 'My Store',
2055
+ companyAddress: '',
2056
+ companyPhoneNumber: '',
2057
+ companyEmail: '',
2058
+ settings: {}
2039
2059
  };
2040
2060
  context.widgets = {
2041
2061
  hero: [],
2042
- products: [],
2062
+ content: [],
2043
2063
  footer: [],
2044
- content: []
2064
+ header: []
2045
2065
  };
2046
2066
  }
2047
2067
  } else if (this.mode === 'real') {
@@ -2063,21 +2083,35 @@ class DevServer {
2063
2083
  context.shop = {
2064
2084
  name: storeName,
2065
2085
  domain: baseHost,
2066
- description: 'O2VEND Store',
2067
- email: storeSettings.companyEmail || 'store@example.com',
2068
- logo,
2069
- favicon,
2070
2086
  currency: storeSettings.currency || themeSettings.currency || 'USD',
2087
+ currencySymbol: storeSettings.currencySymbol || '$',
2088
+ timezone: storeSettings.timezone || 'America/New_York',
2071
2089
  language: storeSettings.language || themeSettings.language || 'en',
2072
- locale: themeSettings.locale || 'en-US',
2073
- settings: {
2074
- currency: storeSettings.currency || themeSettings.currency || 'USD',
2075
- currencySymbol: storeSettings.currencySymbol || storeSettings.currency_symbol || themeSettings.currency_symbol || themeSettings.currencySymbol || '$',
2076
- logo,
2077
- favicon
2078
- }
2090
+ logo,
2091
+ favicon,
2092
+ id: storeInfo?.id || process.env.O2VEND_TENANT_ID || 1,
2093
+ identifier: storeInfo?.identifier || 'store',
2094
+ companyName: storeSettings.companyName || storeName,
2095
+ companyAddress: storeSettings.companyAddress || '',
2096
+ companyPhoneNumber: storeSettings.companyPhoneNumber || '',
2097
+ companyEmail: storeSettings.companyEmail || '',
2098
+ settings: storeSettings
2099
+ };
2100
+ context.store = {
2101
+ id: context.shop.id,
2102
+ identifier: context.shop.identifier,
2103
+ name: context.shop.name,
2104
+ settings: storeSettings,
2105
+ companyName: context.shop.companyName,
2106
+ companyAddress: context.shop.companyAddress,
2107
+ companyPhoneNumber: context.shop.companyPhoneNumber,
2108
+ companyEmail: context.shop.companyEmail
2109
+ };
2110
+ context.tenant = {
2111
+ id: process.env.O2VEND_TENANT_ID || 1,
2112
+ name: storeName,
2113
+ settings: storeSettings
2079
2114
  };
2080
- context.tenant = { id: process.env.O2VEND_TENANT_ID || 'real' };
2081
2115
 
2082
2116
  if (process.env.O2VEND_LAYOUT_FULL === '1' || process.env.O2VEND_LAYOUT_FULL === 'true') {
2083
2117
  context.settings = context.settings || {};
@@ -2111,12 +2145,10 @@ class DevServer {
2111
2145
  const catRes = await webstoreApi.fetchCategories({ limit: 50, offset: 0 }, opts);
2112
2146
  const raw = catRes?.categories || catRes?.data?.categories || catRes || [];
2113
2147
  context.categories = raw.map((c) => {
2114
- const h = (c.handle || (c.name || '').toLowerCase().replace(/\s+/g, '-') || c.id || '').toString().replace(/^\s+|\s+$/g, '') || null;
2148
+ const h = (c.handle || (c.name || '').toLowerCase().replace(/\s+/g, '-') || c.id || '').toString().trim() || null;
2115
2149
  if (h) {
2116
2150
  c.handle = c.handle || h;
2117
2151
  c.slug = c.slug || h;
2118
- c.url = `/${h}`;
2119
- c.link = `/${h}`;
2120
2152
  }
2121
2153
  return c;
2122
2154
  });
@@ -2134,23 +2166,36 @@ class DevServer {
2134
2166
  }
2135
2167
 
2136
2168
  try {
2137
- context.menus = await webstoreApi.fetchMenus(opts);
2138
- context.menus = Array.isArray(context.menus) ? context.menus : [];
2139
- let mainMenu = null;
2140
- if (context.menus.length > 0) {
2141
- const main = context.menus.find((m) => (m.type || '').toLowerCase().trim() === 'main menu');
2142
- const candidate = main || context.menus[0];
2143
- if (candidate && candidate.id) {
2144
- try {
2145
- mainMenu = await webstoreApi.fetchMenuById(candidate.id, opts);
2146
- } catch (e) {
2147
- /* ignore */
2169
+ const allMenus = await webstoreApi.fetchMenus(opts);
2170
+ const menusList = Array.isArray(allMenus) ? allMenus : [];
2171
+ const menusData = {};
2172
+ menusList.forEach(menu => {
2173
+ if (menu && menu.name) {
2174
+ const normalizedName = menu.name.toLowerCase()
2175
+ .replace(/\s+menu$/i, '')
2176
+ .replace(/\s+/g, '_')
2177
+ .replace(/[^a-z0-9_]/g, '');
2178
+ menusData[normalizedName] = menu;
2179
+ }
2180
+ });
2181
+ menusData.all = menusList;
2182
+ context.menus = menusData;
2183
+
2184
+ const mainMenuData = menusList.find(m => m && m.type === 'Main Menu');
2185
+ const candidate = mainMenuData || menusList[0];
2186
+ if (candidate && candidate.id) {
2187
+ try {
2188
+ const fullMenu = await webstoreApi.fetchMenuById(candidate.id, opts);
2189
+ if (fullMenu && fullMenu.items && Array.isArray(fullMenu.items)) {
2190
+ fullMenu.items.sort((a, b) => (a?.displayOrder || 0) - (b?.displayOrder || 0));
2148
2191
  }
2192
+ context.mainMenu = fullMenu || candidate;
2193
+ } catch (e) {
2194
+ context.mainMenu = candidate;
2149
2195
  }
2150
2196
  }
2151
- context.mainMenu = mainMenu;
2152
2197
  } catch (e) {
2153
- context.menus = [];
2198
+ context.menus = {};
2154
2199
  context.mainMenu = null;
2155
2200
  }
2156
2201
 
@@ -2335,199 +2380,81 @@ class DevServer {
2335
2380
  * @returns {Object} Enriched product (creates a copy to avoid mutation)
2336
2381
  */
2337
2382
  enrichProductData(product) {
2338
- // Create a copy to avoid mutating the original
2339
2383
  const enriched = { ...product };
2340
-
2341
- // CRITICAL: Ensure productId exists - it's the most reliable identifier
2384
+
2342
2385
  if (!enriched.productId && enriched.id) {
2343
- // Try to extract numeric ID from string ID (e.g., "product-1" -> 1)
2344
2386
  const numericMatch = String(enriched.id).match(/\d+/);
2345
- if (numericMatch) {
2346
- enriched.productId = parseInt(numericMatch[0], 10);
2347
- } else {
2348
- // Use the ID as-is if it's already numeric
2349
- enriched.productId = enriched.id;
2350
- }
2351
- } else if (!enriched.productId && !enriched.id) {
2352
- console.warn(`[ENRICH] ⚠️ Product missing both id and productId:`, {
2353
- keys: Object.keys(enriched).slice(0, 15),
2354
- title: enriched.title || enriched.name
2355
- });
2387
+ enriched.productId = numericMatch ? parseInt(numericMatch[0], 10) : enriched.id;
2356
2388
  }
2357
-
2358
- // DEBUG: Log incoming product to diagnose missing fields
2359
- if (!product.id && !product.handle && !product.slug && !product.productId) {
2360
- console.warn(`[ENRICH] ⚠️ Product missing all identifiers (id/handle/slug/productId):`, {
2361
- keys: Object.keys(product).slice(0, 15),
2362
- title: product.title || product.name
2363
- });
2364
- }
2365
-
2366
- // Ensure URL field exists - CRITICAL for product navigation
2367
- // Always use /products/{handle} so links target dev server (never external URLs)
2368
- const handle = enriched.handle || enriched.slug || enriched.id;
2389
+
2390
+ const handle = enriched.handle || enriched.slug || String(enriched.id || enriched.productId || '');
2369
2391
  const effectiveHandle = (handle && String(handle).trim() && String(handle) !== '#')
2370
- ? String(handle).trim().toLowerCase()
2371
- : (enriched.productId ? `product-${enriched.productId}`.toLowerCase() : null);
2392
+ ? String(handle).trim().toLowerCase() : null;
2393
+
2372
2394
  if (effectiveHandle) {
2373
- enriched.url = `/products/${effectiveHandle}`;
2374
- enriched.link = `/products/${effectiveHandle}`;
2375
- if (!enriched.handle || enriched.handle === '#' || enriched.handle === '') enriched.handle = effectiveHandle;
2376
- if (!enriched.slug || enriched.slug === '#' || enriched.slug === '') enriched.slug = effectiveHandle;
2377
- } else {
2378
- enriched.url = '#';
2379
- enriched.link = '#';
2380
- console.warn(`[ENRICH] ⚠️ Product has no id/handle/slug/productId, URL set to '#'`, {
2381
- productKeys: Object.keys(enriched).slice(0, 10)
2382
- });
2395
+ enriched.url = effectiveHandle;
2396
+ enriched.link = effectiveHandle;
2397
+ if (!enriched.handle) enriched.handle = effectiveHandle;
2398
+ if (!enriched.slug) enriched.slug = effectiveHandle;
2383
2399
  }
2384
2400
 
2385
- // Ensure handle/slug exist if missing (for template fallback logic)
2386
- // Also normalize them (remove '#' or empty strings)
2387
- if (!enriched.handle || enriched.handle === '#' || enriched.handle === '') {
2388
- if (enriched.slug && enriched.slug !== '#' && enriched.slug !== '') {
2389
- enriched.handle = enriched.slug;
2390
- } else if (enriched.id && enriched.id !== '#') {
2391
- enriched.handle = String(enriched.id).toLowerCase();
2392
- } else if (enriched.productId) {
2393
- enriched.handle = `product-${enriched.productId}`.toLowerCase();
2394
- } else if (!enriched.handle) {
2395
- // Generate from URL if it was set above
2396
- if (enriched.url && enriched.url !== '#') {
2397
- enriched.handle = enriched.url.replace(/^\/products\//, '').toLowerCase();
2398
- }
2399
- }
2400
- }
2401
-
2402
- if (!enriched.slug || enriched.slug === '#' || enriched.slug === '') {
2403
- if (enriched.handle && enriched.handle !== '#' && enriched.handle !== '') {
2404
- enriched.slug = enriched.handle;
2405
- } else if (enriched.id && enriched.id !== '#') {
2406
- enriched.slug = String(enriched.id).toLowerCase();
2407
- } else if (enriched.productId) {
2408
- enriched.slug = `product-${enriched.productId}`.toLowerCase();
2409
- } else if (!enriched.slug) {
2410
- // Generate from URL if it was set above
2411
- if (enriched.url && enriched.url !== '#') {
2412
- enriched.slug = enriched.url.replace(/^\/products\//, '').toLowerCase();
2413
- }
2414
- }
2415
- }
2416
-
2417
- // Ensure name/title exists - CRITICAL for product display
2418
2401
  if (!enriched.name && !enriched.title) {
2419
2402
  enriched.name = `Product ${enriched.id || 'Unknown'}`;
2420
2403
  enriched.title = enriched.name;
2421
2404
  } else if (!enriched.name) {
2422
- enriched.name = enriched.title || `Product ${enriched.id || 'Unknown'}`;
2405
+ enriched.name = enriched.title;
2423
2406
  } else if (!enriched.title) {
2424
2407
  enriched.title = enriched.name;
2425
2408
  }
2426
-
2427
- // Ensure both variations and variants exist (product-card uses variations)
2428
- if (enriched.variants && !enriched.variations) {
2429
- enriched.variations = enriched.variants;
2430
- } else if (enriched.variations && !enriched.variants) {
2431
- enriched.variants = enriched.variations;
2432
- }
2433
-
2434
- // Ensure price exists - CRITICAL for product display
2435
- if (!enriched.price && !enriched.sellingPrice) {
2436
- // Try to get from variants/variations first
2437
- const variantsOrVariations = enriched.variants || enriched.variations;
2438
- if (variantsOrVariations && variantsOrVariations.length > 0) {
2439
- enriched.price = variantsOrVariations[0].price || variantsOrVariations[0].sellingPrice || 0;
2440
- enriched.sellingPrice = enriched.price;
2441
- } else {
2442
- enriched.price = 0;
2443
- enriched.sellingPrice = 0;
2444
- }
2445
- } else if (!enriched.price) {
2446
- enriched.price = enriched.sellingPrice;
2447
- } else if (!enriched.sellingPrice) {
2448
- enriched.sellingPrice = enriched.price;
2409
+
2410
+ if (enriched.variants && !enriched.variations) enriched.variations = enriched.variants;
2411
+ else if (enriched.variations && !enriched.variants) enriched.variants = enriched.variations;
2412
+
2413
+ if (enriched.price == null && enriched.sellingPrice == null) {
2414
+ const v = enriched.variants || enriched.variations;
2415
+ enriched.price = (v && v.length > 0) ? (v[0].price || 0) : 0;
2449
2416
  }
2450
-
2451
- // Ensure prices object exists for product-card compatibility - CRITICAL
2452
- // Make sure price is NOT 0 unless it's actually 0 (check if undefined/null)
2453
- const finalPrice = (enriched.price !== undefined && enriched.price !== null)
2454
- ? enriched.price
2455
- : ((enriched.sellingPrice !== undefined && enriched.sellingPrice !== null)
2456
- ? enriched.sellingPrice
2457
- : 0);
2458
-
2417
+ if (enriched.price == null) enriched.price = enriched.sellingPrice || 0;
2418
+
2459
2419
  if (!enriched.prices) {
2460
- const mrpValue = enriched.compareAtPrice || enriched.comparePrice || enriched.mrp || 0;
2461
2420
  enriched.prices = {
2462
- price: finalPrice,
2463
- mrp: mrpValue,
2464
- currency: enriched.currency || 'USD'
2421
+ price: enriched.price,
2422
+ mrp: enriched.compareAtPrice || enriched.comparePrice || 0
2465
2423
  };
2466
- } else {
2467
- // Update prices object if it exists but is missing fields
2468
- if (enriched.prices.price === undefined || enriched.prices.price === null) {
2469
- enriched.prices.price = finalPrice;
2470
- }
2471
- if (enriched.prices.mrp === undefined || enriched.prices.mrp === null) {
2472
- enriched.prices.mrp = enriched.compareAtPrice || enriched.comparePrice || enriched.mrp || 0;
2473
- }
2474
- if (!enriched.prices.currency) {
2475
- enriched.prices.currency = enriched.currency || 'USD';
2476
- }
2477
- }
2478
-
2479
- // Ensure price/sellingPrice fields also exist (not just prices object)
2480
- if (enriched.price === undefined || enriched.price === null) {
2481
- enriched.price = finalPrice;
2482
2424
  }
2483
- if (enriched.sellingPrice === undefined || enriched.sellingPrice === null) {
2484
- enriched.sellingPrice = finalPrice;
2485
- }
2486
-
2487
- // Ensure image exists - CRITICAL for product display
2488
- if (!enriched.imageUrl && !enriched.thumbnailImage1 && (!enriched.images || enriched.images.length === 0)) {
2489
- // Fallback to picsum placeholder
2425
+
2426
+ if (!enriched.imageUrl && !enriched.thumbnailImage && (!enriched.images || enriched.images.length === 0)) {
2490
2427
  const imageId = enriched.id ? String(enriched.id).replace(/\D/g, '') || '1' : '1';
2491
2428
  enriched.imageUrl = `https://picsum.photos/seed/${imageId}/800/800`;
2492
- enriched.thumbnailImage1 = {
2493
- url: enriched.imageUrl,
2494
- altText: enriched.title || enriched.name || 'Product image'
2495
- };
2429
+ enriched.thumbnailImage = enriched.imageUrl;
2496
2430
  }
2497
-
2498
- // Ensure stock/availability data exists - CRITICAL to prevent incorrect "SOLD OUT" display
2499
- if (enriched.stock === undefined && enriched.stockQuantity === undefined) {
2500
- // Calculate from variants/variations if available
2501
- const variantsOrVariations = enriched.variants || enriched.variations;
2502
- if (variantsOrVariations && variantsOrVariations.length > 0) {
2503
- const totalStock = variantsOrVariations.reduce((sum, v) => sum + (v.stock || v.quantity || 0), 0);
2504
- enriched.stock = totalStock;
2505
- enriched.stockQuantity = totalStock;
2506
- enriched.inStock = totalStock > 0;
2507
- enriched.available = totalStock > 0;
2508
- } else {
2509
- // Default to in stock if no stock info (prevents false "SOLD OUT" display)
2510
- enriched.stock = 10;
2511
- enriched.stockQuantity = 10;
2512
- enriched.inStock = true;
2513
- enriched.available = true;
2514
- }
2515
- } else {
2516
- // Ensure inStock/available flags are set based on stock value
2517
- // CRITICAL: Use nullish coalescing to handle 0 values correctly
2518
- const stockValue = enriched.stock !== undefined ? enriched.stock : (enriched.stockQuantity !== undefined ? enriched.stockQuantity : 0);
2519
- enriched.inStock = stockValue > 0;
2520
- enriched.available = stockValue > 0;
2521
-
2522
- // Also ensure stockQuantity is set if only stock is set (and vice versa)
2523
- if (enriched.stock !== undefined && enriched.stockQuantity === undefined) {
2524
- enriched.stockQuantity = enriched.stock;
2525
- } else if (enriched.stockQuantity !== undefined && enriched.stock === undefined) {
2526
- enriched.stock = enriched.stockQuantity;
2431
+
2432
+ this.enrichProductStockStatus(enriched);
2433
+
2434
+ return enriched;
2435
+ }
2436
+
2437
+ /**
2438
+ * Compute inStock and available flags matching production enrichProductStockStatus().
2439
+ */
2440
+ enrichProductStockStatus(product) {
2441
+ const stockQuantity = product.stockQuantity || 0;
2442
+ const stockTrackingIsEnabled = product.stockTrackingIsEnabled || false;
2443
+ const isAllowToOrder = product.isAllowToOrder || false;
2444
+
2445
+ product.inStock = !stockTrackingIsEnabled || stockQuantity > 0;
2446
+ product.available = !stockTrackingIsEnabled || stockQuantity > 0 || isAllowToOrder;
2447
+
2448
+ if (product.stockQuantity == null) {
2449
+ const variants = product.variants || product.variations;
2450
+ if (variants && variants.length > 0) {
2451
+ product.stockQuantity = variants.reduce((sum, v) => sum + (v.stockQuantity || 0), 0);
2452
+ product.inStock = !stockTrackingIsEnabled || product.stockQuantity > 0;
2453
+ product.available = !stockTrackingIsEnabled || product.stockQuantity > 0 || isAllowToOrder;
2527
2454
  }
2528
2455
  }
2529
-
2530
- return enriched;
2456
+
2457
+ return product;
2531
2458
  }
2532
2459
 
2533
2460
  /**
@@ -2546,35 +2473,39 @@ class DevServer {
2546
2473
  if (n.Name != null && n.name == null) n.name = n.Name;
2547
2474
  if (n.Title != null && n.title == null) n.title = n.Title;
2548
2475
  if (n.Attributes != null && n.attributes == null) n.attributes = n.Attributes;
2549
- const t1 = n.ThumbnailImage1 || n.thumbnailImage1;
2476
+
2477
+ const t1 = n.ThumbnailImage1 || n.thumbnailImage || n.thumbnailImage;
2550
2478
  if (t1 != null) {
2551
2479
  const u = typeof t1 === 'string' ? t1 : (t1.url || t1.Url || t1.imageUrl || t1.ImageUrl);
2552
2480
  if (u) {
2553
- const a = typeof t1 === 'string' ? (n.name || n.title) : (t1.altText || t1.AltText || n.name || n.title);
2554
- n.thumbnailImage1 = { url: u, altText: a };
2555
- if (!n.thumbnailImage) n.thumbnailImage = n.thumbnailImage1;
2556
- } else if (!n.thumbnailImage1) n.thumbnailImage1 = t1;
2481
+ n.thumbnailImage = u;
2482
+ if (!n.imageUrl) n.imageUrl = u;
2483
+ }
2557
2484
  }
2485
+
2558
2486
  let imgs = n.images || n.Images;
2559
2487
  if (Array.isArray(imgs) && imgs.length > 0) {
2560
2488
  n.images = imgs.map((img) => {
2561
2489
  if (typeof img === 'string') return { url: img, altText: n.name || n.title };
2562
2490
  const u = img.url || img.Url || img.imageUrl || img.ImageUrl;
2563
2491
  const a = img.altText || img.AltText || n.name || n.title;
2564
- return u ? { url: u, altText: a } : (typeof img === 'object' ? { url: img, altText: a } : { url: String(img), altText: a });
2492
+ return { url: u || '', altText: a };
2565
2493
  });
2566
- } else if (imgs) {
2567
- n.images = Array.isArray(imgs) ? imgs : [imgs];
2568
2494
  } else {
2569
2495
  const single = n.imageUrl || n.ImageUrl;
2570
2496
  if (single) n.images = [{ url: single, altText: n.name || n.title }];
2571
2497
  }
2498
+
2572
2499
  const handle = (n.handle || n.slug || n.id || '').toString().trim();
2573
- const h = handle && handle !== '#' ? handle.toLowerCase() : (n.productId ? `product-${n.productId}` : null);
2500
+ const h = handle && handle !== '#' ? handle.toLowerCase() : (n.productId ? String(n.productId) : null);
2574
2501
  if (h) {
2575
- n.url = `/products/${h}`;
2576
- n.link = `/products/${h}`;
2502
+ n.url = h;
2503
+ n.link = h;
2504
+ if (!n.handle) n.handle = h;
2505
+ if (!n.slug) n.slug = h;
2577
2506
  }
2507
+
2508
+ this.enrichProductStockStatus(n);
2578
2509
  return n;
2579
2510
  }
2580
2511
 
@@ -2680,7 +2611,7 @@ class DevServer {
2680
2611
  const products = context.collection?.products || [];
2681
2612
  const productCards = products.map(p => `
2682
2613
  <div style="background: #fff; border-radius: 8px; padding: 1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
2683
- <a href="/products/${p.handle || p.id}">
2614
+ <a href="/${p.slug || p.handle || p.id}">
2684
2615
  <img src="${p.images?.[0] || p.imageUrl || 'https://picsum.photos/seed/' + p.id + '/300/300'}"
2685
2616
  alt="${p.title || p.name}" style="width: 100%; height: 200px; object-fit: cover; border-radius: 4px;">
2686
2617
  <h4 style="margin: 0.5rem 0;">${p.title || p.name}</h4>