@o2vend/theme-cli 1.0.33 → 1.0.35
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 +916 -347
- package/lib/lib/liquid-engine.js +83 -11
- package/lib/lib/webstoreapi-fetcher.js +322 -0
- package/package.json +16 -16
- package/test-theme/layout/theme.liquid +195 -195
package/lib/lib/dev-server.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Development Server
|
|
3
3
|
* Express server for local theme development with hot reload
|
|
4
|
+
*
|
|
5
|
+
* Developer guideline: Do NOT add theme-specific CSS class hardcodes (e.g. targeting
|
|
6
|
+
* .mobile-bottom-nav, .site-header, etc.) in this file. Such fixes would force theme
|
|
7
|
+
* developers to use those exact class names. Theme behavior and styling must remain
|
|
8
|
+
* entirely under the theme; the CLI only serves assets, proxies, and generic tooling.
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
const express = require('express');
|
|
@@ -11,6 +16,7 @@ const WidgetService = require('./widget-service');
|
|
|
11
16
|
const O2VendApiClient = require('./api-client');
|
|
12
17
|
const { setupHotReload } = require('./hot-reload');
|
|
13
18
|
const chalk = require('chalk');
|
|
19
|
+
const webstoreApi = require('./webstoreapi-fetcher');
|
|
14
20
|
|
|
15
21
|
class DevServer {
|
|
16
22
|
constructor(options = {}) {
|
|
@@ -130,29 +136,33 @@ class DevServer {
|
|
|
130
136
|
// Only process if it contains actual Liquid variables, not just comments
|
|
131
137
|
if (cssContent.includes('{{') && cssContent.includes('settings.')) {
|
|
132
138
|
try {
|
|
133
|
-
// Load settings from theme config (already extracts 'current' section)
|
|
134
139
|
const settings = this.loadThemeSettings();
|
|
135
|
-
|
|
136
|
-
// Process through Liquid
|
|
137
140
|
const processedCss = await this.liquid.parseAndRender(cssContent, { settings });
|
|
138
|
-
|
|
139
141
|
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
140
142
|
res.setHeader('Cache-Control', 'no-cache');
|
|
141
143
|
return res.send(processedCss);
|
|
142
144
|
} catch (liquidError) {
|
|
143
145
|
console.error(`[CSS] Liquid processing error for ${relativePath}:`, liquidError.message);
|
|
144
|
-
// Fall through to serve static file
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
|
|
148
|
-
// Serve static file if no Liquid processing needed or if processing failed
|
|
149
149
|
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
150
|
-
res.sendFile(
|
|
150
|
+
res.sendFile(relativePath, { root: path.join(this.themePath, 'assets') });
|
|
151
151
|
} catch (error) {
|
|
152
152
|
console.error(`[CSS] Error processing ${req.path}:`, error.message);
|
|
153
153
|
next();
|
|
154
154
|
}
|
|
155
155
|
});
|
|
156
|
+
|
|
157
|
+
// Explicit logo route so /assets/logo.png always serves correctly (avoids broken image)
|
|
158
|
+
this.app.get('/assets/logo.png', (req, res) => {
|
|
159
|
+
const logoPath = path.join(this.themePath, 'assets', 'logo.png');
|
|
160
|
+
if (!fs.existsSync(logoPath)) {
|
|
161
|
+
return res.status(404).setHeader('Content-Type', 'text/plain').end('Logo not found');
|
|
162
|
+
}
|
|
163
|
+
res.setHeader('Content-Type', 'image/png');
|
|
164
|
+
res.sendFile('logo.png', { root: path.join(this.themePath, 'assets') });
|
|
165
|
+
});
|
|
156
166
|
|
|
157
167
|
// Static files (theme assets) with proper MIME types
|
|
158
168
|
this.app.use('/assets', express.static(path.join(this.themePath, 'assets'), {
|
|
@@ -225,37 +235,36 @@ class DevServer {
|
|
|
225
235
|
|
|
226
236
|
/**
|
|
227
237
|
* Setup API client and widget service
|
|
238
|
+
* Real mode: no apiClient (use webstoreapi-fetcher only). WidgetService with null for getTemplateSlug.
|
|
239
|
+
* Mock mode: apiClient created in start() after mock API; WidgetService uses it.
|
|
228
240
|
*/
|
|
229
241
|
async setupServices() {
|
|
230
242
|
if (this.mode === 'real') {
|
|
231
|
-
// Real API mode - create API client from environment
|
|
232
|
-
const tenantId = process.env.O2VEND_TENANT_ID;
|
|
233
|
-
const apiKey = process.env.O2VEND_API_KEY;
|
|
234
243
|
const baseUrl = process.env.O2VEND_API_BASE_URL;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
console.warn(chalk.yellow('⚠️ Real API mode requires O2VEND_TENANT_ID, O2VEND_API_KEY, and O2VEND_API_BASE_URL'));
|
|
244
|
+
if (!baseUrl) {
|
|
245
|
+
console.warn(chalk.yellow('⚠️ Real API mode requires O2VEND_API_BASE_URL (storefront URL, e.g. https://store.myo2vend.com)'));
|
|
238
246
|
console.warn(chalk.yellow(' Falling back to mock mode'));
|
|
239
247
|
this.mode = 'mock';
|
|
240
248
|
} else {
|
|
241
|
-
this.apiClient =
|
|
249
|
+
this.apiClient = null;
|
|
250
|
+
this.widgetService = new WidgetService(null, {
|
|
251
|
+
theme: path.basename(this.themePath),
|
|
252
|
+
themePath: this.themePath
|
|
253
|
+
});
|
|
242
254
|
}
|
|
243
255
|
}
|
|
244
|
-
|
|
245
|
-
// In mock mode, create API client - will be updated after mock API starts
|
|
246
|
-
// We create a placeholder here, actual client will be created in start() after mock API is running
|
|
256
|
+
|
|
247
257
|
if (this.mode === 'mock') {
|
|
248
|
-
// Create a temporary client - will be replaced in start() method
|
|
249
258
|
this.apiClient = null;
|
|
250
259
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
260
|
+
|
|
261
|
+
if (this.mode === 'mock' || !this.widgetService) {
|
|
262
|
+
if (this.apiClient) {
|
|
263
|
+
this.widgetService = new WidgetService(this.apiClient, {
|
|
264
|
+
theme: path.basename(this.themePath),
|
|
265
|
+
themePath: this.themePath
|
|
266
|
+
});
|
|
267
|
+
}
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
// Create Liquid engine
|
|
@@ -420,9 +429,15 @@ class DevServer {
|
|
|
420
429
|
if (!context.products || context.products.length === 0) {
|
|
421
430
|
console.warn('[HOME] No products in context, attempting reload...');
|
|
422
431
|
try {
|
|
423
|
-
if (this.
|
|
432
|
+
if (this.mode === 'real') {
|
|
433
|
+
const res = await webstoreApi.fetchProducts({ limit: 50, offset: 0 }, { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} });
|
|
434
|
+
context.products = res?.products || res?.Products || [];
|
|
435
|
+
context.products = this.enrichProductsData(context.products);
|
|
436
|
+
console.log(`[HOME] Reloaded ${context.products.length} products (real)`);
|
|
437
|
+
} else if (this.apiClient) {
|
|
424
438
|
const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0 });
|
|
425
439
|
context.products = productsResponse.products || productsResponse.data?.products || [];
|
|
440
|
+
context.products = this.enrichProductsData(context.products);
|
|
426
441
|
console.log(`[HOME] Reloaded ${context.products.length} products`);
|
|
427
442
|
} else {
|
|
428
443
|
console.error('[HOME] API client not available');
|
|
@@ -431,12 +446,15 @@ class DevServer {
|
|
|
431
446
|
console.error('[HOME] Failed to reload products:', error.message);
|
|
432
447
|
}
|
|
433
448
|
}
|
|
434
|
-
|
|
435
|
-
// Ensure widgets are available
|
|
449
|
+
|
|
436
450
|
if (!context.widgets || Object.keys(context.widgets).length === 0) {
|
|
437
451
|
console.warn('[HOME] No widgets in context, attempting reload...');
|
|
438
452
|
try {
|
|
439
|
-
if (this.
|
|
453
|
+
if (this.mode === 'real') {
|
|
454
|
+
const opts = { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} };
|
|
455
|
+
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
|
+
} else if (this.widgetService) {
|
|
440
458
|
const widgets = await this.widgetService.getWidgetsBySections();
|
|
441
459
|
context.widgets = widgets || { hero: [], content: [], footer: [] };
|
|
442
460
|
console.log(`[HOME] Reloaded widgets: ${Object.keys(context.widgets).join(', ')}`);
|
|
@@ -449,7 +467,7 @@ class DevServer {
|
|
|
449
467
|
context.widgets = { hero: [], content: [], footer: [] };
|
|
450
468
|
}
|
|
451
469
|
}
|
|
452
|
-
|
|
470
|
+
|
|
453
471
|
const html = await renderWithLayout(this.liquid, 'templates/index', context, this.themePath);
|
|
454
472
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
455
473
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -462,8 +480,8 @@ class DevServer {
|
|
|
462
480
|
// Products listing page
|
|
463
481
|
this.app.get('/products', async (req, res, next) => {
|
|
464
482
|
try {
|
|
465
|
-
// Check for category filter in query parameters
|
|
466
|
-
const categoryFilter = req.query.category;
|
|
483
|
+
// Check for category filter in query parameters (handle both 'category' and 'Category')
|
|
484
|
+
const categoryFilter = req.query.category || req.query.Category;
|
|
467
485
|
|
|
468
486
|
let context = await this.buildContext(req, 'products');
|
|
469
487
|
|
|
@@ -471,44 +489,61 @@ class DevServer {
|
|
|
471
489
|
if (categoryFilter) {
|
|
472
490
|
console.log(`[PRODUCTS PAGE] Filtering by category: ${categoryFilter}`);
|
|
473
491
|
|
|
474
|
-
|
|
475
|
-
const category = context.categories?.find(c =>
|
|
476
|
-
(c.
|
|
477
|
-
(c.
|
|
478
|
-
|
|
492
|
+
const cf = (categoryFilter || '').toLowerCase();
|
|
493
|
+
const category = context.categories?.find(c => {
|
|
494
|
+
const ch = (c.handle || '').toLowerCase();
|
|
495
|
+
const cs = (c.slug || '').toLowerCase();
|
|
496
|
+
const cn = (c.name || '').toLowerCase();
|
|
497
|
+
const cnNorm = (c.name || '').toLowerCase().replace(/\s+/g, '-');
|
|
498
|
+
return ch === cf || cs === cf || cn === cf || cnNorm === cf;
|
|
499
|
+
});
|
|
479
500
|
|
|
480
501
|
if (category) {
|
|
481
502
|
console.log(`[PRODUCTS PAGE] Found category: ${category.name} (ID: ${category.id})`);
|
|
482
503
|
|
|
483
|
-
|
|
484
|
-
const filteredProducts =
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
);
|
|
504
|
+
const filtered = context.products.filter(p => this.productMatchesCategory(p, category));
|
|
505
|
+
const filteredProducts = this.mode === 'real'
|
|
506
|
+
? filtered
|
|
507
|
+
: filtered.map(p => this.enrichProductData(p));
|
|
488
508
|
|
|
509
|
+
// CRITICAL: Set both context.products and context.collection.products to the SAME enriched array
|
|
510
|
+
// This ensures they always reference the same objects (like widgets do)
|
|
489
511
|
context.products = filteredProducts;
|
|
490
|
-
console.log(`[PRODUCTS PAGE] Filtered products: ${filteredProducts.length} products in category`);
|
|
491
|
-
|
|
492
|
-
// Update collection context
|
|
493
512
|
context.collection = context.collection || {};
|
|
513
|
+
context.collection.products = context.products; // Same reference as context.products
|
|
494
514
|
context.collection.title = category.name || 'Products';
|
|
495
515
|
context.collection.handle = category.handle || categoryFilter;
|
|
496
516
|
context.collection.description = category.description;
|
|
497
517
|
context.collection.totalProducts = filteredProducts.length;
|
|
518
|
+
|
|
519
|
+
console.log(`[PRODUCTS PAGE] Filtered products: ${filteredProducts.length} products in category`);
|
|
520
|
+
console.log(`[PRODUCTS PAGE] ✅ collection.products === context.products: ${context.collection.products === context.products}`);
|
|
521
|
+
|
|
522
|
+
console.log(`[PRODUCTS PAGE] ✅ Category filtered: ${filteredProducts.length} enriched products in collection`);
|
|
498
523
|
} else {
|
|
499
524
|
console.warn(`[PRODUCTS PAGE] Category not found: ${categoryFilter}, showing all products`);
|
|
500
525
|
}
|
|
501
526
|
}
|
|
502
527
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
product.url = `/products/${handle}`;
|
|
508
|
-
product.link = `/products/${handle}`;
|
|
528
|
+
if (context.products && context.products.length > 0 && this.mode !== 'real') {
|
|
529
|
+
if (!context.products[0].url && !context.products[0].link) {
|
|
530
|
+
console.log(`[PRODUCTS PAGE] Products need enrichment - enriching now...`);
|
|
531
|
+
context.products = this.enrichProductsData(context.products);
|
|
509
532
|
}
|
|
510
|
-
|
|
511
|
-
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Debug: Log sample product data to verify enrichment
|
|
536
|
+
if (context.products.length > 0) {
|
|
537
|
+
const sample = context.products[0];
|
|
538
|
+
console.log(`[PRODUCTS PAGE] Sample product data:`, {
|
|
539
|
+
title: sample.title || sample.name,
|
|
540
|
+
url: sample.url || sample.link,
|
|
541
|
+
price: sample.price || sample.sellingPrice,
|
|
542
|
+
stock: sample.stock,
|
|
543
|
+
inStock: sample.inStock,
|
|
544
|
+
hasImage: !!(sample.imageUrl || sample.thumbnailImage1)
|
|
545
|
+
});
|
|
546
|
+
}
|
|
512
547
|
|
|
513
548
|
// DEBUG: Log context data for /products page
|
|
514
549
|
console.log(`[PRODUCTS PAGE] Context summary:`);
|
|
@@ -523,31 +558,37 @@ class DevServer {
|
|
|
523
558
|
if (!context.products || context.products.length === 0) {
|
|
524
559
|
console.warn('[PRODUCTS PAGE] ⚠️ No products in context, attempting to reload...');
|
|
525
560
|
try {
|
|
526
|
-
const queryParams = {};
|
|
561
|
+
const queryParams = { limit: 50, offset: 0 };
|
|
527
562
|
if (categoryFilter) {
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
(c.
|
|
531
|
-
(c.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
563
|
+
const cf = (categoryFilter || '').toLowerCase();
|
|
564
|
+
const cat = context.categories?.find(c => {
|
|
565
|
+
const ch = (c.handle || '').toLowerCase();
|
|
566
|
+
const cs = (c.slug || '').toLowerCase();
|
|
567
|
+
const cn = (c.name || '').toLowerCase();
|
|
568
|
+
const cnNorm = (c.name || '').toLowerCase().replace(/\s+/g, '-');
|
|
569
|
+
return ch === cf || cs === cf || cn === cf || cnNorm === cf;
|
|
570
|
+
});
|
|
571
|
+
if (cat) queryParams.categoryId = cat.id;
|
|
572
|
+
}
|
|
573
|
+
if (this.mode === 'real') {
|
|
574
|
+
const res = await webstoreApi.fetchProducts(queryParams, { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} });
|
|
575
|
+
context.products = this.normalizeProductsForRealMode(res?.products || res?.Products || []);
|
|
576
|
+
} else if (this.apiClient) {
|
|
577
|
+
const productsResponse = await this.apiClient.getProducts(queryParams);
|
|
578
|
+
context.products = productsResponse.products || productsResponse.data?.products || [];
|
|
579
|
+
context.products = this.enrichProductsData(context.products);
|
|
536
580
|
}
|
|
537
|
-
const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0, ...queryParams });
|
|
538
|
-
context.products = productsResponse.products || productsResponse.data?.products || [];
|
|
539
|
-
|
|
540
|
-
// Ensure URLs are set
|
|
541
|
-
context.products = context.products.map(product => {
|
|
542
|
-
if (!product.url && !product.link) {
|
|
543
|
-
const handle = product.handle || product.slug || product.id;
|
|
544
|
-
product.url = `/products/${handle}`;
|
|
545
|
-
product.link = `/products/${handle}`;
|
|
546
|
-
}
|
|
547
|
-
return product;
|
|
548
|
-
});
|
|
549
|
-
|
|
550
581
|
console.log(`[PRODUCTS PAGE] ✅ Reloaded ${context.products.length} products`);
|
|
582
|
+
if (context.products.length > 0) {
|
|
583
|
+
const sample = context.products[0];
|
|
584
|
+
console.log(`[PRODUCTS PAGE] Sample reloaded product:`, {
|
|
585
|
+
title: sample.title || sample.name,
|
|
586
|
+
url: sample.url || sample.link,
|
|
587
|
+
price: sample.price || sample.sellingPrice,
|
|
588
|
+
stock: sample.stock,
|
|
589
|
+
inStock: sample.inStock
|
|
590
|
+
});
|
|
591
|
+
}
|
|
551
592
|
} catch (error) {
|
|
552
593
|
console.error('[PRODUCTS PAGE] ❌ Failed to load products:', error.message);
|
|
553
594
|
}
|
|
@@ -575,12 +616,15 @@ class DevServer {
|
|
|
575
616
|
if (!context.widgets || Object.keys(context.widgets).length === 0) {
|
|
576
617
|
console.warn('[PRODUCTS PAGE] ⚠️ No widgets in context, attempting to reload...');
|
|
577
618
|
try {
|
|
578
|
-
if (this.
|
|
619
|
+
if (this.mode === 'real') {
|
|
620
|
+
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)`);
|
|
623
|
+
} else if (this.widgetService) {
|
|
579
624
|
const widgets = await this.widgetService.getWidgetsBySections();
|
|
580
625
|
context.widgets = widgets || { hero: [], content: [], footer: [], header: [] };
|
|
581
626
|
console.log(`[PRODUCTS PAGE] ✅ Reloaded widgets: ${Object.keys(context.widgets).join(', ')}`);
|
|
582
627
|
} else {
|
|
583
|
-
console.error('[PRODUCTS PAGE] ❌ Widget service not available');
|
|
584
628
|
context.widgets = { hero: [], content: [], footer: [], header: [] };
|
|
585
629
|
}
|
|
586
630
|
} catch (error) {
|
|
@@ -588,16 +632,22 @@ class DevServer {
|
|
|
588
632
|
context.widgets = { hero: [], content: [], footer: [], header: [] };
|
|
589
633
|
}
|
|
590
634
|
}
|
|
591
|
-
|
|
635
|
+
|
|
592
636
|
// Ensure menus are available
|
|
593
637
|
if (!context.menus || context.menus.length === 0) {
|
|
594
638
|
console.warn('[PRODUCTS PAGE] ⚠️ No menus in context, attempting to reload...');
|
|
595
639
|
try {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
+
}
|
|
601
651
|
} catch (error) {
|
|
602
652
|
console.error('[PRODUCTS PAGE] ❌ Failed to reload menus:', error.message);
|
|
603
653
|
context.menus = [];
|
|
@@ -605,9 +655,14 @@ class DevServer {
|
|
|
605
655
|
}
|
|
606
656
|
|
|
607
657
|
// Ensure collection.products is set for products template
|
|
658
|
+
// CRITICAL: Always sync collection.products with context.products AFTER enrichment
|
|
659
|
+
// This ensures collection.products always has the same enriched objects as context.products
|
|
608
660
|
if (context.products && context.products.length > 0) {
|
|
661
|
+
// Always reassign collection.products to match context.products (they should be the same)
|
|
609
662
|
context.collection = context.collection || {};
|
|
610
|
-
context.collection.products = context.products;
|
|
663
|
+
context.collection.products = context.products; // Use the same enriched array
|
|
664
|
+
|
|
665
|
+
// Update collection metadata if not set
|
|
611
666
|
if (!context.collection.title) {
|
|
612
667
|
context.collection.title = categoryFilter ? `${categoryFilter} Products` : 'All Products';
|
|
613
668
|
}
|
|
@@ -617,6 +672,56 @@ class DevServer {
|
|
|
617
672
|
if (!context.collection.totalProducts) {
|
|
618
673
|
context.collection.totalProducts = context.products.length;
|
|
619
674
|
}
|
|
675
|
+
|
|
676
|
+
// Debug: Verify collection.products has same reference as context.products
|
|
677
|
+
const sameReference = context.collection.products === context.products;
|
|
678
|
+
console.log(`[PRODUCTS PAGE] collection.products sync: same reference=${sameReference}, length=${context.collection.products?.length || 0}`);
|
|
679
|
+
|
|
680
|
+
// Debug: Log sample product from collection to verify enrichment
|
|
681
|
+
if (context.collection.products && context.collection.products.length > 0) {
|
|
682
|
+
const sample = context.collection.products[0];
|
|
683
|
+
console.log(`[PRODUCTS PAGE] ✅ Sample collection product before render:`, {
|
|
684
|
+
id: sample.id,
|
|
685
|
+
title: sample.title || sample.name,
|
|
686
|
+
url: sample.url || sample.link || 'MISSING',
|
|
687
|
+
price: sample.price || sample.sellingPrice || 'MISSING',
|
|
688
|
+
prices: sample.prices ? { price: sample.prices.price, mrp: sample.prices.mrp } : 'MISSING',
|
|
689
|
+
stock: sample.stock !== undefined ? sample.stock : 'MISSING',
|
|
690
|
+
stockQuantity: sample.stockQuantity !== undefined ? sample.stockQuantity : 'MISSING',
|
|
691
|
+
inStock: sample.inStock !== undefined ? sample.inStock : 'MISSING',
|
|
692
|
+
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,
|
|
696
|
+
hasImagesArray: !!(sample.images && sample.images.length > 0)
|
|
697
|
+
});
|
|
698
|
+
} else {
|
|
699
|
+
console.warn(`[PRODUCTS PAGE] ⚠️ collection.products is empty or undefined!`);
|
|
700
|
+
console.warn(`[PRODUCTS PAGE] context.products.length: ${context.products?.length || 0}`);
|
|
701
|
+
console.warn(`[PRODUCTS PAGE] context.collection:`, context.collection ? Object.keys(context.collection) : 'undefined');
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
console.warn(`[PRODUCTS PAGE] ⚠️ No products in context!`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Debug: Log final context before rendering
|
|
708
|
+
console.log(`[PRODUCTS PAGE] Final context before rendering:`);
|
|
709
|
+
console.log(` - Products count: ${context.products?.length || 0}`);
|
|
710
|
+
if (context.products && context.products.length > 0) {
|
|
711
|
+
const firstProduct = context.products[0];
|
|
712
|
+
console.log(` - First product:`, {
|
|
713
|
+
id: firstProduct.id,
|
|
714
|
+
title: firstProduct.title || firstProduct.name,
|
|
715
|
+
url: firstProduct.url || firstProduct.link,
|
|
716
|
+
price: firstProduct.price || firstProduct.sellingPrice,
|
|
717
|
+
stock: firstProduct.stock,
|
|
718
|
+
inStock: firstProduct.inStock,
|
|
719
|
+
hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage1),
|
|
720
|
+
imageUrl: firstProduct.imageUrl || firstProduct.thumbnailImage1?.url
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
if (context.collection) {
|
|
724
|
+
console.log(` - Collection: ${context.collection.title || context.collection.name}, Products: ${context.collection.products?.length || 0}`);
|
|
620
725
|
}
|
|
621
726
|
|
|
622
727
|
const html = await renderWithLayout(this.liquid, 'templates/products', context, this.themePath);
|
|
@@ -795,51 +900,31 @@ class DevServer {
|
|
|
795
900
|
this.app.get('/collections/:handle', async (req, res, next) => {
|
|
796
901
|
try {
|
|
797
902
|
const context = await this.buildContext(req, 'collection', { collectionHandle: req.params.handle });
|
|
798
|
-
|
|
799
|
-
const category = context.categories?.find(c =>
|
|
800
|
-
|
|
801
|
-
c.
|
|
802
|
-
|
|
903
|
+
const h = (req.params.handle || '').toLowerCase();
|
|
904
|
+
const category = context.categories?.find(c => {
|
|
905
|
+
const ch = (c.handle || '').toLowerCase();
|
|
906
|
+
const cs = (c.slug || '').toLowerCase();
|
|
907
|
+
const cn = (c.name || '').toLowerCase().replace(/\s+/g, '-');
|
|
908
|
+
return ch === h || cs === h || cn === h;
|
|
909
|
+
});
|
|
803
910
|
if (category) {
|
|
804
911
|
context.category = category;
|
|
805
912
|
context.collection = category;
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const filteredProducts = (context.products || []).filter(p =>
|
|
809
|
-
p.categoryId === category.id ||
|
|
810
|
-
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
|
|
811
|
-
);
|
|
812
|
-
|
|
813
|
-
// Ensure all products have proper URL field if missing
|
|
814
|
-
filteredProducts.forEach(product => {
|
|
815
|
-
if (!product.url && !product.link) {
|
|
816
|
-
const handle = product.handle || product.slug || product.id;
|
|
817
|
-
product.url = `/products/${handle}`;
|
|
818
|
-
product.link = `/products/${handle}`;
|
|
819
|
-
}
|
|
820
|
-
});
|
|
821
|
-
|
|
913
|
+
const filtered = (context.products || []).filter(p => this.productMatchesCategory(p, category));
|
|
914
|
+
const filteredProducts = this.mode === 'real' ? filtered : filtered.map(p => this.enrichProductData(p));
|
|
822
915
|
context.collection.products = filteredProducts;
|
|
823
916
|
context.collection.totalProducts = filteredProducts.length;
|
|
824
|
-
context.products = filteredProducts;
|
|
825
|
-
|
|
917
|
+
context.products = filteredProducts;
|
|
826
918
|
console.log(`[COLLECTION PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
|
|
827
919
|
} else {
|
|
828
|
-
|
|
829
|
-
context.products = (context.products || []).map(product => {
|
|
830
|
-
if (!product.url && !product.link) {
|
|
831
|
-
const handle = product.handle || product.slug || product.id;
|
|
832
|
-
product.url = `/products/${handle}`;
|
|
833
|
-
product.link = `/products/${handle}`;
|
|
834
|
-
}
|
|
835
|
-
return product;
|
|
836
|
-
});
|
|
920
|
+
if (this.mode !== 'real') context.products = this.enrichProductsData(context.products || []);
|
|
837
921
|
context.collection = context.collection || {};
|
|
838
922
|
context.collection.products = context.products;
|
|
839
923
|
}
|
|
840
924
|
|
|
925
|
+
// Prioritize products template to show product list, not categories list
|
|
841
926
|
let html;
|
|
842
|
-
const templateOptions = ['templates/
|
|
927
|
+
const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
|
|
843
928
|
for (const template of templateOptions) {
|
|
844
929
|
try {
|
|
845
930
|
html = await renderWithLayout(this.liquid, template, context, this.themePath);
|
|
@@ -891,50 +976,31 @@ class DevServer {
|
|
|
891
976
|
this.app.get('/categories/:handle', async (req, res, next) => {
|
|
892
977
|
try {
|
|
893
978
|
const context = await this.buildContext(req, 'collection', { collectionHandle: req.params.handle });
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
c.
|
|
897
|
-
|
|
979
|
+
const h = (req.params.handle || '').toLowerCase();
|
|
980
|
+
const category = context.categories?.find(c => {
|
|
981
|
+
const ch = (c.handle || '').toLowerCase();
|
|
982
|
+
const cs = (c.slug || '').toLowerCase();
|
|
983
|
+
const cn = (c.name || '').toLowerCase().replace(/\s+/g, '-');
|
|
984
|
+
return ch === h || cs === h || cn === h;
|
|
985
|
+
});
|
|
898
986
|
if (category) {
|
|
899
987
|
context.category = category;
|
|
900
988
|
context.collection = category;
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const filteredProducts = (context.products || []).filter(p =>
|
|
904
|
-
p.categoryId === category.id ||
|
|
905
|
-
String(p.categoryId).toLowerCase() === String(category.id).toLowerCase()
|
|
906
|
-
);
|
|
907
|
-
|
|
908
|
-
// Ensure all products have proper URL field if missing
|
|
909
|
-
filteredProducts.forEach(product => {
|
|
910
|
-
if (!product.url && !product.link) {
|
|
911
|
-
const handle = product.handle || product.slug || product.id;
|
|
912
|
-
product.url = `/products/${handle}`;
|
|
913
|
-
product.link = `/products/${handle}`;
|
|
914
|
-
}
|
|
915
|
-
});
|
|
916
|
-
|
|
989
|
+
const filtered = (context.products || []).filter(p => this.productMatchesCategory(p, category));
|
|
990
|
+
const filteredProducts = this.mode === 'real' ? filtered : filtered.map(p => this.enrichProductData(p));
|
|
917
991
|
context.collection.products = filteredProducts;
|
|
918
992
|
context.collection.totalProducts = filteredProducts.length;
|
|
919
|
-
context.products = filteredProducts;
|
|
920
|
-
|
|
993
|
+
context.products = filteredProducts;
|
|
921
994
|
console.log(`[CATEGORY PAGE] Category: ${category.name}, Products: ${filteredProducts.length}`);
|
|
922
995
|
} else {
|
|
923
|
-
|
|
924
|
-
context.products = (context.products || []).map(product => {
|
|
925
|
-
if (!product.url && !product.link) {
|
|
926
|
-
const handle = product.handle || product.slug || product.id;
|
|
927
|
-
product.url = `/products/${handle}`;
|
|
928
|
-
product.link = `/products/${handle}`;
|
|
929
|
-
}
|
|
930
|
-
return product;
|
|
931
|
-
});
|
|
996
|
+
if (this.mode !== 'real') context.products = this.enrichProductsData(context.products || []);
|
|
932
997
|
context.collection = context.collection || {};
|
|
933
998
|
context.collection.products = context.products;
|
|
934
999
|
}
|
|
935
1000
|
|
|
1001
|
+
// Prioritize products template to show product list, not categories list
|
|
936
1002
|
let html;
|
|
937
|
-
const templateOptions = ['templates/
|
|
1003
|
+
const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
|
|
938
1004
|
for (const template of templateOptions) {
|
|
939
1005
|
try {
|
|
940
1006
|
html = await renderWithLayout(this.liquid, template, context, this.themePath);
|
|
@@ -1055,41 +1121,38 @@ class DevServer {
|
|
|
1055
1121
|
try {
|
|
1056
1122
|
const context = await this.buildContext(req, 'page', { pageHandle: req.params.handle });
|
|
1057
1123
|
|
|
1058
|
-
// Load page content
|
|
1124
|
+
// Load page content
|
|
1059
1125
|
try {
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
context.page = {
|
|
1064
|
-
...context.page,
|
|
1065
|
-
...pageResponse.data,
|
|
1066
|
-
body_html: pageResponse.data.content || pageResponse.data.htmlContent,
|
|
1067
|
-
content: pageResponse.data.content || pageResponse.data.htmlContent
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
} catch (error) {
|
|
1071
|
-
console.warn(`[PAGE] Failed to load landing page for ${req.params.handle}:`, error.message);
|
|
1072
|
-
// Fallback to mock API pages endpoint
|
|
1073
|
-
try {
|
|
1074
|
-
const axios = require('axios');
|
|
1075
|
-
const pageResponse = await axios.get(`http://localhost:${this.mockApiPort}/webstoreapi/pages/${req.params.handle}`);
|
|
1076
|
-
if (pageResponse.data) {
|
|
1126
|
+
if (this.mode === 'real') {
|
|
1127
|
+
const pageData = await webstoreApi.fetchPage(req.params.handle, { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} });
|
|
1128
|
+
if (pageData) {
|
|
1077
1129
|
context.page = {
|
|
1078
1130
|
...context.page,
|
|
1079
|
-
...
|
|
1080
|
-
body_html:
|
|
1081
|
-
content:
|
|
1131
|
+
...pageData,
|
|
1132
|
+
body_html: pageData.content || pageData.body_html,
|
|
1133
|
+
content: pageData.content || pageData.body_html
|
|
1082
1134
|
};
|
|
1083
1135
|
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
}
|
|
1136
|
+
} else {
|
|
1137
|
+
const axios = require('axios');
|
|
1138
|
+
try {
|
|
1139
|
+
const pageResponse = await axios.get(`http://localhost:${this.mockApiPort}/shopfront/api/v2/page/${req.params.handle}`);
|
|
1140
|
+
if (pageResponse.data) {
|
|
1141
|
+
context.page = { ...context.page, ...pageResponse.data, body_html: pageResponse.data.content || pageResponse.data.htmlContent, content: pageResponse.data.content || pageResponse.data.htmlContent };
|
|
1142
|
+
}
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
const fallback = await axios.get(`http://localhost:${this.mockApiPort}/webstoreapi/pages/${req.params.handle}`);
|
|
1145
|
+
if (fallback.data) {
|
|
1146
|
+
context.page = { ...context.page, ...fallback.data, body_html: fallback.data.content, content: fallback.data.content };
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1092
1149
|
}
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
console.warn(`[PAGE] Failed to load page for ${req.params.handle}:`, error.message);
|
|
1152
|
+
}
|
|
1153
|
+
if (!context.page.body_html && !context.page.content) {
|
|
1154
|
+
const t = req.params.handle.charAt(0).toUpperCase() + req.params.handle.slice(1).replace(/-/g, ' ');
|
|
1155
|
+
context.page = { ...context.page, title: t, handle: req.params.handle, body_html: `<h2>${t}</h2><p>Landing page content coming soon.</p>`, content: `<h2>${t}</h2><p>Landing page content coming soon.</p>` };
|
|
1093
1156
|
}
|
|
1094
1157
|
|
|
1095
1158
|
// Try page.liquid template first, then fallback to rendering raw HTML
|
|
@@ -1116,30 +1179,27 @@ class DevServer {
|
|
|
1116
1179
|
try {
|
|
1117
1180
|
const context = await this.buildContext(req, 'page', { pageHandle: req.params.handle });
|
|
1118
1181
|
|
|
1119
|
-
// Load page content from mock API
|
|
1120
1182
|
try {
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1183
|
+
if (this.mode === 'real') {
|
|
1184
|
+
const pageData = await webstoreApi.fetchPage(req.params.handle, { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} });
|
|
1185
|
+
if (pageData) {
|
|
1186
|
+
context.page = { ...context.page, ...pageData, body_html: pageData.content || pageData.body_html, content: pageData.content || pageData.body_html };
|
|
1187
|
+
}
|
|
1188
|
+
} else {
|
|
1189
|
+
const axios = require('axios');
|
|
1190
|
+
const pageResponse = await axios.get(`http://localhost:${this.mockApiPort}/webstoreapi/pages/${req.params.handle}`);
|
|
1191
|
+
if (pageResponse.data) {
|
|
1192
|
+
context.page = { ...context.page, ...pageResponse.data, body_html: pageResponse.data.content, content: pageResponse.data.content };
|
|
1193
|
+
}
|
|
1130
1194
|
}
|
|
1131
1195
|
} catch (error) {
|
|
1132
1196
|
console.warn(`[PAGE] Failed to load page content for ${req.params.handle}:`, error.message);
|
|
1133
|
-
// Set default page content
|
|
1134
|
-
context.page = {
|
|
1135
|
-
...context.page,
|
|
1136
|
-
title: req.params.handle.charAt(0).toUpperCase() + req.params.handle.slice(1).replace(/-/g, ' '),
|
|
1137
|
-
handle: req.params.handle,
|
|
1138
|
-
body_html: `<h2>${req.params.handle.charAt(0).toUpperCase() + req.params.handle.slice(1).replace(/-/g, ' ')}</h2><p>Page content coming soon.</p>`,
|
|
1139
|
-
content: `<h2>${req.params.handle.charAt(0).toUpperCase() + req.params.handle.slice(1).replace(/-/g, ' ')}</h2><p>Page content coming soon.</p>`
|
|
1140
|
-
};
|
|
1141
1197
|
}
|
|
1142
|
-
|
|
1198
|
+
if (!context.page.body_html && !context.page.content) {
|
|
1199
|
+
const t = req.params.handle.charAt(0).toUpperCase() + req.params.handle.slice(1).replace(/-/g, ' ');
|
|
1200
|
+
context.page = { ...context.page, title: t, handle: req.params.handle, body_html: `<h2>${t}</h2><p>Page content coming soon.</p>`, content: `<h2>${t}</h2><p>Page content coming soon.</p>` };
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1143
1203
|
const html = await renderWithLayout(this.liquid, 'templates/page', context, this.themePath);
|
|
1144
1204
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1145
1205
|
res.setHeader('Cache-Control', 'no-cache');
|
|
@@ -1197,10 +1257,13 @@ class DevServer {
|
|
|
1197
1257
|
const widgetType = req.params.type.replace(/\.liquid$/, '');
|
|
1198
1258
|
const context = await this.buildContext(req, 'home');
|
|
1199
1259
|
|
|
1200
|
-
// Ensure products are loaded for widget context (needed for product widgets)
|
|
1201
1260
|
if (!context.products || context.products.length === 0) {
|
|
1202
1261
|
try {
|
|
1203
|
-
if (this.
|
|
1262
|
+
if (this.mode === 'real') {
|
|
1263
|
+
const res = await webstoreApi.fetchProducts({ limit: 50, offset: 0 }, { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} });
|
|
1264
|
+
context.products = this.normalizeProductsForRealMode(res?.products || res?.Products || []);
|
|
1265
|
+
console.log(`[DEV WIDGET] Loaded ${context.products.length} products (real)`);
|
|
1266
|
+
} else if (this.apiClient) {
|
|
1204
1267
|
const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0 });
|
|
1205
1268
|
context.products = productsResponse.products || productsResponse.data?.products || [];
|
|
1206
1269
|
console.log(`[DEV WIDGET] Loaded ${context.products.length} products for widget context`);
|
|
@@ -1353,44 +1416,143 @@ class DevServer {
|
|
|
1353
1416
|
return next();
|
|
1354
1417
|
}
|
|
1355
1418
|
|
|
1356
|
-
// Build context to get categories and products
|
|
1419
|
+
// Build context to get categories, brands, and products
|
|
1357
1420
|
const context = await this.buildContext(req, 'collection', { collectionHandle: handle });
|
|
1358
1421
|
|
|
1359
|
-
// Try to find category by handle (case-insensitive)
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
(c.
|
|
1363
|
-
|
|
1422
|
+
// Try to find category by handle or slug (case-insensitive)
|
|
1423
|
+
const h = handle.toLowerCase();
|
|
1424
|
+
const category = context.categories?.find(c => {
|
|
1425
|
+
const ch = (c.handle || '').toLowerCase();
|
|
1426
|
+
const cs = (c.slug || '').toLowerCase();
|
|
1427
|
+
const cn = (c.name || '').toLowerCase().replace(/\s+/g, '-');
|
|
1428
|
+
return ch === h || cs === h || cn === h;
|
|
1429
|
+
});
|
|
1364
1430
|
|
|
1365
1431
|
if (category) {
|
|
1366
|
-
// Found a category - render collection page
|
|
1367
1432
|
context.category = category;
|
|
1368
1433
|
context.collection = category;
|
|
1434
|
+
console.log(`[ROOT COLLECTION] Found category: ${category.name} (ID: ${category.id})`);
|
|
1435
|
+
console.log(`[ROOT COLLECTION] Total products in context: ${context.products?.length || 0}`);
|
|
1436
|
+
const filtered = (context.products || []).filter(p => this.productMatchesCategory(p, category));
|
|
1437
|
+
const filteredProducts = this.mode === 'real' ? filtered : filtered.map(p => this.enrichProductData(p));
|
|
1438
|
+
console.log(`[ROOT COLLECTION] Filtered products count: ${filteredProducts.length}`);
|
|
1369
1439
|
|
|
1370
|
-
//
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1440
|
+
// Log sample enriched products
|
|
1441
|
+
if (filteredProducts.length > 0) {
|
|
1442
|
+
filteredProducts.slice(0, 3).forEach((product, index) => {
|
|
1443
|
+
console.log(`[ROOT COLLECTION] Product ${index} AFTER enrichment:`, {
|
|
1444
|
+
id: product.id,
|
|
1445
|
+
title: product.title || product.name,
|
|
1446
|
+
url: product.url || product.link,
|
|
1447
|
+
price: product.price || product.sellingPrice,
|
|
1448
|
+
stock: product.stock,
|
|
1449
|
+
inStock: product.inStock,
|
|
1450
|
+
hasImage: !!(product.imageUrl || product.thumbnailImage1),
|
|
1451
|
+
imageUrl: product.imageUrl || product.thumbnailImage1?.url
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1384
1455
|
|
|
1456
|
+
// Set products in multiple places to ensure templates can access them
|
|
1385
1457
|
context.collection.products = filteredProducts;
|
|
1386
1458
|
context.collection.totalProducts = filteredProducts.length;
|
|
1387
1459
|
context.products = filteredProducts; // Also set in products array for consistency
|
|
1388
1460
|
|
|
1389
|
-
|
|
1461
|
+
// Also set in collection object for template compatibility
|
|
1462
|
+
if (!context.collection.products) {
|
|
1463
|
+
context.collection.products = filteredProducts;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
console.log(`[ROOT COLLECTION] Final context setup:`);
|
|
1467
|
+
console.log(` - context.products.length: ${context.products?.length || 0}`);
|
|
1468
|
+
console.log(` - context.collection.products.length: ${context.collection.products?.length || 0}`);
|
|
1469
|
+
console.log(` - context.collection.totalProducts: ${context.collection.totalProducts || 0}`);
|
|
1390
1470
|
|
|
1391
|
-
|
|
1471
|
+
if (filteredProducts.length > 0) {
|
|
1472
|
+
const sample = filteredProducts[0];
|
|
1473
|
+
console.log(`[ROOT COLLECTION] Sample enriched product:`, {
|
|
1474
|
+
id: sample.id,
|
|
1475
|
+
title: sample.title || sample.name,
|
|
1476
|
+
url: sample.url || sample.link,
|
|
1477
|
+
price: sample.price || sample.sellingPrice,
|
|
1478
|
+
stock: sample.stock,
|
|
1479
|
+
inStock: sample.inStock,
|
|
1480
|
+
hasImage: !!(sample.imageUrl || sample.thumbnailImage1),
|
|
1481
|
+
imageUrl: sample.imageUrl || sample.thumbnailImage1?.url,
|
|
1482
|
+
allKeys: Object.keys(sample).slice(0, 20)
|
|
1483
|
+
});
|
|
1484
|
+
} else {
|
|
1485
|
+
console.warn(`[ROOT COLLECTION] ⚠️ No products found for category ${category.name}!`);
|
|
1486
|
+
console.warn(`[ROOT COLLECTION] Available products in context: ${context.products?.length || 0}`);
|
|
1487
|
+
if (context.products && context.products.length > 0) {
|
|
1488
|
+
console.warn(`[ROOT COLLECTION] Sample product categoryIds:`, context.products.slice(0, 3).map(p => ({
|
|
1489
|
+
id: p.id,
|
|
1490
|
+
title: p.title || p.name,
|
|
1491
|
+
categoryId: p.categoryId
|
|
1492
|
+
})));
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Debug: Log final context before rendering
|
|
1497
|
+
console.log(`[ROOT COLLECTION] Final context before rendering:`);
|
|
1498
|
+
console.log(` - Products count: ${context.products?.length || 0}`);
|
|
1499
|
+
if (context.products && context.products.length > 0) {
|
|
1500
|
+
const firstProduct = context.products[0];
|
|
1501
|
+
console.log(` - First product:`, {
|
|
1502
|
+
id: firstProduct.id,
|
|
1503
|
+
title: firstProduct.title || firstProduct.name,
|
|
1504
|
+
url: firstProduct.url || firstProduct.link,
|
|
1505
|
+
price: firstProduct.price || firstProduct.sellingPrice,
|
|
1506
|
+
stock: firstProduct.stock,
|
|
1507
|
+
inStock: firstProduct.inStock,
|
|
1508
|
+
hasImage: !!(firstProduct.imageUrl || firstProduct.thumbnailImage1)
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Try to render products template first (to show product list, not categories list)
|
|
1392
1513
|
let html;
|
|
1393
|
-
const templateOptions = ['templates/
|
|
1514
|
+
const templateOptions = ['templates/products', 'templates/collection', 'templates/category', 'templates/categories'];
|
|
1515
|
+
for (const template of templateOptions) {
|
|
1516
|
+
try {
|
|
1517
|
+
html = await renderWithLayout(this.liquid, template, context, this.themePath);
|
|
1518
|
+
break;
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
if (!error.message?.includes('Template not found')) {
|
|
1521
|
+
throw error;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (html) {
|
|
1527
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
1528
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1529
|
+
return res.send(html);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Not a category - try to find brand by handle
|
|
1534
|
+
const brand = context.brands?.find(b =>
|
|
1535
|
+
(b.handle && b.handle.toLowerCase() === handle.toLowerCase()) ||
|
|
1536
|
+
(b.name && b.name.toLowerCase().replace(/\s+/g, '-') === handle.toLowerCase())
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
if (brand) {
|
|
1540
|
+
context.brand = brand;
|
|
1541
|
+
const filtered = (context.products || []).filter(p =>
|
|
1542
|
+
p.brandId === brand.id ||
|
|
1543
|
+
String(p.brandId).toLowerCase() === String(brand.id).toLowerCase() ||
|
|
1544
|
+
(p.brand && p.brand.toLowerCase() === brand.name.toLowerCase())
|
|
1545
|
+
);
|
|
1546
|
+
const filteredProducts = this.mode === 'real' ? filtered : filtered.map(p => this.enrichProductData(p));
|
|
1547
|
+
|
|
1548
|
+
context.brand.products = filteredProducts;
|
|
1549
|
+
context.products = filteredProducts;
|
|
1550
|
+
|
|
1551
|
+
console.log(`[ROOT BRAND] Brand: ${brand.name}, Products: ${filteredProducts.length}`);
|
|
1552
|
+
|
|
1553
|
+
// Try to render products template
|
|
1554
|
+
let html;
|
|
1555
|
+
const templateOptions = ['templates/products', 'templates/brand', 'templates/collection', 'templates/brands'];
|
|
1394
1556
|
for (const template of templateOptions) {
|
|
1395
1557
|
try {
|
|
1396
1558
|
html = await renderWithLayout(this.liquid, template, context, this.themePath);
|
|
@@ -1447,20 +1609,16 @@ class DevServer {
|
|
|
1447
1609
|
}
|
|
1448
1610
|
});
|
|
1449
1611
|
|
|
1450
|
-
// Favicon handler
|
|
1612
|
+
// Favicon handler: serve from theme assets if present, else 204
|
|
1451
1613
|
this.app.get('/favicon.ico', (req, res) => {
|
|
1452
|
-
|
|
1614
|
+
const faviconPath = path.join(this.themePath, 'assets', 'favicon.ico');
|
|
1615
|
+
if (fs.existsSync(faviconPath)) {
|
|
1616
|
+
res.setHeader('Content-Type', 'image/x-icon');
|
|
1617
|
+
return res.sendFile('favicon.ico', { root: path.join(this.themePath, 'assets') });
|
|
1618
|
+
}
|
|
1619
|
+
res.status(204).end();
|
|
1453
1620
|
});
|
|
1454
1621
|
|
|
1455
|
-
// API proxy (for real API mode)
|
|
1456
|
-
if (this.mode === 'real' && this.apiClient) {
|
|
1457
|
-
this.app.use('/api', (req, res, next) => {
|
|
1458
|
-
// Proxy API requests to real API
|
|
1459
|
-
// This is handled by mock API in mock mode
|
|
1460
|
-
next();
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
1622
|
// Error handler
|
|
1465
1623
|
this.app.use((error, req, res, next) => {
|
|
1466
1624
|
// Ignore request aborted errors - they're harmless (client cancelled request)
|
|
@@ -1677,48 +1835,29 @@ class DevServer {
|
|
|
1677
1835
|
const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0 });
|
|
1678
1836
|
context.products = productsResponse.products || productsResponse.data?.products || [];
|
|
1679
1837
|
|
|
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
|
-
});
|
|
1838
|
+
// Enrich all products using the reusable enrichment function
|
|
1839
|
+
// This ensures consistency with products used in widgets and pages
|
|
1840
|
+
context.products = this.enrichProductsData(context.products);
|
|
1841
|
+
|
|
1842
|
+
// Debug first few products to verify enrichment
|
|
1843
|
+
if (context.products.length > 0) {
|
|
1844
|
+
context.products.slice(0, 3).forEach((product, index) => {
|
|
1845
|
+
console.log(`[CONTEXT] Product ${index + 1} after enrichment:`, {
|
|
1846
|
+
id: product.id,
|
|
1847
|
+
handle: product.handle || 'none',
|
|
1848
|
+
url: product.url || product.link || 'MISSING',
|
|
1849
|
+
title: product.title || product.name || 'MISSING',
|
|
1850
|
+
price: product.price || product.sellingPrice || 'MISSING',
|
|
1851
|
+
hasPrices: !!product.prices,
|
|
1852
|
+
stock: product.stock !== undefined ? product.stock : 'MISSING',
|
|
1853
|
+
stockQuantity: product.stockQuantity !== undefined ? product.stockQuantity : 'MISSING',
|
|
1854
|
+
inStock: product.inStock !== undefined ? product.inStock : 'MISSING',
|
|
1855
|
+
available: product.available !== undefined ? product.available : 'MISSING',
|
|
1856
|
+
hasImage: !!(product.imageUrl || product.thumbnailImage1),
|
|
1857
|
+
imageUrl: product.imageUrl || product.thumbnailImage1?.url || 'MISSING'
|
|
1858
|
+
});
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1722
1861
|
|
|
1723
1862
|
console.log(`[CONTEXT] Loaded ${context.products.length} products`);
|
|
1724
1863
|
|
|
@@ -1746,6 +1885,18 @@ class DevServer {
|
|
|
1746
1885
|
try {
|
|
1747
1886
|
const categoriesResponse = await this.apiClient.getCategories({ limit: 50, offset: 0 });
|
|
1748
1887
|
context.categories = categoriesResponse.categories || categoriesResponse.data?.categories || categoriesResponse || [];
|
|
1888
|
+
|
|
1889
|
+
// Ensure all categories have proper URL fields for navigation
|
|
1890
|
+
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}`;
|
|
1896
|
+
}
|
|
1897
|
+
return category;
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1749
1900
|
context.collections = context.categories; // Also set collections for compatibility
|
|
1750
1901
|
console.log(`[CONTEXT] Loaded ${context.categories.length} categories`);
|
|
1751
1902
|
} catch (error) {
|
|
@@ -1789,6 +1940,14 @@ class DevServer {
|
|
|
1789
1940
|
console.error('[CONTEXT] Error details:', error.response?.status, error.response?.data);
|
|
1790
1941
|
context.menus = [];
|
|
1791
1942
|
}
|
|
1943
|
+
|
|
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];
|
|
1949
|
+
}
|
|
1950
|
+
context.mainMenu = mainMenu;
|
|
1792
1951
|
|
|
1793
1952
|
// Load cart data
|
|
1794
1953
|
try {
|
|
@@ -1885,59 +2044,164 @@ class DevServer {
|
|
|
1885
2044
|
content: []
|
|
1886
2045
|
};
|
|
1887
2046
|
}
|
|
1888
|
-
} else if (this.
|
|
1889
|
-
//
|
|
2047
|
+
} else if (this.mode === 'real') {
|
|
2048
|
+
// Real mode: use webstoreapi-fetcher only (no apiClient). Thin HTTP calls to /webstoreapi/*.
|
|
2049
|
+
const opts = { headers: req.headers.cookie ? { Cookie: req.headers.cookie } : {} };
|
|
2050
|
+
const baseHost = (process.env.O2VEND_API_BASE_URL || '').replace(/^https?:\/\//, '').replace(/\/.*$/, '') || 'localhost';
|
|
2051
|
+
const themeSettings = context.settings || {};
|
|
2052
|
+
let storeInfo = null;
|
|
1890
2053
|
try {
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2054
|
+
storeInfo = await webstoreApi.fetchStoreInfo();
|
|
2055
|
+
} catch (e) {
|
|
2056
|
+
/* use fallbacks */
|
|
2057
|
+
}
|
|
2058
|
+
const logo = (storeInfo && storeInfo.logoUrl) ? storeInfo.logoUrl : '/assets/logo.png';
|
|
2059
|
+
const favicon = (storeInfo && storeInfo.favouriteIconUrl) ? storeInfo.favouriteIconUrl : '/favicon.ico';
|
|
2060
|
+
const storeName = (storeInfo && storeInfo.name) ? storeInfo.name : 'Store';
|
|
2061
|
+
const storeSettings = (storeInfo && storeInfo.settings) || {};
|
|
2062
|
+
try {
|
|
2063
|
+
context.shop = {
|
|
2064
|
+
name: storeName,
|
|
2065
|
+
domain: baseHost,
|
|
2066
|
+
description: 'O2VEND Store',
|
|
2067
|
+
email: storeSettings.companyEmail || 'store@example.com',
|
|
2068
|
+
logo,
|
|
2069
|
+
favicon,
|
|
2070
|
+
currency: storeSettings.currency || themeSettings.currency || 'USD',
|
|
2071
|
+
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
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
context.tenant = { id: process.env.O2VEND_TENANT_ID || 'real' };
|
|
2081
|
+
|
|
2082
|
+
if (process.env.O2VEND_LAYOUT_FULL === '1' || process.env.O2VEND_LAYOUT_FULL === 'true') {
|
|
2083
|
+
context.settings = context.settings || {};
|
|
2084
|
+
context.settings.layout_style = 'full-width';
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const page = pageType === 'home' ? 'home' : (pageType === 'products' ? 'products' : 'home');
|
|
2088
|
+
context.widgets = await webstoreApi.fetchWidgetsBySections(page, webstoreApi.DEFAULT_SECTIONS, opts);
|
|
2089
|
+
context.widgets = context.widgets || { hero: [], products: [], footer: [], content: [], header: [] };
|
|
2090
|
+
|
|
2091
|
+
if (this.widgetService) {
|
|
2092
|
+
Object.keys(context.widgets).forEach((section) => {
|
|
2093
|
+
if (Array.isArray(context.widgets[section])) {
|
|
2094
|
+
context.widgets[section].forEach((widget) => {
|
|
2095
|
+
if (!widget.template_path && widget.template) widget.template_path = `widgets/${widget.template}`;
|
|
2096
|
+
if (!widget.template_path && widget.type) {
|
|
2097
|
+
const slug = this.widgetService.getTemplateSlug(widget.type);
|
|
2098
|
+
widget.template_path = `widgets/${slug}`;
|
|
2099
|
+
widget.template = slug;
|
|
2100
|
+
}
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
const productsRes = await webstoreApi.fetchProducts({ limit: 50, offset: 0 }, opts);
|
|
2107
|
+
context.products = productsRes?.products || productsRes?.Products || [];
|
|
2108
|
+
context.products = this.normalizeProductsForRealMode(context.products);
|
|
2109
|
+
|
|
2110
|
+
try {
|
|
2111
|
+
const catRes = await webstoreApi.fetchCategories({ limit: 50, offset: 0 }, opts);
|
|
2112
|
+
const raw = catRes?.categories || catRes?.data?.categories || catRes || [];
|
|
2113
|
+
context.categories = raw.map((c) => {
|
|
2114
|
+
const h = (c.handle || (c.name || '').toLowerCase().replace(/\s+/g, '-') || c.id || '').toString().replace(/^\s+|\s+$/g, '') || null;
|
|
2115
|
+
if (h) {
|
|
2116
|
+
c.handle = c.handle || h;
|
|
2117
|
+
c.slug = c.slug || h;
|
|
2118
|
+
c.url = `/${h}`;
|
|
2119
|
+
c.link = `/${h}`;
|
|
2120
|
+
}
|
|
2121
|
+
return c;
|
|
2122
|
+
});
|
|
2123
|
+
context.collections = context.categories;
|
|
2124
|
+
} catch (e) {
|
|
2125
|
+
context.categories = [];
|
|
2126
|
+
context.collections = [];
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
try {
|
|
2130
|
+
const brandRes = await webstoreApi.fetchBrands({ limit: 50, offset: 0 }, opts);
|
|
2131
|
+
context.brands = brandRes?.brands || brandRes?.data?.brands || brandRes || [];
|
|
2132
|
+
} catch (e) {
|
|
2133
|
+
context.brands = [];
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
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 */
|
|
1910
2148
|
}
|
|
1911
|
-
}
|
|
2149
|
+
}
|
|
1912
2150
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
context.
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2151
|
+
context.mainMenu = mainMenu;
|
|
2152
|
+
} catch (e) {
|
|
2153
|
+
context.menus = [];
|
|
2154
|
+
context.mainMenu = null;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
try {
|
|
2158
|
+
const cartRes = await webstoreApi.fetchCart(opts);
|
|
2159
|
+
const data = cartRes?.data || cartRes;
|
|
2160
|
+
if (data) {
|
|
2161
|
+
context.cart = {
|
|
2162
|
+
...context.cart,
|
|
2163
|
+
...data,
|
|
2164
|
+
itemCount: data.items?.length ?? data.itemCount ?? 0,
|
|
2165
|
+
total: data.total ?? data.subTotal ?? 0
|
|
2166
|
+
};
|
|
1927
2167
|
}
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
|
|
2168
|
+
} catch (e) {
|
|
2169
|
+
/* cart optional */
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
if (pageType === 'products') {
|
|
2173
|
+
context.collection = context.collection || {};
|
|
2174
|
+
context.collection.products = context.products;
|
|
2175
|
+
context.collection.title = 'All Products';
|
|
2176
|
+
context.collection.handle = 'all';
|
|
2177
|
+
context.collection.totalProducts = context.products.length;
|
|
1931
2178
|
} else if (pageType === 'collection') {
|
|
1932
|
-
// Load products for collection
|
|
1933
|
-
const productsResponse = await this.apiClient.getProducts({ limit: 50, offset: 0 });
|
|
1934
|
-
context.products = productsResponse.products || [];
|
|
1935
|
-
// Set collection.products for collection pages
|
|
1936
2179
|
context.collection = context.collection || {};
|
|
1937
2180
|
context.collection.products = context.products;
|
|
2181
|
+
} else if (pageType === 'product' && extra.productHandle) {
|
|
2182
|
+
const handle = String(extra.productHandle).toLowerCase().trim();
|
|
2183
|
+
let product = context.products.find((p) => {
|
|
2184
|
+
const h = (p.handle || '').toLowerCase().trim();
|
|
2185
|
+
const s = (p.slug || '').toLowerCase().trim();
|
|
2186
|
+
const id = String(p.id || '').toLowerCase().trim();
|
|
2187
|
+
const pid = String(p.productId || '').toLowerCase().trim();
|
|
2188
|
+
if (h === handle || s === handle || id === handle) return true;
|
|
2189
|
+
if (handle.startsWith('product-') && (pid === handle.replace(/^product-/, '') || id === handle.replace(/^product-/, ''))) return true;
|
|
2190
|
+
return false;
|
|
2191
|
+
});
|
|
2192
|
+
if (!product) {
|
|
2193
|
+
try {
|
|
2194
|
+
const idToFetch = handle.startsWith('product-') ? handle.replace(/^product-/, '') : handle;
|
|
2195
|
+
product = await webstoreApi.fetchProductById(idToFetch, opts);
|
|
2196
|
+
if (product) product = this.normalizeProductForRealMode(product);
|
|
2197
|
+
} catch (e) {
|
|
2198
|
+
/* ignore */
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
if (product) context.product = product;
|
|
1938
2202
|
}
|
|
1939
2203
|
} catch (error) {
|
|
1940
|
-
console.warn('[CONTEXT]
|
|
2204
|
+
console.warn('[CONTEXT] Real mode fetch failed:', error.message);
|
|
1941
2205
|
}
|
|
1942
2206
|
}
|
|
1943
2207
|
|
|
@@ -1975,19 +2239,18 @@ class DevServer {
|
|
|
1975
2239
|
// Widget templates use: widget.data.products, widget_data.products
|
|
1976
2240
|
if (type.includes('product') || type === 'featuredproducts' || type === 'bestsellerproducts' || type === 'newproducts' || type === 'simpleproduct' || type === 'singleproduct') {
|
|
1977
2241
|
const widgetProducts = this.getProductsForWidget(widget, products);
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
widget.products =
|
|
1983
|
-
|
|
1984
|
-
|
|
2242
|
+
const formatted = this.mode === 'real'
|
|
2243
|
+
? this.normalizeProductsForRealMode(widgetProducts)
|
|
2244
|
+
: this.enrichProductsData(widgetProducts);
|
|
2245
|
+
|
|
2246
|
+
widget.data.products = formatted;
|
|
2247
|
+
widget.data.Products = formatted;
|
|
2248
|
+
widget.products = formatted;
|
|
1985
2249
|
widget.data.content = widget.data.content || {};
|
|
1986
|
-
widget.data.content.products =
|
|
1987
|
-
widget.data.content.Products =
|
|
1988
|
-
|
|
2250
|
+
widget.data.content.products = formatted;
|
|
2251
|
+
widget.data.content.Products = formatted;
|
|
1989
2252
|
enrichedCount++;
|
|
1990
|
-
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${
|
|
2253
|
+
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${formatted.length} products`);
|
|
1991
2254
|
}
|
|
1992
2255
|
|
|
1993
2256
|
// Enrich CategoryList and CategoryListCarousel widgets with categories
|
|
@@ -2024,11 +2287,16 @@ class DevServer {
|
|
|
2024
2287
|
|
|
2025
2288
|
// Enrich RecentlyViewed widgets with products (use first 6 products as mock recent views)
|
|
2026
2289
|
if (type === 'recentlyviewed' || type === 'recently-viewed') {
|
|
2027
|
-
const
|
|
2290
|
+
const recent = products.slice(0, 6);
|
|
2291
|
+
const recentProducts = this.mode === 'real'
|
|
2292
|
+
? this.normalizeProductsForRealMode(recent)
|
|
2293
|
+
: this.enrichProductsData(recent);
|
|
2028
2294
|
widget.data.products = recentProducts;
|
|
2029
2295
|
widget.data.Products = recentProducts;
|
|
2030
2296
|
widget.products = recentProducts;
|
|
2031
|
-
|
|
2297
|
+
widget.data.content = widget.data.content || {};
|
|
2298
|
+
widget.data.content.products = recentProducts;
|
|
2299
|
+
widget.data.content.Products = recentProducts;
|
|
2032
2300
|
enrichedCount++;
|
|
2033
2301
|
console.log(`[ENRICH] ${widget.type} (${widget.id}): Added ${recentProducts.length} recently viewed products`);
|
|
2034
2302
|
}
|
|
@@ -2038,37 +2306,322 @@ class DevServer {
|
|
|
2038
2306
|
console.log(`[CONTEXT] ✅ Enriched ${enrichedCount} widgets with data`);
|
|
2039
2307
|
}
|
|
2040
2308
|
|
|
2309
|
+
/**
|
|
2310
|
+
* Check if a product belongs to a category (for collection/category filtering).
|
|
2311
|
+
* Handles categoryId, primaryCategoryId, categoryHandle, and categories[].
|
|
2312
|
+
* @param {Object} p - Product
|
|
2313
|
+
* @param {Object} category - Category { id, handle }
|
|
2314
|
+
* @returns {boolean}
|
|
2315
|
+
*/
|
|
2316
|
+
productMatchesCategory(p, category) {
|
|
2317
|
+
const cid = category.id;
|
|
2318
|
+
const ch = (category.handle || category.slug || '').toLowerCase();
|
|
2319
|
+
if (p.categoryId != null && (p.categoryId === cid || String(p.categoryId).toLowerCase() === String(cid).toLowerCase())) return true;
|
|
2320
|
+
if (p.primaryCategoryId != null && (p.primaryCategoryId === cid || String(p.primaryCategoryId).toLowerCase() === String(cid).toLowerCase())) return true;
|
|
2321
|
+
const ph = (p.categoryHandle || p.category?.handle || '').toLowerCase();
|
|
2322
|
+
if (ch && ph && ph === ch) return true;
|
|
2323
|
+
const arr = p.categories || p.categoryIds || [];
|
|
2324
|
+
if (Array.isArray(arr) && arr.some(cat => {
|
|
2325
|
+
const o = typeof cat === 'object' ? cat : { id: cat };
|
|
2326
|
+
return o.id === cid || (o.handle || o.slug || '').toLowerCase() === ch;
|
|
2327
|
+
})) return true;
|
|
2328
|
+
return false;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* Enrich a single product with all required data (URL, name/title, price, images, stock)
|
|
2333
|
+
* This ensures consistency between widgets and pages
|
|
2334
|
+
* @param {Object} product - Product object to enrich
|
|
2335
|
+
* @returns {Object} Enriched product (creates a copy to avoid mutation)
|
|
2336
|
+
*/
|
|
2337
|
+
enrichProductData(product) {
|
|
2338
|
+
// Create a copy to avoid mutating the original
|
|
2339
|
+
const enriched = { ...product };
|
|
2340
|
+
|
|
2341
|
+
// CRITICAL: Ensure productId exists - it's the most reliable identifier
|
|
2342
|
+
if (!enriched.productId && enriched.id) {
|
|
2343
|
+
// Try to extract numeric ID from string ID (e.g., "product-1" -> 1)
|
|
2344
|
+
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
|
+
});
|
|
2356
|
+
}
|
|
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;
|
|
2369
|
+
const effectiveHandle = (handle && String(handle).trim() && String(handle) !== '#')
|
|
2370
|
+
? String(handle).trim().toLowerCase()
|
|
2371
|
+
: (enriched.productId ? `product-${enriched.productId}`.toLowerCase() : null);
|
|
2372
|
+
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
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
|
|
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
|
+
if (!enriched.name && !enriched.title) {
|
|
2419
|
+
enriched.name = `Product ${enriched.id || 'Unknown'}`;
|
|
2420
|
+
enriched.title = enriched.name;
|
|
2421
|
+
} else if (!enriched.name) {
|
|
2422
|
+
enriched.name = enriched.title || `Product ${enriched.id || 'Unknown'}`;
|
|
2423
|
+
} else if (!enriched.title) {
|
|
2424
|
+
enriched.title = enriched.name;
|
|
2425
|
+
}
|
|
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;
|
|
2449
|
+
}
|
|
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
|
+
|
|
2459
|
+
if (!enriched.prices) {
|
|
2460
|
+
const mrpValue = enriched.compareAtPrice || enriched.comparePrice || enriched.mrp || 0;
|
|
2461
|
+
enriched.prices = {
|
|
2462
|
+
price: finalPrice,
|
|
2463
|
+
mrp: mrpValue,
|
|
2464
|
+
currency: enriched.currency || 'USD'
|
|
2465
|
+
};
|
|
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
|
+
}
|
|
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
|
|
2490
|
+
const imageId = enriched.id ? String(enriched.id).replace(/\D/g, '') || '1' : '1';
|
|
2491
|
+
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
|
+
};
|
|
2496
|
+
}
|
|
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;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
return enriched;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/**
|
|
2534
|
+
* Normalize real-API product for template use (no enrichment).
|
|
2535
|
+
* Aliases API keys (Description, Images, etc.) to template-expected keys,
|
|
2536
|
+
* sets url/link for dev navigation, and normalizes image items to { url, altText }.
|
|
2537
|
+
* @param {Object} p - Product from real API
|
|
2538
|
+
* @returns {Object} Normalized product (shallow copy + aliases)
|
|
2539
|
+
*/
|
|
2540
|
+
normalizeProductForRealMode(p) {
|
|
2541
|
+
if (!p || typeof p !== 'object') return p;
|
|
2542
|
+
const n = { ...p };
|
|
2543
|
+
if (n.Description != null && n.description == null) n.description = n.Description;
|
|
2544
|
+
if (n.HtmlContent != null && n.htmlContent == null) n.htmlContent = n.HtmlContent;
|
|
2545
|
+
if (n.ShortDescription != null && n.shortDescription == null) n.shortDescription = n.ShortDescription;
|
|
2546
|
+
if (n.Name != null && n.name == null) n.name = n.Name;
|
|
2547
|
+
if (n.Title != null && n.title == null) n.title = n.Title;
|
|
2548
|
+
if (n.Attributes != null && n.attributes == null) n.attributes = n.Attributes;
|
|
2549
|
+
const t1 = n.ThumbnailImage1 || n.thumbnailImage1;
|
|
2550
|
+
if (t1 != null) {
|
|
2551
|
+
const u = typeof t1 === 'string' ? t1 : (t1.url || t1.Url || t1.imageUrl || t1.ImageUrl);
|
|
2552
|
+
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;
|
|
2557
|
+
}
|
|
2558
|
+
let imgs = n.images || n.Images;
|
|
2559
|
+
if (Array.isArray(imgs) && imgs.length > 0) {
|
|
2560
|
+
n.images = imgs.map((img) => {
|
|
2561
|
+
if (typeof img === 'string') return { url: img, altText: n.name || n.title };
|
|
2562
|
+
const u = img.url || img.Url || img.imageUrl || img.ImageUrl;
|
|
2563
|
+
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 });
|
|
2565
|
+
});
|
|
2566
|
+
} else if (imgs) {
|
|
2567
|
+
n.images = Array.isArray(imgs) ? imgs : [imgs];
|
|
2568
|
+
} else {
|
|
2569
|
+
const single = n.imageUrl || n.ImageUrl;
|
|
2570
|
+
if (single) n.images = [{ url: single, altText: n.name || n.title }];
|
|
2571
|
+
}
|
|
2572
|
+
const handle = (n.handle || n.slug || n.id || '').toString().trim();
|
|
2573
|
+
const h = handle && handle !== '#' ? handle.toLowerCase() : (n.productId ? `product-${n.productId}` : null);
|
|
2574
|
+
if (h) {
|
|
2575
|
+
n.url = `/products/${h}`;
|
|
2576
|
+
n.link = `/products/${h}`;
|
|
2577
|
+
}
|
|
2578
|
+
return n;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
normalizeProductsForRealMode(products) {
|
|
2582
|
+
if (!Array.isArray(products)) return [];
|
|
2583
|
+
return products.map((p) => this.normalizeProductForRealMode(p));
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
* Enrich multiple products with all required data
|
|
2588
|
+
* @param {Array} products - Array of products to enrich
|
|
2589
|
+
* @returns {Array} Array of enriched products
|
|
2590
|
+
*/
|
|
2591
|
+
enrichProductsData(products) {
|
|
2592
|
+
if (!Array.isArray(products)) return [];
|
|
2593
|
+
return products.map(product => this.enrichProductData(product));
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2041
2596
|
/**
|
|
2042
2597
|
* Get products for a specific widget based on its settings
|
|
2043
2598
|
*/
|
|
2044
2599
|
getProductsForWidget(widget, products) {
|
|
2045
2600
|
// Get the limit from widget settings, default to 12
|
|
2046
|
-
const limit = widget.settings?.limit || widget.data?.content?.Limit || 12;
|
|
2601
|
+
const limit = widget.settings?.limit || widget.data?.content?.Limit || widget.settings?.numberOfProducts || widget.settings?.NumberOfProducts || 12;
|
|
2047
2602
|
|
|
2048
2603
|
// Get products based on widget type
|
|
2049
2604
|
const type = (widget.type || '').toLowerCase();
|
|
2050
2605
|
|
|
2606
|
+
let widgetProducts = [];
|
|
2607
|
+
|
|
2051
2608
|
if (type.includes('featured') || type === 'featuredproducts') {
|
|
2052
2609
|
// Featured products - filter by featured tag or just take first N
|
|
2053
2610
|
const featured = products.filter(p => p.tags?.includes('featured') || p.featured);
|
|
2054
|
-
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
if (type.includes('bestseller') || type === 'bestsellerproducts') {
|
|
2611
|
+
widgetProducts = featured.length > 0 ? featured.slice(0, limit) : products.slice(0, limit);
|
|
2612
|
+
} else if (type.includes('bestseller') || type === 'bestsellerproducts') {
|
|
2058
2613
|
// Best seller products - sort by sales or just take random N
|
|
2059
|
-
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
if (type.includes('new') || type === 'newproducts') {
|
|
2614
|
+
widgetProducts = products.slice(0, limit);
|
|
2615
|
+
} else if (type.includes('new') || type === 'newproducts') {
|
|
2063
2616
|
// New products - sort by created date
|
|
2064
2617
|
const sorted = [...products].sort((a, b) =>
|
|
2065
2618
|
new Date(b.createdAt || 0) - new Date(a.createdAt || 0)
|
|
2066
2619
|
);
|
|
2067
|
-
|
|
2620
|
+
widgetProducts = sorted.slice(0, limit);
|
|
2621
|
+
} else {
|
|
2622
|
+
widgetProducts = products.slice(0, limit);
|
|
2068
2623
|
}
|
|
2069
|
-
|
|
2070
|
-
// Default: return first N products
|
|
2071
|
-
return products.slice(0, limit);
|
|
2624
|
+
return widgetProducts;
|
|
2072
2625
|
}
|
|
2073
2626
|
|
|
2074
2627
|
/**
|
|
@@ -2873,6 +3426,22 @@ class DevServer {
|
|
|
2873
3426
|
logLevel: 'silent',
|
|
2874
3427
|
timeout: 2000
|
|
2875
3428
|
}));
|
|
3429
|
+
} else if (this.mode === 'real') {
|
|
3430
|
+
const base = (process.env.O2VEND_API_BASE_URL || '').replace(/\/$/, '').replace(/\/shopfront\/api\/v2\/?$/i, '').replace(/\/webstoreapi\/?$/i, '');
|
|
3431
|
+
if (base) {
|
|
3432
|
+
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
3433
|
+
const proxyOpts = {
|
|
3434
|
+
target: base,
|
|
3435
|
+
changeOrigin: true,
|
|
3436
|
+
logLevel: 'silent',
|
|
3437
|
+
timeout: parseInt(process.env.O2VEND_API_TIMEOUT, 10) || 10000,
|
|
3438
|
+
onProxyReq: (proxyReq, req) => {
|
|
3439
|
+
if (req.headers.cookie) proxyReq.setHeader('Cookie', req.headers.cookie);
|
|
3440
|
+
}
|
|
3441
|
+
};
|
|
3442
|
+
this.app.use('/webstoreapi', createProxyMiddleware(proxyOpts));
|
|
3443
|
+
this.app.use('/shopfront/api', createProxyMiddleware(proxyOpts));
|
|
3444
|
+
}
|
|
2876
3445
|
}
|
|
2877
3446
|
|
|
2878
3447
|
// Inject hot reload script into HTML responses
|