@lightspeed/crane 2.0.5 → 3.0.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 +34 -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 +235 -102
  49. package/template/preview/shared/logger.ts +9 -0
  50. package/template/preview/shared/preview.ts +108 -72
  51. package/template/preview/shared/utils.ts +63 -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 +15 -19
  124. package/template/sections/example-section/settings/layout.ts +15 -14
  125. package/template/sections/example-section/settings/translations.ts +4 -2
  126. package/template/sections/example-section/showcases/1.ts +52 -79
  127. package/template/sections/example-section/showcases/2.ts +46 -62
  128. package/template/sections/example-section/showcases/3.ts +50 -76
  129. package/template/sections/example-section/showcases/translations.ts +4 -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,7 +239,7 @@ 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]) => {
@@ -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,8 +559,9 @@ 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
 
537
567
  return {
@@ -544,20 +574,26 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
544
574
  * Gets the showcase state by rendering server-side.
545
575
  * Uses renderServerModule to get HTML directly from the SSR server.
546
576
  */
547
- export async function getShowcaseState(sectionName: string, showcaseId: string, distFolder: string, context: Record<string, any>) {
577
+ export async function getShowcaseState(
578
+ blockType: BlockType,
579
+ blockName: string,
580
+ showcaseId: string,
581
+ distFolder: string,
582
+ context: Record<string, any>,
583
+ externalContent: Record<string, any>) {
548
584
  const { content: overriddenContent, design: overriddenDesign }
549
- = await getShowcaseData(sectionName, showcaseId, distFolder);
585
+ = await getShowcaseData(blockType, blockName, showcaseId, distFolder);
550
586
 
551
587
  const data = {
552
588
  content: overriddenContent,
553
589
  design: overriddenDesign,
554
590
  defaults: {},
555
591
  background: {},
556
- externalContent: getExternalContentMock(),
592
+ externalContent: externalContent,
557
593
  };
558
594
 
559
595
  // Render server-side using the SSR server
560
- const basePath = `${distFolder}/sections/${sectionName}`;
596
+ const basePath = `${distFolder}/${blockType}/${blockName}`;
561
597
  const serverModulePath = `${basePath}/js/main/server/server.js`;
562
598
  const html = await renderServerModule(serverModulePath, context, data);
563
599
 
@@ -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.
@@ -107,21 +107,19 @@ export function updateTilesSection(
107
107
  ): any {
108
108
  const targetPattern = `_${sectionName}_${showcaseId}`;
109
109
 
110
- const targetItem = tilesResponse.tiles.find((item: any) => {
110
+ const updatedTiles = tilesResponse.tiles.map((item: any) => {
111
111
  const itemId = item.sourceId;
112
- if (itemId && typeof itemId === 'string') {
113
- return itemId.includes(targetPattern);
112
+ if (itemId && typeof itemId === 'string' && itemId.includes(targetPattern)) {
113
+ item.content = content;
114
+ // If new design has no layout, use CUSTOM_LAYOUT
115
+ const layout = design.layout || 'CUSTOM_LAYOUT';
116
+ item.design = design;
117
+ item.design.layout = layout;
114
118
  }
115
- return false;
119
+ return item;
116
120
  });
117
121
 
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;
122
+ return { ...tilesResponse, tiles: updatedTiles };
125
123
  }
126
124
 
127
125
  /**
@@ -154,17 +152,18 @@ export async function getSectionIds(sectionName: string, showcaseId: string, aut
154
152
  }
155
153
 
156
154
  /**
157
- * Gets all showcase IDs for a given section
158
- * @param sectionName - The section name (e.g., 'example-section')
155
+ * Gets all showcase IDs for a given block
156
+ * @param blockType - The block type ('sections', 'headers', 'footers')
157
+ * @param blockName - The block name (e.g., 'example-section')
159
158
  * @param distPath - The dist path
160
159
  * @returns Array of showcase IDs
161
160
  */
162
- export function getShowcaseIds(sectionName: string, distPath: string): string[] {
161
+ export function getShowcaseIds(blockType: BlockType, blockName: string, distPath: string): string[] {
163
162
  try {
164
- const showcasesPath = path.join(distPath, 'sections', sectionName, 'js', 'showcases');
163
+ const showcasesPath = path.join(distPath, blockType, blockName, 'js', 'showcases');
165
164
 
166
165
  if (!fs.existsSync(showcasesPath)) {
167
- console.error(`Showcases directory not found for section: ${sectionName}`);
166
+ console.error(`Showcases directory not found for ${blockType}: ${blockName}`);
168
167
  return [];
169
168
  }
170
169
 
@@ -174,23 +173,38 @@ export function getShowcaseIds(sectionName: string, distPath: string): string[]
174
173
 
175
174
  return showcaseIds;
176
175
  } catch (error) {
177
- console.error(`Error getting showcase IDs for section ${sectionName}:`, error);
176
+ console.error(`Error getting showcase IDs for ${blockType} ${blockName}:`, error);
178
177
  return [];
179
178
  }
180
179
  }
181
180
 
182
181
  /**
183
- * Updates all matching section showcases in custom content
184
- * @param sectionName - The section name
182
+ * Determine the block type by checking which folder contains the block
183
+ */
184
+ export function getBlockType(blockName: string, distPath: string): BlockType | null {
185
+ for (const blockType of BLOCK_TYPES) {
186
+ const blockPath = path.join(distPath, blockType, blockName);
187
+ if (fs.existsSync(blockPath)) {
188
+ return blockType;
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+
194
+ /**
195
+ * Updates all matching block showcases in custom content
196
+ * @param blockType - The block type ('sections', 'headers', 'footers')
197
+ * @param blockName - The block name
185
198
  * @param showcaseId - The showcase ID
186
199
  * @param distPath - The dist path
187
200
  * @param customContentSections - The sections array from custom content
188
201
  * @param authToken - Bearer token
189
202
  * @param tilesUrl - The tiles URL
190
- * @returns True if at least one section was updated successfully, false otherwise
203
+ * @returns True if at least one block was updated successfully, false otherwise
191
204
  */
192
- async function updateSectionShowcase(
193
- sectionName: string,
205
+ async function updateBlockShowcase(
206
+ blockType: BlockType,
207
+ blockName: string,
194
208
  showcaseId: string,
195
209
  distPath: string,
196
210
  customContentSections: Array<Record<string, unknown>>,
@@ -198,61 +212,62 @@ async function updateSectionShowcase(
198
212
  tilesUrl?: string,
199
213
  ): Promise<boolean> {
200
214
  try {
201
- // Get all section IDs from tiles
202
- const sectionIds = await getSectionIds(sectionName, showcaseId, authToken, tilesUrl);
215
+ // Get all section IDs from tiles matching this block/showcase
216
+ const sectionIds = await getSectionIds(blockName, showcaseId, authToken, tilesUrl);
203
217
 
204
218
  if (!sectionIds || sectionIds.length === 0) {
205
219
  return false;
206
- }
207
- // Process each section ID
220
+ }// Process each section ID
208
221
  for (const sectionId of sectionIds) {
209
- // Find the section by ID in custom content
210
- const targetSection = customContentSections.find((section) => {
222
+ // Find the block by block ID in custom content
223
+ const targetBlock = customContentSections.find((section) => {
211
224
  const state = section.state as Record<string, unknown>;
212
225
  const data = state.data as Record<string, unknown>;
213
226
  return data.tileId === sectionId;
214
227
  });
215
228
 
216
- if (!targetSection) {
217
- console.error(`Section not found in custom content with ID: ${sectionId}`);
229
+ if (!targetBlock) {
230
+ console.error(`Block not found in custom content with ID: ${sectionId}`);
218
231
  continue;
219
232
  }
220
233
 
221
234
  // Fetch content, design, and html using getShowcaseState
222
- const state = targetSection.state as Record<string, unknown>;
235
+ const state = targetBlock.state as Record<string, unknown>;
223
236
  // eslint-disable-next-line no-await-in-loop
224
237
  const { content, design, html } = await getShowcaseState(
225
- sectionName,
238
+ blockType,
239
+ blockName,
226
240
  showcaseId,
227
241
  distPath,
228
242
  state.context as Record<string, unknown>,
243
+ (state.data as Record<string, unknown>).externalContent as Record<string, unknown>,
229
244
  );
230
245
 
231
246
  // Update content, design, and html
232
247
  const data = state.data as Record<string, unknown>;
233
248
  data.content = JSON.stringify(content);
234
249
  data.design = JSON.stringify(design);
235
- targetSection.html = html;
250
+ targetBlock.html = html;
236
251
  }
237
252
 
238
253
  return true;
239
254
  } catch (error) {
240
- console.error(`Error updating ${sectionName} showcase ${showcaseId}:`, error);
255
+ console.error(`Error updating ${blockType} ${blockName} showcase ${showcaseId}:`, error);
241
256
  return false;
242
257
  }
243
258
  }
244
259
 
245
260
  /**
246
261
  * 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'])
262
+ * @param blockNames - Array of block names to update (e.g., ['about-us', 'example-header', 'example-footer'])
248
263
  * @param distPath - The dist path for getContentAndDesign
249
264
  * @param authToken - Bearer token from request header (e.g., "Bearer xxx")
250
265
  * @param customContentUrl - The custom content URL
251
- * @param tilesUrl - The tiles URL to fetch section IDs
266
+ * @param tilesUrl - The tiles URL to fetch block IDs
252
267
  * @returns The updated custom content response or null if failed
253
268
  */
254
269
  export async function updateCustomContent(
255
- sectionNames: string[],
270
+ blockNames: string[],
256
271
  distPath: string,
257
272
  authToken?: string,
258
273
  customContentUrl?: string,
@@ -290,13 +305,18 @@ export async function updateCustomContent(
290
305
  }
291
306
 
292
307
  const customContentSections = customContent.sections as Array<Record<string, unknown>>;
308
+ // Process all blocks and showcases in parallel
309
+ const updatePromises = blockNames.flatMap((sectionName) => {
310
+ const blockType = getBlockType(sectionName, distPath);
311
+ if (!blockType) {
312
+ console.warn(`Block not found: ${sectionName}`);
313
+ return [];
314
+ }
293
315
 
294
- // Process all sections and showcases in parallel
295
- const updatePromises = sectionNames.flatMap((sectionName) => {
296
- const showcaseIds = getShowcaseIds(sectionName, distPath);
297
-
316
+ const showcaseIds = getShowcaseIds(blockType, sectionName, distPath);
298
317
  return showcaseIds.map(showcaseId =>
299
- updateSectionShowcase(
318
+ updateBlockShowcase(
319
+ blockType,
300
320
  sectionName,
301
321
  showcaseId,
302
322
  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');
@@ -43,31 +43,29 @@ const backgroundStyle = computed<CSSProperties>(() => {
43
43
  });
44
44
  </script>
45
45
 
46
- <style lang="scss" scoped>
47
- .about-us-section {
48
- &__wrapper {
49
- display: grid;
50
- grid-template-columns: 1fr;
51
- gap: 75px;
46
+ <style scoped>
47
+ .about-us-section__wrapper {
48
+ display: grid;
49
+ grid-template-columns: 1fr;
50
+ gap: 75px;
52
51
 
53
- @media screen and (min-width: 900px) {
54
- grid-template-columns: 1fr 1fr;
55
- align-items: center;
56
- gap: 100px;
57
- }
52
+ @media screen and (min-width: 900px) {
53
+ grid-template-columns: 1fr 1fr;
54
+ align-items: center;
55
+ gap: 100px;
58
56
  }
57
+ }
59
58
 
60
- &__image {
61
- max-width: 479px;
62
- max-height: 610px;
63
- margin: 0 auto;
64
- }
59
+ .about-us-section__image {
60
+ max-width: 479px;
61
+ max-height: 610px;
62
+ margin: 0 auto;
63
+ }
65
64
 
66
- &__content {
67
- flex: 1;
68
- display: flex;
69
- flex-direction: column;
70
- align-self: center;
71
- }
65
+ .about-us-section__content {
66
+ flex: 1;
67
+ display: flex;
68
+ flex-direction: column;
69
+ align-self: center;
72
70
  }
73
71
  </style>