@lightspeed/crane 2.0.5 → 3.1.0

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.
Files changed (131) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/UPGRADE.md +96 -0
  3. package/dist/cli.mjs +44 -25
  4. package/package.json +6 -3
  5. package/template/blank/sections/blank-section/BlankSection.vue +9 -0
  6. package/template/blank/sections/blank-section/assets/blank_section_showcase_1_preview.jpg +0 -0
  7. package/template/blank/sections/blank-section/client.ts +6 -0
  8. package/template/blank/sections/blank-section/server.ts +6 -0
  9. package/template/blank/sections/blank-section/settings/content.ts +2 -0
  10. package/template/blank/sections/blank-section/settings/design.ts +2 -0
  11. package/template/blank/sections/blank-section/settings/layout.ts +2 -0
  12. package/template/blank/sections/blank-section/settings/translations.ts +8 -0
  13. package/template/blank/sections/blank-section/showcases/1.ts +15 -0
  14. package/template/blank/sections/blank-section/showcases/translations.ts +10 -0
  15. package/template/blank/sections/blank-section/type.ts +5 -0
  16. package/template/collections/assets/collection_cover_image.png +0 -0
  17. package/template/collections/example-collection/configuration.ts +21 -0
  18. package/template/crane.config.json +1 -1
  19. package/template/footers/example-footer/ExampleFooter.vue +4 -4
  20. package/template/footers/example-footer/component/LegalLinks.vue +1 -1
  21. package/template/footers/example-footer/component/MadeWith.vue +1 -1
  22. package/template/footers/example-footer/component/ReportAbuse.vue +1 -1
  23. package/template/footers/example-footer/settings/design.ts +5 -4
  24. package/template/footers/example-footer/settings/translations.ts +4 -3
  25. package/template/footers/example-footer/showcases/1.ts +4 -2
  26. package/template/footers/example-footer/showcases/translations.ts +4 -2
  27. package/template/headers/example-header/ExampleHeader.vue +1 -1
  28. package/template/headers/example-header/component/Account.vue +1 -1
  29. package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
  30. package/template/headers/example-header/component/Logo.vue +7 -7
  31. package/template/headers/example-header/component/NavigationMenu.vue +1 -1
  32. package/template/headers/example-header/settings/content.ts +8 -1
  33. package/template/headers/example-header/settings/design.ts +7 -1
  34. package/template/headers/example-header/settings/layout.ts +1 -1
  35. package/template/headers/example-header/settings/translations.ts +4 -2
  36. package/template/headers/example-header/showcases/1.ts +15 -11
  37. package/template/headers/example-header/showcases/2.ts +12 -8
  38. package/template/headers/example-header/showcases/translations.ts +4 -2
  39. package/template/layouts/catalog/example-catalog/components/Icon.vue +14 -14
  40. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/CustomBottomBar.vue +1 -1
  41. package/template/package.json +1 -0
  42. package/template/page-templates/example-template/configuration.ts +8 -10
  43. package/template/page-templates/example-template/pages/catalog.ts +3 -6
  44. package/template/page-templates/example-template/pages/category.ts +3 -6
  45. package/template/page-templates/example-template/pages/home.ts +42 -57
  46. package/template/page-templates/example-template/pages/product.ts +3 -6
  47. package/template/preview/sections/preview.html +10 -6
  48. package/template/preview/shared/api-routes.ts +236 -103
  49. package/template/preview/shared/logger.ts +9 -0
  50. package/template/preview/shared/preview.ts +117 -76
  51. package/template/preview/shared/utils.ts +68 -43
  52. package/template/preview/ssr-server.ts +1 -1
  53. package/template/reference/sections/about-us/AboutUs.vue +20 -22
  54. package/template/reference/sections/about-us/component/Image.vue +18 -18
  55. package/template/reference/sections/about-us/component/Stats.vue +40 -40
  56. package/template/reference/sections/about-us/component/Title.vue +1 -1
  57. package/template/reference/sections/about-us/settings/content.ts +15 -19
  58. package/template/reference/sections/about-us/settings/design.ts +14 -18
  59. package/template/reference/sections/about-us/settings/layout.ts +7 -5
  60. package/template/reference/sections/about-us/settings/translations.ts +4 -2
  61. package/template/reference/sections/about-us/showcases/1.ts +48 -62
  62. package/template/reference/sections/about-us/showcases/2.ts +44 -56
  63. package/template/reference/sections/about-us/showcases/translations.ts +4 -2
  64. package/template/reference/sections/featured-products/FeaturedProducts.vue +12 -6
  65. package/template/reference/sections/featured-products/component/ProductItem.vue +18 -1
  66. package/template/reference/sections/featured-products/component/ProductPlaceholder.vue +42 -0
  67. package/template/reference/sections/featured-products/component/Title.vue +1 -1
  68. package/template/reference/sections/featured-products/settings/content.ts +8 -10
  69. package/template/reference/sections/featured-products/settings/design.ts +7 -7
  70. package/template/reference/sections/featured-products/settings/translations.ts +4 -2
  71. package/template/reference/sections/featured-products/showcases/1.ts +8 -12
  72. package/template/reference/sections/featured-products/showcases/translations.ts +4 -2
  73. package/template/reference/sections/intro-slider/IntroSlider.vue +6 -6
  74. package/template/reference/sections/intro-slider/component/Slider.vue +42 -43
  75. package/template/reference/sections/intro-slider/component/Title.vue +7 -7
  76. package/template/reference/sections/intro-slider/settings/content.ts +33 -36
  77. package/template/reference/sections/intro-slider/settings/design.ts +17 -22
  78. package/template/reference/sections/intro-slider/settings/layout.ts +6 -4
  79. package/template/reference/sections/intro-slider/settings/translations.ts +4 -2
  80. package/template/reference/sections/intro-slider/showcases/1.ts +52 -75
  81. package/template/reference/sections/intro-slider/showcases/2.ts +50 -72
  82. package/template/reference/sections/intro-slider/showcases/translations.ts +4 -2
  83. package/template/reference/sections/tag-lines/TagLines.vue +41 -47
  84. package/template/reference/sections/tag-lines/component/HighlightedText.vue +1 -1
  85. package/template/reference/sections/tag-lines/component/SectionImage.vue +18 -18
  86. package/template/reference/sections/tag-lines/component/Title.vue +1 -1
  87. package/template/reference/sections/tag-lines/settings/content.ts +47 -47
  88. package/template/reference/sections/tag-lines/settings/design.ts +15 -19
  89. package/template/reference/sections/tag-lines/settings/layout.ts +6 -4
  90. package/template/reference/sections/tag-lines/settings/translations.ts +4 -2
  91. package/template/reference/sections/tag-lines/showcases/1.ts +40 -50
  92. package/template/reference/sections/tag-lines/showcases/2.ts +40 -50
  93. package/template/reference/sections/tag-lines/showcases/translations.ts +4 -2
  94. package/template/reference/sections/trending-categories/TrendingCategories.vue +1 -1
  95. package/template/reference/sections/trending-categories/component/CategoryItem.vue +18 -1
  96. package/template/reference/sections/trending-categories/component/Title.vue +1 -1
  97. package/template/reference/sections/trending-categories/settings/content.ts +8 -10
  98. package/template/reference/sections/trending-categories/settings/design.ts +7 -7
  99. package/template/reference/sections/trending-categories/settings/translations.ts +4 -2
  100. package/template/reference/sections/trending-categories/showcases/1.ts +14 -15
  101. package/template/reference/sections/trending-categories/showcases/translations.ts +4 -2
  102. package/template/reference/shared/components/Button.vue +6 -6
  103. package/template/reference/shared/components/SectionWrapper.vue +5 -5
  104. package/template/reference/shared/components/Tagline.vue +12 -11
  105. package/template/reference/templates/reference-template-apparel/configuration.ts +8 -8
  106. package/template/reference/templates/reference-template-apparel/pages/catalog.ts +3 -6
  107. package/template/reference/templates/reference-template-apparel/pages/category.ts +3 -6
  108. package/template/reference/templates/reference-template-apparel/pages/home.ts +14 -18
  109. package/template/reference/templates/reference-template-apparel/pages/product.ts +3 -6
  110. package/template/reference/templates/reference-template-bike/configuration.ts +9 -9
  111. package/template/reference/templates/reference-template-bike/pages/catalog.ts +3 -6
  112. package/template/reference/templates/reference-template-bike/pages/category.ts +3 -6
  113. package/template/reference/templates/reference-template-bike/pages/home.ts +14 -18
  114. package/template/reference/templates/reference-template-bike/pages/product.ts +3 -6
  115. package/template/sections/example-section/ExampleSection.vue +3 -5
  116. package/template/sections/example-section/component/button/Button.vue +1 -1
  117. package/template/sections/example-section/component/image/Image.vue +43 -43
  118. package/template/sections/example-section/component/image/ImagesGrid.vue +21 -32
  119. package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
  120. package/template/sections/example-section/component/title/Title.vue +1 -1
  121. package/template/sections/example-section/component/toggle/Toggle.vue +4 -4
  122. package/template/sections/example-section/settings/content.ts +25 -34
  123. package/template/sections/example-section/settings/design.ts +23 -19
  124. package/template/sections/example-section/settings/layout.ts +18 -14
  125. package/template/sections/example-section/settings/translations.ts +17 -2
  126. package/template/sections/example-section/showcases/1.ts +55 -79
  127. package/template/sections/example-section/showcases/2.ts +49 -62
  128. package/template/sections/example-section/showcases/3.ts +53 -76
  129. package/template/sections/example-section/showcases/translations.ts +13 -2
  130. package/template/shared/components/LanguageSelector.vue +1 -1
  131. package/template/shared/components/SectionWrapper.vue +5 -5
@@ -2,9 +2,18 @@
2
2
  import { getExternalContentMock } from './mock';
3
3
  import { loadModule, renderServerModule } from './utils';
4
4
 
5
+ export type BlockType = 'sections' | 'headers' | 'footers';
6
+ export const BLOCK_TYPES: BlockType[] = ['sections', 'headers', 'footers'];
7
+
8
+ const BLOCK_TYPE_LABELS: Record<BlockType, string> = {
9
+ sections: 'Section',
10
+ headers: 'Header',
11
+ footers: 'Footer',
12
+ };
13
+
5
14
  let distFolderPath: string | null = null;
6
15
  let currentApp: any = null;
7
- let currentSectionName: string | null = null;
16
+ let currentBlockKey: string | null = null;
8
17
 
9
18
  export function setDistFolderPath(path: string): void {
10
19
  distFolderPath = path;
@@ -17,8 +26,8 @@ export function setDistFolderPath(path: string): void {
17
26
  /**
18
27
  * Loads all required modules for a showcase.
19
28
  */
20
- async function loadShowcaseModules(sectionName: string, showcaseId: string, distFolder: string): Promise<[any, any, any, any, any]> {
21
- const basePath = `${distFolder}/sections/${sectionName}`;
29
+ async function loadShowcaseModules(blockType: BlockType, blockName: string, showcaseId: string, distFolder: string): Promise<[any, any, any, any, any]> {
30
+ const basePath = `${distFolder}/${blockType}/${blockName}`;
22
31
  return Promise.all([
23
32
  loadModule(`${basePath}/js/settings/content.mjs`),
24
33
  loadModule(`${basePath}/js/settings/translations.mjs`),
@@ -230,12 +239,12 @@ export function mergeDesign(
230
239
  return result;
231
240
  }
232
241
 
233
- export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any>): Record<string, any> {
242
+ export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any>, isApiRender: boolean = false): Record<string, any> {
234
243
  const parsedDesign: Record<string, any> = {};
235
244
  const parsedShowcaseDesign: Record<string, any> = {};
236
245
  Object.entries(design || {}).forEach(([key, comp]) => {
237
- // Skip DIVIDER elements - they're UI-only and don't have runtime data
238
- if (comp?.type === 'DIVIDER') {
246
+ // Skip DIVIDER and INFO elements - they're UI-only and don't have runtime data
247
+ if (comp?.type === 'DIVIDER' || comp?.type === 'INFO') {
239
248
  return;
240
249
  }
241
250
  // Skip BACKGROUND - it's handled separately by createBackgroundDesign
@@ -247,8 +256,8 @@ export function designTransformer(design: Record<string, any>, showCaseDesign: R
247
256
  });
248
257
 
249
258
  Object.entries(showCaseDesign || {}).forEach(([key, comp]) => {
250
- // Skip DIVIDER elements - they're UI-only and don't have runtime data
251
- if (comp?.type === 'DIVIDER') {
259
+ // Skip DIVIDER and INFO elements - they're UI-only and don't have runtime data
260
+ if (comp?.type === 'DIVIDER' || comp?.type === 'INFO') {
252
261
  return;
253
262
  }
254
263
  // Skip BACKGROUND - it's handled separately by createBackgroundDesign
@@ -267,15 +276,17 @@ export function designTransformer(design: Record<string, any>, showCaseDesign: R
267
276
  console.error('[Preview] Failed to update hex colors:', error);
268
277
  }
269
278
 
270
- replaceGlobalFont(overridenDesign, 'Roboto');
279
+ if ( !isApiRender ) {
280
+ replaceGlobalFont(overridenDesign, 'Roboto');
281
+ }
271
282
  return overridenDesign;
272
283
  }
273
284
 
274
- function processImage(component: any, sectionName: string, key: string, isApiRender: boolean = false): Record<string, any> {
285
+ function processImage(component: any, blockType: BlockType, blockName: string, key: string, isApiRender: boolean = false): Record<string, any> {
275
286
  try {
276
287
  const assetLocation = isApiRender ?
277
- `http://${distFolderPath}/sections/${sectionName}/assets/` :
278
- `${distFolderPath}/sections/${sectionName}/assets/`;
288
+ `http://${distFolderPath}/${blockType}/${blockName}/assets/` :
289
+ `${distFolderPath}/${blockType}/${blockName}/assets/`;
279
290
 
280
291
  const set = component.defaults?.imageData?.set || component.imageData?.set;
281
292
  const newSet = {
@@ -297,7 +308,7 @@ function processImage(component: any, sectionName: string, key: string, isApiRen
297
308
  }
298
309
  }
299
310
 
300
- export function processComponent(key: string, component: any, translations: any, sectionName?: string,
311
+ export function processComponent(key: string, component: any, translations: any, blockType?: BlockType, blockName?: string,
301
312
  isApiRender: boolean = false, extraParams?: Record<string, any>): any {
302
313
  if (!component?.type) return {};
303
314
  switch (component.type) {
@@ -313,16 +324,16 @@ export function processComponent(key: string, component: any, translations: any,
313
324
  if (defaultSettings) {
314
325
  const result: Record<string, any> = {};
315
326
  Object.entries(defaultSettings).forEach(([k, c]) => {
316
- Object.assign(result, processComponent(k, c, translations, sectionName, isApiRender));
327
+ Object.assign(result, processComponent(k, c, translations, blockType, blockName, isApiRender));
317
328
  });
318
329
  return { [key]: result };
319
330
  } else {
320
331
  const cards = component.cards.map((card: any, index: number) => {
321
332
  const cardContent: Record<string, any> = {};
322
333
  Object.entries(card.settings).forEach(([k, c]) => {
323
- Object.assign(cardContent, processComponent(k, c, translations, sectionName, isApiRender));
334
+ Object.assign(cardContent, processComponent(k, c, translations, blockType, blockName, isApiRender));
324
335
  });
325
- return { settings: cardContent, id: index.toString(), title: extraParams?.deckCardLabel};
336
+ return { settings: cardContent, id: index.toString(), title: extraParams?.deckCardLabel };
326
337
  });
327
338
  return { [key]: { cards } };
328
339
  }
@@ -340,14 +351,14 @@ export function processComponent(key: string, component: any, translations: any,
340
351
  case 'TOGGLE': {
341
352
  return {
342
353
  [key]: {
343
- enabled: component.defaults?.enabled ?? component.enabled
344
- }
354
+ enabled: component.defaults?.enabled ?? component.enabled,
355
+ },
345
356
  };
346
357
  }
347
358
  case 'SELECTBOX':
348
359
  return { [key]: component.defaults?.value ?? component.value };
349
360
  case 'IMAGE':
350
- return processImage(component, sectionName!, key, isApiRender);
361
+ return processImage(component, blockType!, blockName!, key, isApiRender);
351
362
  case 'INFO':
352
363
  return {
353
364
  [key]: {
@@ -359,11 +370,38 @@ export function processComponent(key: string, component: any, translations: any,
359
370
  },
360
371
  },
361
372
  };
362
- // Leaving PRODUCT_SELECTOR and CATEGORY_SELECTOR references for future implementation
363
373
  case 'PRODUCT_SELECTOR':
364
- return {};
374
+ return {
375
+ [key]: {
376
+ maxProducts: component?.maxProducts ?? component?.defaults?.maxProducts,
377
+ products: {
378
+ selectionType: 'MANUAL',
379
+ },
380
+ },
381
+ };
365
382
  case 'CATEGORY_SELECTOR':
366
- return {};
383
+ return {
384
+ [key]: {
385
+ maxCategories: component?.maxCategories ?? component?.defaults?.maxCategories,
386
+ categories: {
387
+ selectionType: 'ROOT',
388
+ },
389
+ },
390
+ };
391
+ case 'NAVIGATION_MENU':
392
+ return {
393
+ [key]: {
394
+ items: component.items ?? component?.defaults?.items ?? [],
395
+ },
396
+ };
397
+ case 'LOGO':
398
+ return {
399
+ [key]: {
400
+ text: translations[component?.text ?? component?.defaults?.text ?? ''],
401
+ type: component.logoType,
402
+ image: component.image,
403
+ },
404
+ };
367
405
  default:
368
406
  return {};
369
407
  }
@@ -374,7 +412,8 @@ function getContentToRender(
374
412
  showcase: any,
375
413
  contentTranslations: any,
376
414
  showcaseTranslations: any,
377
- sectionName: string,
415
+ blockType: BlockType,
416
+ blockName: string,
378
417
  isApiRender: boolean = false,
379
418
  ): any {
380
419
  let deckCardLabel: string | undefined = undefined;
@@ -382,12 +421,11 @@ function getContentToRender(
382
421
  if ((c as any).type === 'DECK') {
383
422
  deckCardLabel = contentTranslations[(c as any).cards?.defaultCardContent?.label];
384
423
  }
385
- return { ...acc, ...processComponent(k, c, contentTranslations, sectionName, isApiRender) };
424
+ return { ...acc, ...processComponent(k, c, contentTranslations, blockType, blockName, isApiRender) };
386
425
  }, {});
387
426
 
388
-
389
427
  const parsedShowcase = Object.entries(showcase || {}).reduce((acc, [k, c]) => {
390
- return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName, isApiRender, {deckCardLabel}) };
428
+ return { ...acc, ...processComponent(k, c, showcaseTranslations, blockType, blockName, isApiRender, { deckCardLabel }) };
391
429
  }, {});
392
430
 
393
431
  return overrideSettingsFromShowcase(parsedContent, parsedShowcase);
@@ -395,44 +433,34 @@ function getContentToRender(
395
433
 
396
434
  export function dropdownOptions(showcaseModules: Record<string, any>): Array<{ value: string; label: string }> {
397
435
  return Object.keys(showcaseModules).map((path) => {
398
- const match = path.match(/\/sections\/([^/]+)\/js\/showcases\/(\d+)\.mjs$/);
436
+ const match = path.match(/\/(sections|headers|footers)\/([^/]+)\/js\/showcases\/(\d+)\.mjs$/);
399
437
  if (!match) return null;
438
+ const blockType = match[1] as BlockType;
439
+ const blockName = match[2];
440
+ const showcaseId = match[3];
400
441
  return {
401
- value: `${match[1]}:${match[2]}`,
402
- label: `${match[1]}: showcase ${match[2]}`,
442
+ value: `${blockType}:${blockName}:${showcaseId}`,
443
+ label: `${BLOCK_TYPE_LABELS[blockType]}: ${blockName} - Showcase ${showcaseId}`,
403
444
  };
404
445
  }).filter(Boolean) as Array<{ value: string; label: string }>;
405
446
  }
406
447
 
407
- export function loadSectionCss(sectionName: string): void {
408
- const cssHref = `${distFolderPath}/sections/${sectionName}/js/main/client/assets/client.css`;
409
-
410
- // Check if CSS for this section is already loaded
411
- const existingLink = document.querySelector(`link[href="${cssHref}"]`);
412
- if (existingLink) {
413
- return; // CSS already loaded
414
- }
415
-
416
- // Remove CSS from other sections (cleanup)
417
- const oldLinks = document.querySelectorAll('link[data-section-css]');
418
- oldLinks.forEach(link => link.remove());
419
-
448
+ export function loadBlockCss(blockType: BlockType, blockName: string): void {
420
449
  const link = document.createElement('link');
421
450
  link.rel = 'stylesheet';
422
- link.href = `${distFolderPath}/sections/${sectionName}/js/main/client/assets/client.css`;
423
- link.setAttribute('data-section-css', sectionName);
451
+ link.href = `${distFolderPath}/${blockType}/${blockName}/js/main/client/assets/client.css`;
424
452
  document.head.appendChild(link);
425
453
  }
426
454
 
427
455
  /**
428
456
  * Renders a showcase in the browser (client-side).
429
457
  */
430
- export async function renderShowcase(sectionName: string, showcaseId: string): Promise<void> {
458
+ export async function renderShowcase(blockType: BlockType, blockName: string, showcaseId: string): Promise<void> {
431
459
  if (!distFolderPath) throw new Error('distFolderPath not set');
432
460
 
433
- const basePath = `${distFolderPath}/sections/${sectionName}`;
461
+ const basePath = `${distFolderPath}/${blockType}/${blockName}`;
434
462
  const [content, contentTranslations, showcaseTranslations, showcase, design]
435
- = await loadShowcaseModules(sectionName, showcaseId, distFolderPath);
463
+ = await loadShowcaseModules(blockType, blockName, showcaseId, distFolderPath);
436
464
 
437
465
  const client = await loadModule(`${basePath}/js/main/client/client.js`) as any;
438
466
 
@@ -448,11 +476,12 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
448
476
  (showcase as any).default.content || {},
449
477
  (contentTranslations as any).default.en,
450
478
  (showcaseTranslations as any).default.en,
451
- sectionName,
479
+ blockType,
480
+ blockName,
452
481
  );
453
482
 
454
483
  // Load CSS
455
- loadSectionCss(sectionName);
484
+ loadBlockCss(blockType, blockName);
456
485
 
457
486
  // Prepare state
458
487
  const state = {
@@ -466,63 +495,63 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
466
495
  },
467
496
  };
468
497
 
469
- // Check if we're switching to a different section
470
- const isSameSection = currentSectionName === sectionName;
498
+ // Check if we're switching to a different block
499
+ const blockKey = `${blockType}:${blockName}`;
500
+ const isSameBlock = currentBlockKey === blockKey;
471
501
 
472
502
  // Mount or update app
473
- if (currentApp && isSameSection) {
474
- // Same section, just update with new showcase data
503
+ if (currentApp && isSameBlock) {
475
504
  currentApp.update(state);
476
505
  } else {
477
- // Different section or first load - unmount old app and mount new one
478
506
  if (currentApp) {
479
507
  currentApp.unmount();
480
- // Clear the app container
481
508
  const appContainer = document.getElementById('app');
482
509
  if (appContainer) {
483
510
  appContainer.innerHTML = '';
484
511
  }
485
512
  }
486
-
487
513
  const { mount, update, unmount } = (client as any).default.init();
488
514
  currentApp = { update, unmount };
489
- currentSectionName = sectionName;
515
+ currentBlockKey = blockKey;
490
516
  mount('#app', state);
491
517
  }
492
518
  }
493
519
 
494
- async function getLayout(showcase: any, sectionName: string, distFolder: string) {
520
+ async function getLayout(showcase: any, blockType: BlockType, blockName: string, distFolder: string) {
495
521
  // First, try to get layoutId from showcase
496
522
  if (showcase.default?.layoutId) {
497
523
  return showcase.default.layoutId;
498
524
  }
499
525
 
500
- // If not present in showcase, read from section's layout.mjs
501
- const basePath = `${distFolder}/sections/${sectionName}`;
502
- const layoutModule = await loadModule(`${basePath}/js/settings/layout.mjs`) as any;
526
+ // If not in showcase, try reading from section's layout.mjs (optional)
527
+ try {
528
+ const basePath = `${distFolder}/${blockType}/${blockName}`;
529
+ const layoutModule = await loadModule(`${basePath}/js/settings/layout.mjs`) as any;
503
530
 
504
- // layout.mjs exports an array of layout configurations
505
- // Return the first element's layoutId
506
- if (Array.isArray(layoutModule.default) && layoutModule.default.length > 0) {
507
- return layoutModule.default[0].layoutId;
531
+ // layout.mjs exports an array of layout configurations
532
+ // Return the first element's layoutId
533
+ if (Array.isArray(layoutModule.default) && layoutModule.default.length > 0) {
534
+ return layoutModule.default[0].layoutId;
535
+ }
536
+ } catch {
537
+ // Layout file is optional
508
538
  }
509
- throw new Error('Layout module is not an array or is empty');
510
539
  }
511
540
 
512
541
  /**
513
542
  * Prepares showcase data (content and design) without rendering.
514
543
  * Used by API routes to return tile data.
515
544
  */
516
- export async function getShowcaseData(sectionName: string, showcaseId: string, distFolder: string) {
545
+ export async function getShowcaseData(blockType: BlockType, blockName: string, showcaseId: string, distFolder: string) {
517
546
  const [content, contentTranslations, showcaseTranslations, showcase, design]
518
- = await loadShowcaseModules(sectionName, showcaseId, distFolder);
547
+ = await loadShowcaseModules(blockType, blockName, showcaseId, distFolder);
519
548
 
520
549
  // Prepare design
521
550
  const showcaseBackground = (showcase as any).default?.design?.background;
522
551
  const backgroundDesign = createBackgroundDesign(showcaseBackground, true); // API render
523
- const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {});
552
+ const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {}, true);
524
553
  overriddenDesign.background = backgroundDesign;
525
- overriddenDesign.layout = await getLayout(showcase, sectionName, distFolder);
554
+ overriddenDesign.layout = await getLayout(showcase, blockType, blockName, distFolder);
526
555
 
527
556
  // Prepare content
528
557
  const overriddenContent = getContentToRender(
@@ -530,13 +559,19 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
530
559
  (showcase as any).default.content || {},
531
560
  (contentTranslations as any).default.en,
532
561
  (showcaseTranslations as any).default.en,
533
- sectionName,
534
- true
562
+ blockType,
563
+ blockName,
564
+ true,
535
565
  );
536
566
 
567
+ // Resolve tileName from showcase blockName translation
568
+ const blockNameKey = (showcase as any).default?.blockName;
569
+ const tileName = blockNameKey ? (showcaseTranslations as any).default.en[blockNameKey] : undefined;
570
+
537
571
  return {
538
572
  content: overriddenContent,
539
573
  design: overriddenDesign,
574
+ tileName,
540
575
  };
541
576
  }
542
577
 
@@ -544,20 +579,26 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
544
579
  * Gets the showcase state by rendering server-side.
545
580
  * Uses renderServerModule to get HTML directly from the SSR server.
546
581
  */
547
- export async function getShowcaseState(sectionName: string, showcaseId: string, distFolder: string, context: Record<string, any>) {
582
+ export async function getShowcaseState(
583
+ blockType: BlockType,
584
+ blockName: string,
585
+ showcaseId: string,
586
+ distFolder: string,
587
+ context: Record<string, any>,
588
+ externalContent: Record<string, any>) {
548
589
  const { content: overriddenContent, design: overriddenDesign }
549
- = await getShowcaseData(sectionName, showcaseId, distFolder);
590
+ = await getShowcaseData(blockType, blockName, showcaseId, distFolder);
550
591
 
551
592
  const data = {
552
593
  content: overriddenContent,
553
594
  design: overriddenDesign,
554
595
  defaults: {},
555
596
  background: {},
556
- externalContent: getExternalContentMock(),
597
+ externalContent: externalContent,
557
598
  };
558
599
 
559
600
  // Render server-side using the SSR server
560
- const basePath = `${distFolder}/sections/${sectionName}`;
601
+ const basePath = `${distFolder}/${blockType}/${blockName}`;
561
602
  const serverModulePath = `${basePath}/js/main/server/server.js`;
562
603
  const html = await renderServerModule(serverModulePath, context, data);
563
604
 
@@ -2,7 +2,7 @@
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
 
5
- import { getShowcaseState } from './preview';
5
+ import { getShowcaseState, BlockType, BLOCK_TYPES } from './preview';
6
6
 
7
7
  /**
8
8
  * Renders a server.js module by directly calling the SSR server.
@@ -96,6 +96,7 @@ export async function fetchTiles(authToken?: string, tilesUrl?: string): Promise
96
96
  * @param showcaseId - The showcase ID to match
97
97
  * @param content - The new content to replace
98
98
  * @param design - The new design to replace
99
+ * @param tileName - The new tile name to replace
99
100
  * @returns The updated tiles response object
100
101
  */
101
102
  export function updateTilesSection(
@@ -104,24 +105,26 @@ export function updateTilesSection(
104
105
  showcaseId: string,
105
106
  content: any,
106
107
  design: any,
108
+ tileName?: string,
107
109
  ): any {
108
110
  const targetPattern = `_${sectionName}_${showcaseId}`;
109
111
 
110
- const targetItem = tilesResponse.tiles.find((item: any) => {
112
+ const updatedTiles = tilesResponse.tiles.map((item: any) => {
111
113
  const itemId = item.sourceId;
112
- if (itemId && typeof itemId === 'string') {
113
- return itemId.includes(targetPattern);
114
+ if (itemId && typeof itemId === 'string' && itemId.includes(targetPattern)) {
115
+ item.content = content;
116
+ // If new design has no layout, use CUSTOM_LAYOUT
117
+ const layout = design.layout || 'CUSTOM_LAYOUT';
118
+ item.design = design;
119
+ item.design.layout = layout;
120
+ if (tileName) {
121
+ item.tileName = tileName;
122
+ }
114
123
  }
115
- return false;
124
+ return item;
116
125
  });
117
126
 
118
- if (targetItem) {
119
- // Replace only content and design, preserve all other properties including tils
120
- targetItem.content = content;
121
- targetItem.design = design;
122
- }
123
-
124
- return tilesResponse;
127
+ return { ...tilesResponse, tiles: updatedTiles };
125
128
  }
126
129
 
127
130
  /**
@@ -154,17 +157,18 @@ export async function getSectionIds(sectionName: string, showcaseId: string, aut
154
157
  }
155
158
 
156
159
  /**
157
- * Gets all showcase IDs for a given section
158
- * @param sectionName - The section name (e.g., 'example-section')
160
+ * Gets all showcase IDs for a given block
161
+ * @param blockType - The block type ('sections', 'headers', 'footers')
162
+ * @param blockName - The block name (e.g., 'example-section')
159
163
  * @param distPath - The dist path
160
164
  * @returns Array of showcase IDs
161
165
  */
162
- export function getShowcaseIds(sectionName: string, distPath: string): string[] {
166
+ export function getShowcaseIds(blockType: BlockType, blockName: string, distPath: string): string[] {
163
167
  try {
164
- const showcasesPath = path.join(distPath, 'sections', sectionName, 'js', 'showcases');
168
+ const showcasesPath = path.join(distPath, blockType, blockName, 'js', 'showcases');
165
169
 
166
170
  if (!fs.existsSync(showcasesPath)) {
167
- console.error(`Showcases directory not found for section: ${sectionName}`);
171
+ console.error(`Showcases directory not found for ${blockType}: ${blockName}`);
168
172
  return [];
169
173
  }
170
174
 
@@ -174,23 +178,38 @@ export function getShowcaseIds(sectionName: string, distPath: string): string[]
174
178
 
175
179
  return showcaseIds;
176
180
  } catch (error) {
177
- console.error(`Error getting showcase IDs for section ${sectionName}:`, error);
181
+ console.error(`Error getting showcase IDs for ${blockType} ${blockName}:`, error);
178
182
  return [];
179
183
  }
180
184
  }
181
185
 
182
186
  /**
183
- * Updates all matching section showcases in custom content
184
- * @param sectionName - The section name
187
+ * Determine the block type by checking which folder contains the block
188
+ */
189
+ export function getBlockType(blockName: string, distPath: string): BlockType | null {
190
+ for (const blockType of BLOCK_TYPES) {
191
+ const blockPath = path.join(distPath, blockType, blockName);
192
+ if (fs.existsSync(blockPath)) {
193
+ return blockType;
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+
199
+ /**
200
+ * Updates all matching block showcases in custom content
201
+ * @param blockType - The block type ('sections', 'headers', 'footers')
202
+ * @param blockName - The block name
185
203
  * @param showcaseId - The showcase ID
186
204
  * @param distPath - The dist path
187
205
  * @param customContentSections - The sections array from custom content
188
206
  * @param authToken - Bearer token
189
207
  * @param tilesUrl - The tiles URL
190
- * @returns True if at least one section was updated successfully, false otherwise
208
+ * @returns True if at least one block was updated successfully, false otherwise
191
209
  */
192
- async function updateSectionShowcase(
193
- sectionName: string,
210
+ async function updateBlockShowcase(
211
+ blockType: BlockType,
212
+ blockName: string,
194
213
  showcaseId: string,
195
214
  distPath: string,
196
215
  customContentSections: Array<Record<string, unknown>>,
@@ -198,61 +217,62 @@ async function updateSectionShowcase(
198
217
  tilesUrl?: string,
199
218
  ): Promise<boolean> {
200
219
  try {
201
- // Get all section IDs from tiles
202
- const sectionIds = await getSectionIds(sectionName, showcaseId, authToken, tilesUrl);
220
+ // Get all section IDs from tiles matching this block/showcase
221
+ const sectionIds = await getSectionIds(blockName, showcaseId, authToken, tilesUrl);
203
222
 
204
223
  if (!sectionIds || sectionIds.length === 0) {
205
224
  return false;
206
- }
207
- // Process each section ID
225
+ }// Process each section ID
208
226
  for (const sectionId of sectionIds) {
209
- // Find the section by ID in custom content
210
- const targetSection = customContentSections.find((section) => {
227
+ // Find the block by block ID in custom content
228
+ const targetBlock = customContentSections.find((section) => {
211
229
  const state = section.state as Record<string, unknown>;
212
230
  const data = state.data as Record<string, unknown>;
213
231
  return data.tileId === sectionId;
214
232
  });
215
233
 
216
- if (!targetSection) {
217
- console.error(`Section not found in custom content with ID: ${sectionId}`);
234
+ if (!targetBlock) {
235
+ console.error(`Block not found in custom content with ID: ${sectionId}`);
218
236
  continue;
219
237
  }
220
238
 
221
239
  // Fetch content, design, and html using getShowcaseState
222
- const state = targetSection.state as Record<string, unknown>;
240
+ const state = targetBlock.state as Record<string, unknown>;
223
241
  // eslint-disable-next-line no-await-in-loop
224
242
  const { content, design, html } = await getShowcaseState(
225
- sectionName,
243
+ blockType,
244
+ blockName,
226
245
  showcaseId,
227
246
  distPath,
228
247
  state.context as Record<string, unknown>,
248
+ (state.data as Record<string, unknown>).externalContent as Record<string, unknown>,
229
249
  );
230
250
 
231
251
  // Update content, design, and html
232
252
  const data = state.data as Record<string, unknown>;
233
253
  data.content = JSON.stringify(content);
234
254
  data.design = JSON.stringify(design);
235
- targetSection.html = html;
255
+ targetBlock.html = html;
236
256
  }
237
257
 
238
258
  return true;
239
259
  } catch (error) {
240
- console.error(`Error updating ${sectionName} showcase ${showcaseId}:`, error);
260
+ console.error(`Error updating ${blockType} ${blockName} showcase ${showcaseId}:`, error);
241
261
  return false;
242
262
  }
243
263
  }
244
264
 
245
265
  /**
246
266
  * Fetches custom content and updates all specified sections with their showcases
247
- * @param sectionNames - Array of section names to update (e.g., ['example-section', 'test-section'])
267
+ * @param blockNames - Array of block names to update (e.g., ['about-us', 'example-header', 'example-footer'])
248
268
  * @param distPath - The dist path for getContentAndDesign
249
269
  * @param authToken - Bearer token from request header (e.g., "Bearer xxx")
250
270
  * @param customContentUrl - The custom content URL
251
- * @param tilesUrl - The tiles URL to fetch section IDs
271
+ * @param tilesUrl - The tiles URL to fetch block IDs
252
272
  * @returns The updated custom content response or null if failed
253
273
  */
254
274
  export async function updateCustomContent(
255
- sectionNames: string[],
275
+ blockNames: string[],
256
276
  distPath: string,
257
277
  authToken?: string,
258
278
  customContentUrl?: string,
@@ -290,13 +310,18 @@ export async function updateCustomContent(
290
310
  }
291
311
 
292
312
  const customContentSections = customContent.sections as Array<Record<string, unknown>>;
313
+ // Process all blocks and showcases in parallel
314
+ const updatePromises = blockNames.flatMap((sectionName) => {
315
+ const blockType = getBlockType(sectionName, distPath);
316
+ if (!blockType) {
317
+ console.warn(`Block not found: ${sectionName}`);
318
+ return [];
319
+ }
293
320
 
294
- // Process all sections and showcases in parallel
295
- const updatePromises = sectionNames.flatMap((sectionName) => {
296
- const showcaseIds = getShowcaseIds(sectionName, distPath);
297
-
321
+ const showcaseIds = getShowcaseIds(blockType, sectionName, distPath);
298
322
  return showcaseIds.map(showcaseId =>
299
- updateSectionShowcase(
323
+ updateBlockShowcase(
324
+ blockType,
300
325
  sectionName,
301
326
  showcaseId,
302
327
  distPath,
@@ -391,7 +391,7 @@ const server = http.createServer(async (req: http.IncomingMessage, res: http.Ser
391
391
  const { render } = serverModule.init();
392
392
 
393
393
  // Call render with context and data
394
- const result = await render(context, data || { defaults: {}, background: {}, externalContent: {} });
394
+ const result = await render(context, data || { defaults: {}, background: {} });
395
395
 
396
396
  res.statusCode = 200;
397
397
  res.setHeader('Content-Type', 'application/json');