meno-core 1.0.53 → 1.0.54

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 (387) hide show
  1. package/.claude/settings.local.json +1 -3
  2. package/bin/cli.ts +48 -57
  3. package/build-astro.ts +131 -113
  4. package/build-next.ts +109 -96
  5. package/build-static.test.ts +39 -10
  6. package/build-static.ts +120 -122
  7. package/dist/bin/cli.js +34 -38
  8. package/dist/bin/cli.js.map +2 -2
  9. package/dist/build-static.js +12 -11
  10. package/dist/chunks/chunk-2AR55GYH.js +42 -0
  11. package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
  12. package/dist/chunks/{chunk-IGYR22T6.js → chunk-2FN4UOVO.js} +512 -239
  13. package/dist/chunks/chunk-2FN4UOVO.js.map +7 -0
  14. package/dist/chunks/chunk-3XER4E5W.js +168 -0
  15. package/dist/chunks/chunk-3XER4E5W.js.map +7 -0
  16. package/dist/chunks/{chunk-X754AHS5.js → chunk-5ETZFREW.js} +9 -12
  17. package/dist/chunks/chunk-5ETZFREW.js.map +7 -0
  18. package/dist/chunks/{chunk-2MHDV5BF.js → chunk-7E4IF5L7.js} +15 -21
  19. package/dist/chunks/chunk-7E4IF5L7.js.map +7 -0
  20. package/dist/chunks/{chunk-JGWFTO6P.js → chunk-7HWQUVTU.js} +1578 -1349
  21. package/dist/chunks/chunk-7HWQUVTU.js.map +7 -0
  22. package/dist/chunks/{chunk-O3NAGJP4.js → chunk-AE3QK5QW.js} +110 -21
  23. package/dist/chunks/chunk-AE3QK5QW.js.map +7 -0
  24. package/dist/chunks/{chunk-GZHGVVW3.js → chunk-F6KTJYGV.js} +7 -7
  25. package/dist/chunks/chunk-F6KTJYGV.js.map +7 -0
  26. package/dist/chunks/{chunk-WQFG7PAH.js → chunk-FZITJSSS.js} +2 -6
  27. package/dist/chunks/chunk-FZITJSSS.js.map +7 -0
  28. package/dist/chunks/{chunk-QB2LNO4W.js → chunk-GSYYA5GX.js} +2 -2
  29. package/dist/chunks/chunk-GSYYA5GX.js.map +7 -0
  30. package/dist/chunks/{chunk-YBLHKYFF.js → chunk-HIZMY3EP.js} +1 -1
  31. package/dist/chunks/chunk-HIZMY3EP.js.map +7 -0
  32. package/dist/chunks/{chunk-AZQYF6KE.js → chunk-I2WEGYA7.js} +41 -176
  33. package/dist/chunks/chunk-I2WEGYA7.js.map +7 -0
  34. package/dist/chunks/{chunk-I7YIGZXT.js → chunk-JNO3CNLJ.js} +6 -9
  35. package/dist/chunks/chunk-JNO3CNLJ.js.map +7 -0
  36. package/dist/chunks/{chunk-UB44F4Z2.js → chunk-NVRBTSQG.js} +2 -4
  37. package/dist/chunks/chunk-NVRBTSQG.js.map +7 -0
  38. package/dist/chunks/{chunk-JGP5A3Y5.js → chunk-Q4OBWKXG.js} +40 -33
  39. package/dist/chunks/chunk-Q4OBWKXG.js.map +7 -0
  40. package/dist/chunks/{chunk-R6XHAFBF.js → chunk-QTE32Y53.js} +250 -242
  41. package/dist/chunks/chunk-QTE32Y53.js.map +7 -0
  42. package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-STDY3OVM.js} +214 -85
  43. package/dist/chunks/chunk-STDY3OVM.js.map +7 -0
  44. package/dist/chunks/configService-PRJZF7Y6.js +14 -0
  45. package/dist/chunks/{constants-STK2YBIW.js → constants-KIQEYMAM.js} +2 -2
  46. package/dist/chunks/{fs-JGINUXGL.js → fs-ZI5JEU7V.js} +2 -2
  47. package/dist/entries/server-router.js +14 -19
  48. package/dist/entries/server-router.js.map +2 -2
  49. package/dist/lib/client/index.js +638 -332
  50. package/dist/lib/client/index.js.map +2 -2
  51. package/dist/lib/server/index.js +177 -235
  52. package/dist/lib/server/index.js.map +2 -2
  53. package/dist/lib/shared/index.js +80 -44
  54. package/dist/lib/shared/index.js.map +2 -2
  55. package/dist/lib/shared/richtext/index.js +1 -1
  56. package/dist/lib/test-utils/index.js +38 -60
  57. package/dist/lib/test-utils/index.js.map +2 -2
  58. package/entries/client-router.tsx +10 -8
  59. package/entries/server-router.tsx +1 -7
  60. package/lib/client/ClientInitializer.ts +8 -8
  61. package/lib/client/ErrorBoundary.test.tsx +146 -143
  62. package/lib/client/ErrorBoundary.tsx +175 -127
  63. package/lib/client/componentRegistry.test.ts +96 -108
  64. package/lib/client/componentRegistry.ts +1 -2
  65. package/lib/client/contexts/ThemeContext.tsx +3 -2
  66. package/lib/client/core/ComponentBuilder.test.ts +513 -560
  67. package/lib/client/core/ComponentBuilder.ts +318 -146
  68. package/lib/client/core/ComponentRenderer.test.tsx +1 -2
  69. package/lib/client/core/ComponentRenderer.tsx +46 -33
  70. package/lib/client/core/builders/embedBuilder.ts +241 -53
  71. package/lib/client/core/builders/linkBuilder.ts +71 -44
  72. package/lib/client/core/builders/linkNodeBuilder.ts +78 -53
  73. package/lib/client/core/builders/listBuilder.ts +115 -86
  74. package/lib/client/core/builders/localeListBuilder.ts +95 -60
  75. package/lib/client/core/builders/types.ts +5 -5
  76. package/lib/client/core/cmsTemplateProcessor.ts +7 -7
  77. package/lib/client/elementRegistry.ts +3 -3
  78. package/lib/client/fontFamiliesService.test.ts +2 -10
  79. package/lib/client/fontFamiliesService.ts +3 -3
  80. package/lib/client/hmr/HMRManager.tsx +8 -0
  81. package/lib/client/hmrCssReload.ts +14 -8
  82. package/lib/client/hmrWebSocket.ts +9 -14
  83. package/lib/client/hooks/useColorVariables.test.ts +21 -21
  84. package/lib/client/hooks/useColorVariables.ts +12 -10
  85. package/lib/client/hooks/usePropertyAutocomplete.ts +3 -5
  86. package/lib/client/hooks/useVariables.ts +4 -4
  87. package/lib/client/hydration/HydrationUtils.test.ts +24 -25
  88. package/lib/client/hydration/HydrationUtils.ts +3 -4
  89. package/lib/client/i18nConfigService.test.ts +2 -7
  90. package/lib/client/i18nConfigService.ts +2 -2
  91. package/lib/client/meno-filter/MenoFilter.test.ts +19 -21
  92. package/lib/client/meno-filter/MenoFilter.ts +5 -9
  93. package/lib/client/meno-filter/bindings.ts +15 -40
  94. package/lib/client/meno-filter/init.ts +1 -1
  95. package/lib/client/meno-filter/renderer.ts +23 -29
  96. package/lib/client/meno-filter/script.generated.ts +1 -3
  97. package/lib/client/meno-filter/ui.ts +3 -5
  98. package/lib/client/meno-filter/updates.ts +15 -21
  99. package/lib/client/navigation.test.ts +159 -159
  100. package/lib/client/navigation.ts +0 -1
  101. package/lib/client/responsiveStyleResolver.test.ts +230 -228
  102. package/lib/client/responsiveStyleResolver.ts +13 -16
  103. package/lib/client/routing/RouteLoader.test.ts +23 -24
  104. package/lib/client/routing/RouteLoader.ts +22 -35
  105. package/lib/client/routing/Router.tsx +31 -3
  106. package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
  107. package/lib/client/scripts/ScriptExecutor.ts +15 -33
  108. package/lib/client/services/PrefetchService.test.ts +2 -2
  109. package/lib/client/services/PrefetchService.ts +10 -24
  110. package/lib/client/styleProcessor.test.ts +9 -9
  111. package/lib/client/styleProcessor.ts +18 -15
  112. package/lib/client/styles/StyleInjector.test.ts +122 -115
  113. package/lib/client/styles/StyleInjector.ts +9 -6
  114. package/lib/client/styles/UtilityClassCollector.ts +19 -26
  115. package/lib/client/styles/cspNonce.test.ts +2 -5
  116. package/lib/client/templateEngine.test.ts +554 -482
  117. package/lib/client/templateEngine.ts +200 -64
  118. package/lib/client/theme.ts +0 -1
  119. package/lib/client/utils/toast.ts +0 -1
  120. package/lib/server/__integration__/api-routes.test.ts +8 -4
  121. package/lib/server/__integration__/cms-integration.test.ts +1 -4
  122. package/lib/server/__integration__/server-lifecycle.test.ts +2 -5
  123. package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
  124. package/lib/server/__integration__/static-assets.test.ts +1 -1
  125. package/lib/server/__integration__/test-helpers.ts +84 -70
  126. package/lib/server/ab/generateFunctions.ts +12 -10
  127. package/lib/server/astro/cmsPageEmitter.ts +23 -38
  128. package/lib/server/astro/componentEmitter.ts +25 -36
  129. package/lib/server/astro/cssCollector.ts +10 -26
  130. package/lib/server/astro/nodeToAstro.test.ts +1750 -30
  131. package/lib/server/astro/nodeToAstro.ts +198 -187
  132. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +105 -109
  133. package/lib/server/astro/normalizeOrphanTemplateProps.ts +4 -12
  134. package/lib/server/astro/pageEmitter.ts +9 -13
  135. package/lib/server/astro/tailwindMapper.test.ts +10 -37
  136. package/lib/server/astro/tailwindMapper.ts +33 -40
  137. package/lib/server/astro/templateTransformer.ts +14 -17
  138. package/lib/server/createServer.ts +5 -17
  139. package/lib/server/cssGenerator.test.ts +35 -44
  140. package/lib/server/cssGenerator.ts +6 -17
  141. package/lib/server/fileWatcher.test.ts +124 -10
  142. package/lib/server/fileWatcher.ts +124 -118
  143. package/lib/server/index.ts +7 -1
  144. package/lib/server/jsonLoader.test.ts +39 -2
  145. package/lib/server/jsonLoader.ts +33 -31
  146. package/lib/server/middleware/cors.test.ts +20 -20
  147. package/lib/server/middleware/cors.ts +28 -4
  148. package/lib/server/middleware/errorHandler.test.ts +5 -3
  149. package/lib/server/middleware/errorHandler.ts +3 -8
  150. package/lib/server/middleware/index.ts +0 -1
  151. package/lib/server/middleware/logger.test.ts +7 -5
  152. package/lib/server/middleware/logger.ts +10 -22
  153. package/lib/server/pageCache.test.ts +76 -77
  154. package/lib/server/pageCache.ts +0 -1
  155. package/lib/server/projectContext.ts +4 -3
  156. package/lib/server/providers/fileSystemCMSProvider.test.ts +124 -95
  157. package/lib/server/providers/fileSystemCMSProvider.ts +35 -20
  158. package/lib/server/providers/fileSystemPageProvider.test.ts +84 -0
  159. package/lib/server/providers/fileSystemPageProvider.ts +31 -12
  160. package/lib/server/routes/api/cms.test.ts +26 -14
  161. package/lib/server/routes/api/cms.ts +9 -14
  162. package/lib/server/routes/api/components.ts +30 -34
  163. package/lib/server/routes/api/config.ts +0 -1
  164. package/lib/server/routes/api/core-routes.ts +47 -74
  165. package/lib/server/routes/api/functions.ts +8 -16
  166. package/lib/server/routes/api/index.ts +3 -6
  167. package/lib/server/routes/api/pages.ts +8 -11
  168. package/lib/server/routes/api/shared.test.ts +1 -1
  169. package/lib/server/routes/api/shared.ts +12 -9
  170. package/lib/server/routes/api/variables.test.ts +1 -3
  171. package/lib/server/routes/api/variables.ts +1 -1
  172. package/lib/server/routes/index.ts +23 -26
  173. package/lib/server/routes/pages.ts +34 -29
  174. package/lib/server/routes/static.ts +16 -4
  175. package/lib/server/runtime/bundler.ts +47 -32
  176. package/lib/server/runtime/fs.ts +3 -13
  177. package/lib/server/runtime/httpServer.ts +5 -9
  178. package/lib/server/services/ColorService.ts +32 -27
  179. package/lib/server/services/EnumService.test.ts +2 -6
  180. package/lib/server/services/EnumService.ts +7 -2
  181. package/lib/server/services/VariableService.test.ts +1 -5
  182. package/lib/server/services/VariableService.ts +6 -1
  183. package/lib/server/services/cmsService.test.ts +116 -78
  184. package/lib/server/services/cmsService.ts +24 -54
  185. package/lib/server/services/componentService.test.ts +122 -36
  186. package/lib/server/services/componentService.ts +54 -38
  187. package/lib/server/services/configService.test.ts +9 -31
  188. package/lib/server/services/configService.ts +20 -27
  189. package/lib/server/services/fileWatcherService.ts +40 -6
  190. package/lib/server/services/index.ts +0 -1
  191. package/lib/server/services/pageService.test.ts +1 -3
  192. package/lib/server/services/pageService.ts +14 -13
  193. package/lib/server/ssr/attributeBuilder.ts +16 -6
  194. package/lib/server/ssr/buildErrorOverlay.ts +11 -12
  195. package/lib/server/ssr/clientDataInjector.ts +7 -21
  196. package/lib/server/ssr/cmsSSRProcessor.ts +3 -6
  197. package/lib/server/ssr/cssCollector.ts +1 -1
  198. package/lib/server/ssr/errorOverlay.test.ts +1 -1
  199. package/lib/server/ssr/errorOverlay.ts +3 -9
  200. package/lib/server/ssr/htmlGenerator.nonce.test.ts +3 -9
  201. package/lib/server/ssr/htmlGenerator.test.ts +120 -43
  202. package/lib/server/ssr/htmlGenerator.ts +120 -85
  203. package/lib/server/ssr/imageMetadata.test.ts +3 -1
  204. package/lib/server/ssr/imageMetadata.ts +25 -19
  205. package/lib/server/ssr/jsCollector.test.ts +3 -13
  206. package/lib/server/ssr/jsCollector.ts +3 -8
  207. package/lib/server/ssr/liveReloadIntegration.test.ts +69 -28
  208. package/lib/server/ssr/metaTagGenerator.ts +2 -2
  209. package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
  210. package/lib/server/ssr/ssrRenderer.test.ts +196 -246
  211. package/lib/server/ssr/ssrRenderer.ts +609 -225
  212. package/lib/server/ssrRenderer.test.ts +1044 -950
  213. package/lib/server/utils/jsonLineMapper.test.ts +28 -28
  214. package/lib/server/utils/jsonLineMapper.ts +1 -1
  215. package/lib/server/validateStyleCoverage.ts +18 -20
  216. package/lib/server/webflow/buildWebflow.ts +41 -53
  217. package/lib/server/webflow/nodeToWebflow.test.ts +150 -218
  218. package/lib/server/webflow/nodeToWebflow.ts +195 -258
  219. package/lib/server/webflow/styleMapper.test.ts +15 -56
  220. package/lib/server/webflow/styleMapper.ts +33 -41
  221. package/lib/server/webflow/types.ts +1 -8
  222. package/lib/server/websocketManager.ts +16 -19
  223. package/lib/shared/attributeNodeUtils.test.ts +15 -15
  224. package/lib/shared/attributeNodeUtils.ts +5 -12
  225. package/lib/shared/breakpoints.ts +4 -11
  226. package/lib/shared/cmsQueryParser.test.ts +50 -42
  227. package/lib/shared/cmsQueryParser.ts +49 -31
  228. package/lib/shared/colorVariableUtils.test.ts +5 -5
  229. package/lib/shared/colorVariableUtils.ts +3 -8
  230. package/lib/shared/componentRefs.ts +1 -5
  231. package/lib/shared/constants.test.ts +3 -3
  232. package/lib/shared/constants.ts +4 -8
  233. package/lib/shared/cssGeneration.test.ts +262 -144
  234. package/lib/shared/cssGeneration.ts +189 -516
  235. package/lib/shared/cssNamedColors.ts +152 -30
  236. package/lib/shared/cssProperties.test.ts +5 -6
  237. package/lib/shared/cssProperties.ts +296 -112
  238. package/lib/shared/elementClassName.test.ts +109 -109
  239. package/lib/shared/elementClassName.ts +1 -1
  240. package/lib/shared/elementUtils.ts +12 -16
  241. package/lib/shared/errorLogger.ts +2 -10
  242. package/lib/shared/errors.test.ts +2 -13
  243. package/lib/shared/errors.ts +2 -8
  244. package/lib/shared/expressionEvaluator.test.ts +119 -0
  245. package/lib/shared/expressionEvaluator.ts +43 -24
  246. package/lib/shared/fontLoader.test.ts +19 -5
  247. package/lib/shared/friendlyError.ts +2 -3
  248. package/lib/shared/gradientUtils.test.ts +1 -5
  249. package/lib/shared/gradientUtils.ts +2 -6
  250. package/lib/shared/hrefRefs.ts +2 -10
  251. package/lib/shared/i18n.test.ts +1 -1
  252. package/lib/shared/i18n.ts +13 -34
  253. package/lib/shared/index.ts +4 -0
  254. package/lib/shared/inlineSvgStyleRules.ts +2 -4
  255. package/lib/shared/interactiveStyleMappings.test.ts +11 -33
  256. package/lib/shared/interactiveStyleMappings.ts +9 -16
  257. package/lib/shared/interactiveStyles.test.ts +165 -188
  258. package/lib/shared/interfaces/contentProvider.ts +1 -1
  259. package/lib/shared/itemTemplateUtils.test.ts +6 -12
  260. package/lib/shared/itemTemplateUtils.ts +19 -35
  261. package/lib/shared/jsonRepair.ts +8 -2
  262. package/lib/shared/libraryLoader.test.ts +15 -49
  263. package/lib/shared/libraryLoader.ts +7 -22
  264. package/lib/shared/netlifyLocale404.test.ts +179 -0
  265. package/lib/shared/netlifyLocale404.ts +110 -0
  266. package/lib/shared/nodeUtils.test.ts +24 -16
  267. package/lib/shared/nodeUtils.ts +49 -19
  268. package/lib/shared/pathArrayUtils.test.ts +1 -2
  269. package/lib/shared/pathArrayUtils.ts +1 -1
  270. package/lib/shared/pathSecurity.ts +1 -1
  271. package/lib/shared/pathUtils.test.ts +4 -6
  272. package/lib/shared/pathUtils.ts +42 -48
  273. package/lib/shared/paths/Path.test.ts +2 -2
  274. package/lib/shared/paths/Path.ts +0 -1
  275. package/lib/shared/paths/PathConverter.test.ts +1 -1
  276. package/lib/shared/paths/PathConverter.ts +14 -17
  277. package/lib/shared/paths/PathUtils.ts +9 -10
  278. package/lib/shared/paths/PathValidator.test.ts +2 -15
  279. package/lib/shared/paths/PathValidator.ts +11 -9
  280. package/lib/shared/paths/index.ts +1 -2
  281. package/lib/shared/propResolver.test.ts +240 -244
  282. package/lib/shared/propResolver.ts +14 -25
  283. package/lib/shared/pxToRem.test.ts +7 -6
  284. package/lib/shared/pxToRem.ts +2 -5
  285. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +9 -5
  286. package/lib/shared/registry/ClientRegistry.ts +0 -1
  287. package/lib/shared/registry/ComponentRegistry.test.ts +43 -29
  288. package/lib/shared/registry/ComponentRegistry.ts +9 -11
  289. package/lib/shared/registry/NodeTypeDefinition.ts +15 -8
  290. package/lib/shared/registry/RegistryManager.ts +1 -2
  291. package/lib/shared/registry/SSRRegistry.ts +0 -1
  292. package/lib/shared/registry/createNodeType.ts +7 -9
  293. package/lib/shared/registry/defineNodeType.ts +2 -6
  294. package/lib/shared/registry/index.ts +0 -1
  295. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
  296. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
  297. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +47 -18
  298. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
  299. package/lib/shared/registry/nodeTypes/ListNodeType.ts +78 -74
  300. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
  301. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +6 -7
  302. package/lib/shared/registry/nodeTypes/index.ts +10 -2
  303. package/lib/shared/responsiveScaling.test.ts +15 -31
  304. package/lib/shared/responsiveScaling.ts +55 -37
  305. package/lib/shared/responsiveStyleUtils.ts +11 -13
  306. package/lib/shared/richtext/htmlToTiptap.test.ts +23 -14
  307. package/lib/shared/richtext/htmlToTiptap.ts +1 -3
  308. package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
  309. package/lib/shared/richtext/types.ts +1 -8
  310. package/lib/shared/slugTranslator.test.ts +13 -13
  311. package/lib/shared/slugTranslator.ts +12 -16
  312. package/lib/shared/slugify.ts +9 -15
  313. package/lib/shared/styleNodeUtils.test.ts +8 -8
  314. package/lib/shared/styleNodeUtils.ts +5 -10
  315. package/lib/shared/styleUtils.test.ts +87 -61
  316. package/lib/shared/styleUtils.ts +5 -6
  317. package/lib/shared/themeDefaults.test.ts +11 -11
  318. package/lib/shared/themeDefaults.ts +3 -4
  319. package/lib/shared/tree/PathBuilder.test.ts +62 -138
  320. package/lib/shared/tree/PathBuilder.ts +49 -39
  321. package/lib/shared/treePathUtils.test.ts +2 -10
  322. package/lib/shared/treePathUtils.ts +54 -59
  323. package/lib/shared/types/api.ts +1 -2
  324. package/lib/shared/types/cms.ts +25 -21
  325. package/lib/shared/types/comment.ts +49 -16
  326. package/lib/shared/types/components.ts +27 -25
  327. package/lib/shared/types/errors.test.ts +1 -6
  328. package/lib/shared/types/errors.ts +3 -7
  329. package/lib/shared/types/experiments.ts +28 -28
  330. package/lib/shared/types/index.ts +2 -2
  331. package/lib/shared/types/styles.ts +0 -1
  332. package/lib/shared/types/variables.test.ts +4 -13
  333. package/lib/shared/types/variables.ts +48 -27
  334. package/lib/shared/types.ts +1 -2
  335. package/lib/shared/utilityClassConfig.ts +648 -321
  336. package/lib/shared/utilityClassMapper.test.ts +203 -92
  337. package/lib/shared/utilityClassMapper.ts +188 -246
  338. package/lib/shared/utilityClassNames.ts +326 -0
  339. package/lib/shared/utils.test.ts +2 -10
  340. package/lib/shared/utils.ts +19 -10
  341. package/lib/shared/validation/cmsValidators.ts +2 -1
  342. package/lib/shared/validation/commentValidators.test.ts +53 -0
  343. package/lib/shared/validation/commentValidators.ts +12 -1
  344. package/lib/shared/validation/propValidator.test.ts +18 -20
  345. package/lib/shared/validation/propValidator.ts +12 -17
  346. package/lib/shared/validation/schemas.test.ts +24 -33
  347. package/lib/shared/validation/schemas.ts +469 -344
  348. package/lib/shared/validation/validators.test.ts +1 -6
  349. package/lib/shared/validation/validators.ts +89 -68
  350. package/lib/shared/viewportUnits.integration.test.ts +14 -10
  351. package/lib/shared/viewportUnits.test.ts +11 -23
  352. package/lib/test-utils/dom-setup.ts +1 -1
  353. package/lib/test-utils/factories/ConsoleMockFactory.ts +3 -7
  354. package/lib/test-utils/factories/DomMockFactory.ts +7 -19
  355. package/lib/test-utils/factories/EventMockFactory.ts +7 -13
  356. package/lib/test-utils/factories/FetchMockFactory.ts +39 -57
  357. package/lib/test-utils/factories/ServerMockFactory.ts +5 -9
  358. package/lib/test-utils/factories/StoreMockFactory.ts +14 -25
  359. package/lib/test-utils/fixtures.ts +45 -45
  360. package/lib/test-utils/helpers/asyncHelpers.test.ts +15 -18
  361. package/lib/test-utils/helpers/asyncHelpers.ts +11 -20
  362. package/lib/test-utils/helpers.ts +1 -5
  363. package/lib/test-utils/index.ts +0 -4
  364. package/lib/test-utils/mockFactories.ts +12 -18
  365. package/lib/test-utils/mocks.ts +4 -2
  366. package/package.json +1 -1
  367. package/scripts/build-meno-filter.ts +1 -4
  368. package/vite.config.ts +4 -4
  369. package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
  370. package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
  371. package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
  372. package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
  373. package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
  374. package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
  375. package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
  376. package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
  377. package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
  378. package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
  379. package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
  380. package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
  381. package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
  382. package/dist/chunks/chunk-X754AHS5.js.map +0 -7
  383. package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
  384. package/dist/chunks/configService-R3OGU2UD.js +0 -13
  385. /package/dist/chunks/{configService-R3OGU2UD.js.map → configService-PRJZF7Y6.js.map} +0 -0
  386. /package/dist/chunks/{constants-STK2YBIW.js.map → constants-KIQEYMAM.js.map} +0 -0
  387. /package/dist/chunks/{fs-JGINUXGL.js.map → fs-ZI5JEU7V.js.map} +0 -0
@@ -3,21 +3,64 @@
3
3
  * Converts JSON component structures to HTML strings for SEO-friendly initial page loads
4
4
  */
5
5
 
6
- import type { ComponentNode, ComponentDefinition, JSONPage, CMSItem, EmbedNode, LinkNode, LocaleListNode, CMSFilterCondition } from '../../shared/types';
6
+ import type {
7
+ ComponentNode,
8
+ ComponentDefinition,
9
+ JSONPage,
10
+ CMSItem,
11
+ EmbedNode,
12
+ LinkNode,
13
+ LocaleListNode,
14
+ CMSFilterCondition,
15
+ } from '../../shared/types';
7
16
  import type { TemplateContext, NestedCMSListConfig } from '../../shared/types/cms';
8
- import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, buildTemplateContext, resolveItemsTemplate, resolveTemplateRawValue, addItemUrl, getNestedValue, type ValueResolver } from '../../shared/itemTemplateUtils';
17
+ import {
18
+ processItemTemplate,
19
+ processItemPropsTemplate,
20
+ hasItemTemplates,
21
+ buildTemplateContext,
22
+ resolveItemsTemplate,
23
+ resolveTemplateRawValue,
24
+ addItemUrl,
25
+ getNestedValue,
26
+ type ValueResolver,
27
+ } from '../../shared/itemTemplateUtils';
9
28
  import { singularize, isItemDraftForLocale } from '../../shared/types/cms';
10
29
  import type { ResponsiveStyleObject, StyleObject } from '../../shared/types';
11
30
  import type { BreakpointConfig } from '../../shared/breakpoints';
12
31
  import type { ResponsiveScales } from '../../shared/responsiveScaling';
13
- import { evaluateTemplate, processStructure, isResponsiveStyle, isHtmlMapping, resolveHtmlMapping } from '../../client/templateEngine';
32
+ import {
33
+ evaluateTemplate,
34
+ processStructure,
35
+ isResponsiveStyle,
36
+ isHtmlMapping,
37
+ resolveHtmlMapping,
38
+ } from '../../client/templateEngine';
14
39
  import { resolvePropsFromDefinition, isRichTextMarker, richTextMarkerToHtml } from '../../shared/propResolver';
15
40
  import { loadBreakpointConfig, loadI18nConfig } from '../jsonLoader';
16
41
  import { configService } from '../services/configService';
17
42
  import type { I18nConfig } from '../../shared/types/components';
18
- import { extractLocaleFromPath, DEFAULT_I18N_CONFIG, resolveI18nValue, buildLocalizedPath, isI18nValue } from '../../shared/i18n';
43
+ import {
44
+ extractLocaleFromPath,
45
+ DEFAULT_I18N_CONFIG,
46
+ resolveI18nValue,
47
+ buildLocalizedPath,
48
+ isI18nValue,
49
+ } from '../../shared/i18n';
19
50
  import { NODE_TYPE } from '../../shared/constants';
20
- import { isComponentNode, isHtmlNode, isLinkNode, isEmbedNode, isLocaleListNode, isListNode, markAsSlotContent, isBooleanMapping, hasIf, isSlotContent, hasChildren } from '../../shared/nodeUtils';
51
+ import {
52
+ isComponentNode,
53
+ isHtmlNode,
54
+ isLinkNode,
55
+ isEmbedNode,
56
+ isLocaleListNode,
57
+ isListNode,
58
+ markAsSlotContent,
59
+ isBooleanMapping,
60
+ hasIf,
61
+ isSlotContent,
62
+ hasChildren,
63
+ } from '../../shared/nodeUtils';
21
64
  import type { ListNode } from '../../shared/registry/nodeTypes/ListNodeType';
22
65
  import type { CMSService } from '../services/cmsService';
23
66
  import { extractAttributesFromNode, skipEmptyTemplateAttributes } from '../../shared/attributeNodeUtils';
@@ -46,20 +89,31 @@ import { responsiveStylesToClasses } from '../../shared/utilityClassMapper';
46
89
  import { validateStyleCoverage } from '../validateStyleCoverage';
47
90
  import { generateElementClassName, type ElementClassContext } from '../../shared/elementClassName';
48
91
  import type { InteractiveStyles } from '../../shared/types/styles';
49
- import { extractInteractiveStyleMappings, resolveExtractedMappings, hasInteractiveStyleMappings } from '../../shared/interactiveStyleMappings';
92
+ import {
93
+ extractInteractiveStyleMappings,
94
+ resolveExtractedMappings,
95
+ hasInteractiveStyleMappings,
96
+ } from '../../shared/interactiveStyleMappings';
50
97
  import { isCurrentLink } from '../../shared/linkUtils';
51
98
  import type { SlugMap } from '../../shared/slugTranslator';
52
99
  import { buildSlugIndex, getLocaleLinks, translatePath } from '../../shared/slugTranslator';
53
100
 
54
101
  // Import from modularized files
55
102
  import { escapeHtml, buildAttributes, buildEditorAttrs, styleToString } from './attributeBuilder';
103
+ import { logRuntimeError } from '../../shared/errorLogger';
56
104
  import { toFriendlyError } from '../../shared/friendlyError';
57
105
  import { rewriteViewportUnits } from '../../shared/viewportUnits';
58
106
  import { extractPageMeta, generateMetaTags } from './metaTagGenerator';
59
107
  import { collectComponentCSS } from './cssCollector';
60
108
  import { collectComponentJavaScript } from './jsCollector';
61
- import { CMSContext, processCMSTemplate, processCMSPropsTemplate, createI18nResolver, RAW_HTML_PREFIX } from './cmsSSRProcessor';
62
- import { ImageMetadataMap, DEFAULT_SIZES, buildImageMetadataMap, rewriteRichTextImages } from './imageMetadata';
109
+ import {
110
+ type CMSContext,
111
+ processCMSTemplate,
112
+ processCMSPropsTemplate,
113
+ createI18nResolver,
114
+ RAW_HTML_PREFIX,
115
+ } from './cmsSSRProcessor';
116
+ import { type ImageMetadataMap, DEFAULT_SIZES, buildImageMetadataMap, rewriteRichTextImages } from './imageMetadata';
63
117
 
64
118
  /**
65
119
  * Image preload info for generating <link rel="preload"> tags in head
@@ -86,7 +140,7 @@ export type { CMSContext } from './cmsSSRProcessor';
86
140
  function resolveI18nAttrs<T extends Record<string, unknown>>(
87
141
  attrs: T,
88
142
  locale: string | undefined,
89
- i18nConfig: I18nConfig | undefined
143
+ i18nConfig: I18nConfig | undefined,
90
144
  ): T {
91
145
  let mutated: Record<string, unknown> | null = null;
92
146
  const config = i18nConfig ?? DEFAULT_I18N_CONFIG;
@@ -243,47 +297,37 @@ function localizeHref(href: string, ctx: SSRContext): string {
243
297
  */
244
298
  function localizeRichTextLinks(html: string, ctx: SSRContext): string {
245
299
  if (ctx.templateMode || !ctx.locale || !ctx.i18nConfig) return html;
246
- return html.replace(
247
- /<a\b([^>]*?)href=(["'])([^"']*?)\2([^>]*?)>/gi,
248
- (match, before, quote, href, after) => {
249
- if (!href.startsWith('/') || href.startsWith('//')) return match;
250
- const localized = localizeHref(href, ctx);
251
- return localized === href ? match : `<a${before}href=${quote}${localized}${quote}${after}>`;
252
- }
253
- );
300
+ return html.replace(/<a\b([^>]*?)href=(["'])([^"']*?)\2([^>]*?)>/gi, (match, before, quote, href, after) => {
301
+ if (!href.startsWith('/') || href.startsWith('//')) return match;
302
+ const localized = localizeHref(href, ctx);
303
+ return localized === href ? match : `<a${before}href=${quote}${localized}${quote}${after}>`;
304
+ });
254
305
  }
255
306
 
256
307
  /**
257
308
  * Process style templates and convert to utility classes.
258
309
  * Handles {{item.field}} patterns within list contexts.
259
310
  */
260
- function processStyleToClasses(
261
- style: StyleObject | ResponsiveStyleObject | undefined,
262
- ctx: SSRContext
263
- ): string[] {
311
+ function processStyleToClasses(style: StyleObject | ResponsiveStyleObject | undefined, ctx: SSRContext): string[] {
264
312
  if (!style) return [];
265
313
 
266
314
  let processedStyle = style;
267
315
  const templateCtx = getTemplateContext(ctx);
268
316
  if (templateCtx && !ctx.templateMode) {
269
- processedStyle = processItemPropsTemplate(
270
- style as Record<string, unknown>,
271
- templateCtx,
272
- getI18nResolver(ctx)
273
- ) as StyleObject | ResponsiveStyleObject;
317
+ processedStyle = processItemPropsTemplate(style as Record<string, unknown>, templateCtx, getI18nResolver(ctx)) as
318
+ | StyleObject
319
+ | ResponsiveStyleObject;
274
320
  }
275
321
 
276
322
  // Fluid container pattern: in fluid mode `responsiveStylesToClasses` rewrites
277
323
  // any tier whose `width === maxWidth` to use `calc(100% - var(--site-margin)*2)`
278
324
  // with auto margins.
279
- const fluidActive =
280
- ctx.responsiveScales?.enabled === true &&
281
- ctx.responsiveScales?.mode === 'fluid';
325
+ const fluidActive = ctx.responsiveScales?.enabled === true && ctx.responsiveScales?.mode === 'fluid';
282
326
 
283
- return responsiveStylesToClasses(
284
- processedStyle as ResponsiveStyleObject,
285
- { fluidActive, responsiveScales: ctx.responsiveScales }
286
- );
327
+ return responsiveStylesToClasses(processedStyle as ResponsiveStyleObject, {
328
+ fluidActive,
329
+ responsiveScales: ctx.responsiveScales,
330
+ });
287
331
  }
288
332
 
289
333
  /**
@@ -344,50 +388,88 @@ function evaluateIfCondition(node: ComponentNode, ctx: SSRContext): boolean {
344
388
  return true;
345
389
  }
346
390
 
391
+ /**
392
+ * Sentinel returned by resolveFilterValue when a {{...}} template value can't
393
+ * be resolved from the available scope. Lets resolveFilterTemplates tell
394
+ * "resolved to a real value" apart from "unresolvable" so the condition can be
395
+ * dropped from the CMS query instead of being sent as a literal template
396
+ * string — which would never match a real value and silently return nothing.
397
+ */
398
+ const UNRESOLVED_FILTER_VALUE = Symbol('unresolved-filter-value');
399
+
347
400
  /**
348
401
  * Resolve a template value like "{{category.slug}}" from a flat scope.
349
- * Returns the original value if not a template or can't be resolved.
402
+ * Returns the original value for non-template values, and
403
+ * UNRESOLVED_FILTER_VALUE when a template can't be resolved.
350
404
  */
351
405
  function resolveFilterValue(value: unknown, scope: Record<string, unknown> | undefined): unknown {
352
- if (!scope || typeof value !== 'string' || !value.startsWith('{{') || !value.endsWith('}}')) {
406
+ if (typeof value !== 'string' || !value.startsWith('{{') || !value.endsWith('}}')) {
353
407
  return value;
354
408
  }
355
409
  const path = value.slice(2, -2).trim();
356
- const resolved = getNestedValue(scope, path);
357
- return resolved !== undefined ? resolved : value;
410
+ const resolved = scope ? getNestedValue(scope, path) : undefined;
411
+ return resolved !== undefined ? resolved : UNRESOLVED_FILTER_VALUE;
358
412
  }
359
413
 
360
414
  /**
361
415
  * Resolve template values in CMS filter for nested cms-list support.
362
416
  * Handles filter values like "{{category.slug}}" against a flat scope built from
363
417
  * parent template context overlaid on host-component resolved props.
418
+ *
419
+ * Conditions whose template value can't be resolved are dropped from the query
420
+ * (a verbatim "{{x.y}}" string would match nothing) and surfaced via
421
+ * logRuntimeError — dropping a condition changes results, so users must be
422
+ * able to see why.
364
423
  */
365
424
  function resolveFilterTemplates(
366
425
  filter: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined,
367
- scope: Record<string, unknown> | undefined
426
+ scope: Record<string, unknown> | undefined,
427
+ collection: string,
368
428
  ): CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined {
369
- if (!filter || !scope) return filter;
429
+ if (!filter) return filter;
430
+
431
+ const reportDropped = (field: unknown, template: unknown): void => {
432
+ logRuntimeError(
433
+ 'ssrRenderer.resolveFilterTemplates',
434
+ new Error(`Unresolved filter template ${String(template)} — condition dropped from "${collection}" query`),
435
+ { collection, field: String(field), template: String(template) },
436
+ );
437
+ };
370
438
 
371
439
  // Handle array of conditions
372
440
  if (Array.isArray(filter)) {
373
- return filter.map(cond => ({
374
- ...cond,
375
- value: resolveFilterValue(cond.value, scope)
376
- }));
441
+ const conditions: CMSFilterCondition[] = [];
442
+ for (const cond of filter) {
443
+ const value = resolveFilterValue(cond.value, scope);
444
+ if (value === UNRESOLVED_FILTER_VALUE) {
445
+ reportDropped(cond.field, cond.value);
446
+ continue;
447
+ }
448
+ conditions.push({ ...cond, value } as CMSFilterCondition);
449
+ }
450
+ return conditions;
377
451
  }
378
452
 
379
453
  // Handle single condition object with field/value
380
454
  if ('field' in filter && 'value' in filter) {
381
- return {
382
- ...(filter as CMSFilterCondition),
383
- value: resolveFilterValue((filter as CMSFilterCondition).value, scope)
384
- };
455
+ const cond = filter as CMSFilterCondition;
456
+ const value = resolveFilterValue(cond.value, scope);
457
+ if (value === UNRESOLVED_FILTER_VALUE) {
458
+ reportDropped(cond.field, cond.value);
459
+ return undefined;
460
+ }
461
+ return { ...cond, value } as CMSFilterCondition;
385
462
  }
386
463
 
387
464
  // Handle simple object filter: { category: "{{category.slug}}" }
388
465
  const resolved: Record<string, unknown> = {};
389
466
  for (const [key, value] of Object.entries(filter)) {
390
- resolved[key] = resolveFilterValue(value, scope);
467
+ const resolvedValue = resolveFilterValue(value, scope);
468
+ if (resolvedValue === UNRESOLVED_FILTER_VALUE) {
469
+ reportDropped(key, value);
470
+ continue;
471
+ }
472
+ resolved[key] = resolvedValue;
391
473
  }
392
474
  return resolved;
393
475
  }
@@ -422,15 +504,16 @@ async function expandRichTextComponents(html: string, ctx: SSRContext): Promise<
422
504
  .replace(/&#039;/g, "'")
423
505
  .replace(/&amp;/g, '&');
424
506
  props = JSON.parse(propsStr);
425
- } catch {
426
- // ignore parse errors
507
+ } catch (error) {
508
+ // Malformed data-meno-props JSON: fall back to interface defaults so the
509
+ // marker never crashes the page, but surface the parse failure — the
510
+ // component silently losing its instance props is otherwise invisible.
511
+ logRuntimeError('ssrRenderer.expandRichTextComponents', error, { component: componentName });
427
512
  }
428
513
 
429
514
  // Render the component using SSR registry
430
515
  if (ssrComponentRegistry.has(componentName)) {
431
- parts.push(
432
- renderComponent(componentName, props, [], {}, ctx)
433
- );
516
+ parts.push(renderComponent(componentName, props, [], {}, ctx));
434
517
  } else {
435
518
  // Keep marker for unknown components
436
519
  parts.push(match[0]);
@@ -470,8 +553,15 @@ export async function buildComponentHTML(
470
553
  cmsContext?: CMSContext,
471
554
  cmsService?: CMSService,
472
555
  isProductionBuild?: boolean,
473
- injectEditorAttrs?: boolean
474
- ): Promise<{ html: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string>; processedRawHtmlCollector: Map<string, string> }> {
556
+ injectEditorAttrs?: boolean,
557
+ ): Promise<{
558
+ html: string;
559
+ interactiveStylesMap: Map<string, InteractiveStyles>;
560
+ preloadImages: PreloadImage[];
561
+ neededCollections: Set<string>;
562
+ ssrFallbackCollector: Map<string, string>;
563
+ processedRawHtmlCollector: Map<string, string>;
564
+ }> {
475
565
  // Create map to collect interactive styles during render
476
566
  const interactiveStylesMap = new Map<string, InteractiveStyles>();
477
567
  // Create array to collect high-priority images for preloading
@@ -483,7 +573,15 @@ export async function buildComponentHTML(
483
573
  // Create map to collect raw-HTML → processed-HTML for Astro exporter parity
484
574
  const processedRawHtmlCollector = new Map<string, string>();
485
575
 
486
- if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector };
576
+ if (!node)
577
+ return {
578
+ html: '',
579
+ interactiveStylesMap,
580
+ preloadImages,
581
+ neededCollections,
582
+ ssrFallbackCollector,
583
+ processedRawHtmlCollector,
584
+ };
487
585
 
488
586
  // Register components for this render
489
587
  ssrComponentRegistry.merge(globalComponents);
@@ -523,7 +621,14 @@ export async function buildComponentHTML(
523
621
 
524
622
  const html = await renderNode(node, ctx);
525
623
 
526
- return { html, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector };
624
+ return {
625
+ html,
626
+ interactiveStylesMap,
627
+ preloadImages,
628
+ neededCollections,
629
+ ssrFallbackCollector,
630
+ processedRawHtmlCollector,
631
+ };
527
632
  }
528
633
 
529
634
  /**
@@ -531,10 +636,7 @@ export async function buildComponentHTML(
531
636
  * Used when parent list has emitTemplate: true.
532
637
  * The placeholder contains the configuration and template for MenoFilter to hydrate.
533
638
  */
534
- async function renderNestedListPlaceholder(
535
- node: ListNode,
536
- ctx: SSRContext
537
- ): Promise<string> {
639
+ async function renderNestedListPlaceholder(node: ListNode, ctx: SSRContext): Promise<string> {
538
640
  // Get source - handle both string and pre-resolved array sources
539
641
  const sourceValue = node.source || node.collection;
540
642
  const sourceStr = typeof sourceValue === 'string' ? sourceValue : '';
@@ -563,17 +665,17 @@ async function renderNestedListPlaceholder(
563
665
  templateContext: undefined, // Clear context for inner item templates
564
666
  nestedCMSListMode: false, // Reset for deeper nesting levels
565
667
  };
566
- const templateContent = node.children
567
- ? await renderChildrenAsync(node.children, childTemplateCtx)
568
- : '';
668
+ const templateContent = node.children ? await renderChildrenAsync(node.children, childTemplateCtx) : '';
569
669
 
570
670
  // Escape config for attribute
571
671
  const configJson = escapeHtml(JSON.stringify(config));
572
672
 
573
673
  // Emit placeholder with embedded config and template
574
- return `<div data-cms-list-nested="true" data-collection="${escapeHtml(sourceStr)}" data-cms-config="${configJson}"${editorAttrs(ctx, { isCMSListContainer: true })}>` +
674
+ return (
675
+ `<div data-cms-list-nested="true" data-collection="${escapeHtml(sourceStr)}" data-cms-config="${configJson}"${editorAttrs(ctx, { isCMSListContainer: true })}>` +
575
676
  `<template data-nested-template>${templateContent}</template>` +
576
- `</div>`;
677
+ `</div>`
678
+ );
577
679
  }
578
680
 
579
681
  /**
@@ -606,7 +708,7 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
606
708
  // Determine source type (default to 'prop', but handle legacy 'cms-list' type)
607
709
  const nodeType = node.type as string;
608
710
  const isLegacyCMSList = nodeType === 'cms-list';
609
- const sourceType = isLegacyCMSList ? 'collection' : (node.sourceType || 'prop');
711
+ const sourceType = isLegacyCMSList ? 'collection' : node.sourceType || 'prop';
610
712
 
611
713
  // For collection mode, check CMS service availability
612
714
  if (sourceType === 'collection' && !ctx.cmsService) {
@@ -676,26 +778,16 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
676
778
  }
677
779
 
678
780
  // Get schema for URL computation (collection mode only)
679
- const schema = sourceType === 'collection' && ctx.cmsService
680
- ? ctx.cmsService.getSchema(source)
681
- : undefined;
781
+ const schema = sourceType === 'collection' && ctx.cmsService ? ctx.cmsService.getSchema(source) : undefined;
682
782
 
683
783
  // Render children for each item
684
784
  const renderedItems: string[] = [];
685
785
 
686
786
  for (let i = 0; i < items.length; i++) {
687
787
  const rawItem = items[i] as Record<string, unknown>;
688
- const item = schema
689
- ? addItemUrl(rawItem as CMSItem, schema, ctx.locale, ctx.i18nConfig)
690
- : rawItem;
691
-
692
- const templateContext = buildTemplateContext(
693
- variableName,
694
- item as CMSItem,
695
- i,
696
- items.length,
697
- ctx.templateContext
698
- );
788
+ const item = schema ? addItemUrl(rawItem as CMSItem, schema, ctx.locale, ctx.i18nConfig) : rawItem;
789
+
790
+ const templateContext = buildTemplateContext(variableName, item as CMSItem, i, items.length, ctx.templateContext);
699
791
 
700
792
  const itemCtx: SSRContext = {
701
793
  ...ctx,
@@ -762,7 +854,7 @@ async function getCollectionItems(node: ListNode, source: string, ctx: SSRContex
762
854
  }
763
855
  }
764
856
  if (value !== null && value !== undefined) {
765
- resolvedIds = Array.isArray(value) ? value.map(v => String(v)) : String(value);
857
+ resolvedIds = Array.isArray(value) ? value.map((v) => String(v)) : String(value);
766
858
  }
767
859
  } else {
768
860
  // Otherwise resolve from a merged scope: parent list loop variables (highest precedence)
@@ -786,14 +878,18 @@ async function getCollectionItems(node: ListNode, source: string, ctx: SSRContex
786
878
  }
787
879
  // Filter draft items from ID-based queries
788
880
  if (ctx.locale) {
789
- items = items.filter(item => !isItemDraftForLocale(item, ctx.locale!));
881
+ items = items.filter((item) => !isItemDraftForLocale(item, ctx.locale!));
790
882
  }
791
883
  } else {
792
884
  // Build query from node props (resolve filter templates against parent loop variables
793
885
  // overlaid on host-component resolved props, with parent loop variables winning).
794
886
  const query = {
795
887
  collection: source,
796
- filter: resolveFilterTemplates(node.filter as CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined, buildListResolutionScope(ctx)),
888
+ filter: resolveFilterTemplates(
889
+ node.filter as CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined,
890
+ buildListResolutionScope(ctx),
891
+ source,
892
+ ),
797
893
  sort: node.sort,
798
894
  limit: node.limit,
799
895
  offset: node.offset,
@@ -805,7 +901,7 @@ async function getCollectionItems(node: ListNode, source: string, ctx: SSRContex
805
901
  // Exclude current item if option is set and we have a current CMS context
806
902
  if (node.excludeCurrentItem && ctx.cmsContext?.cms?._id) {
807
903
  const currentId = ctx.cmsContext.cms._id as string;
808
- items = items.filter(item => item._id !== currentId);
904
+ items = items.filter((item) => item._id !== currentId);
809
905
  }
810
906
 
811
907
  return items;
@@ -866,25 +962,21 @@ async function renderChildrenAsync(children: unknown[], ctx: SSRContext): Promis
866
962
  * Build element class name for a node based on its SSR context.
867
963
  * Used by embed, link, locale-list, and regular HTML nodes.
868
964
  */
869
- function buildNodeElementClass(
870
- ctx: SSRContext,
871
- label: string | undefined,
872
- isSlotContent?: boolean
873
- ): string {
965
+ function buildNodeElementClass(ctx: SSRContext, label: string | undefined, isSlotContent?: boolean): string {
874
966
  const { pagePath } = ctx;
875
967
  // Slot content uses page context (defined in page, not component)
876
968
  const useComponentContext = !isSlotContent && Boolean(ctx.componentContext);
877
969
  const effectiveFileType = useComponentContext ? 'component' : 'page';
878
- const effectiveFileName = useComponentContext ? ctx.componentContext : (pagePath
879
- ? pagePath.replace(/^\//, '').replace(/\//g, '_') || 'index'
880
- : 'page');
970
+ const effectiveFileName = useComponentContext
971
+ ? ctx.componentContext
972
+ : pagePath
973
+ ? pagePath.replace(/^\//, '').replace(/\//g, '_') || 'index'
974
+ : 'page';
881
975
 
882
976
  // Slice off the component-instance prefix so class hashes stay stable across
883
977
  // instances of the same component (mirrors ComponentBuilder.ts:678-680).
884
978
  const rawPath = ctx.elementPath || [];
885
- const path = useComponentContext && ctx.componentRootPath
886
- ? rawPath.slice(ctx.componentRootPath.length)
887
- : rawPath;
979
+ const path = useComponentContext && ctx.componentRootPath ? rawPath.slice(ctx.componentRootPath.length) : rawPath;
888
980
 
889
981
  const elementClassCtx: ElementClassContext = {
890
982
  fileType: effectiveFileType,
@@ -902,7 +994,7 @@ function buildNodeElementClass(
902
994
  function registerInteractiveStyles(
903
995
  ctx: SSRContext,
904
996
  elementClass: string,
905
- interactiveStyles: InteractiveStyles
997
+ interactiveStyles: InteractiveStyles,
906
998
  ): Record<string, string> {
907
999
  if (!ctx.interactiveStylesMap) return {};
908
1000
 
@@ -939,20 +1031,18 @@ function arraysEqual(a: number[] | undefined, b: number[] | undefined): boolean
939
1031
  * Compute editor-only attributes for the current context.
940
1032
  * Returns '' when injectEditorAttrs is off — preview-only feature.
941
1033
  */
942
- function editorAttrs(
943
- ctx: SSRContext,
944
- opts: { isSlotContent?: boolean; isCMSListContainer?: boolean } = {}
945
- ): string {
1034
+ function editorAttrs(ctx: SSRContext, opts: { isSlotContent?: boolean; isCMSListContainer?: boolean } = {}): string {
946
1035
  if (!ctx.injectEditorAttrs) return '';
947
1036
  // Component root: inside a component instance and at the path the instance was reset to.
948
- const isComponentRoot = !!ctx.componentContext
949
- && !opts.isSlotContent
950
- && arraysEqual(ctx.elementPath, ctx.componentRootPath);
1037
+ const isComponentRoot =
1038
+ !!ctx.componentContext && !opts.isSlotContent && arraysEqual(ctx.elementPath, ctx.componentRootPath);
951
1039
  // Mirror client semantics: at component root, parent is the outer component;
952
1040
  // for descendants of a component, parent is the current component itself.
953
1041
  const effectiveParent = opts.isSlotContent
954
1042
  ? ctx.parentComponentName
955
- : (isComponentRoot ? ctx.parentComponentName : (ctx.componentContext ?? ctx.parentComponentName));
1043
+ : isComponentRoot
1044
+ ? ctx.parentComponentName
1045
+ : (ctx.componentContext ?? ctx.parentComponentName);
956
1046
  return buildEditorAttrs({
957
1047
  elementPath: ctx.elementPath,
958
1048
  cmsItemIndexPath: ctx.cmsItemIndexPath,
@@ -970,7 +1060,7 @@ function editorAttrs(
970
1060
  */
971
1061
  async function renderNode(
972
1062
  node: ComponentNode | ComponentNode[] | string | number | null | undefined,
973
- ctx: SSRContext
1063
+ ctx: SSRContext,
974
1064
  ): Promise<string> {
975
1065
  const { breakpoints, viewportWidth, locale, i18nConfig, slugMappings, pagePath } = ctx;
976
1066
 
@@ -1028,10 +1118,14 @@ async function renderNode(
1028
1118
  }
1029
1119
 
1030
1120
  if (Array.isArray(node)) {
1031
- return (await Promise.all(node.map((child, index) => {
1032
- const childPath = ctx.elementPath ? [...ctx.elementPath.slice(0, -1), index] : [index];
1033
- return renderNode(child, { ...ctx, elementPath: childPath });
1034
- }))).join('');
1121
+ return (
1122
+ await Promise.all(
1123
+ node.map((child, index) => {
1124
+ const childPath = ctx.elementPath ? [...ctx.elementPath.slice(0, -1), index] : [index];
1125
+ return renderNode(child, { ...ctx, elementPath: childPath });
1126
+ }),
1127
+ )
1128
+ ).join('');
1035
1129
  }
1036
1130
 
1037
1131
  if (typeof node !== 'object') return '';
@@ -1045,10 +1139,7 @@ async function renderNode(
1045
1139
  const i18nResolveConfig = i18nConfig ?? DEFAULT_I18N_CONFIG;
1046
1140
  const i18nEffectiveLocale = locale || i18nResolveConfig.defaultLocale;
1047
1141
  const resolved = resolveI18nValue(node, i18nEffectiveLocale, i18nResolveConfig);
1048
- return renderNode(
1049
- resolved as ComponentNode | string | number | null | undefined,
1050
- ctx
1051
- );
1142
+ return renderNode(resolved as ComponentNode | string | number | null | undefined, ctx);
1052
1143
  }
1053
1144
 
1054
1145
  // Check if condition - skip rendering if false
@@ -1069,8 +1160,8 @@ async function renderNode(
1069
1160
  }
1070
1161
 
1071
1162
  const nodeType = 'type' in node ? node.type : undefined;
1072
- const nodeStyle = ('style' in node) ? node.style as StyleObject | ResponsiveStyleObject | undefined : undefined;
1073
- const children = ('children' in node) ? (node.children || []) : [];
1163
+ const nodeStyle = 'style' in node ? (node.style as StyleObject | ResponsiveStyleObject | undefined) : undefined;
1164
+ const children = 'children' in node ? node.children || [] : [];
1074
1165
 
1075
1166
  // Handle embed nodes - render custom HTML content
1076
1167
  if (isEmbedNode(node)) {
@@ -1106,11 +1197,198 @@ async function renderNode(
1106
1197
 
1107
1198
  // Sanitize HTML with allowlist for SVG, rich-text formatting, and common elements (same as client)
1108
1199
  const purify = getDOMPurify();
1109
- const purifiedHtml = purify ? purify.sanitize(htmlContent, {
1110
- ALLOWED_TAGS: ['svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'text', 'tspan', 'image', 'defs', 'use', 'linearGradient', 'radialGradient', 'stop', 'clipPath', 'mask', 'pattern', 'marker', 'symbol', 'a', 'div', 'span', 'p', 'br', 'button', 'img', 'iframe', 'video', 'audio', 'source', 'canvas', 'b', 'i', 'u', 'strong', 'em', 'sub', 'sup', 'mark', 's', 'small', 'del', 'ins', 'q', 'abbr', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'style', 'animate', 'animateTransform', 'animateMotion', 'set', 'filter', 'feGaussianBlur', 'feOffset', 'feMerge', 'feMergeNode', 'feColorMatrix', 'feComposite', 'feFlood', 'feMorphology', 'feBlend', 'feDropShadow', 'feTurbulence', 'feDisplacementMap', 'foreignObject'],
1111
- ALLOWED_ATTR: ['class', 'id', 'style', 'width', 'height', 'viewBox', 'xmlns', 'xmlns:xlink', 'xlink:href', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'd', 'cx', 'cy', 'r', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'points', 'href', 'src', 'alt', 'target', 'rel', 'data-*', 'aria-*', 'transform', 'opacity', 'fill-opacity', 'fill-rule', 'clip-rule', 'clip-path', 'clipPathUnits', 'mask', 'mask-type', 'maskUnits', 'maskContentUnits', 'patternUnits', 'patternContentUnits', 'patternTransform', 'gradientUnits', 'gradientTransform', 'spreadMethod', 'preserveAspectRatio', 'marker-start', 'marker-mid', 'marker-end', 'markerUnits', 'markerWidth', 'markerHeight', 'refX', 'refY', 'orient', 'paint-order', 'vector-effect', 'filter', 'filterUnits', 'primitiveUnits', 'in', 'in2', 'result', 'stdDeviation', 'flood-color', 'flood-opacity', 'stroke-opacity', 'font-size', 'font-family', 'font-weight', 'font-style', 'text-anchor', 'dominant-baseline', 'offset', 'stop-color', 'stop-opacity', 'frameborder', 'allowfullscreen', 'allow', 'title', 'attributeName', 'values', 'dur', 'begin', 'end', 'repeatCount', 'repeatDur', 'keyTimes', 'keySplines', 'calcMode', 'from', 'to', 'by', 'additive', 'accumulate', 'type', 'rotate', 'keyPoints', 'path'],
1112
- KEEP_CONTENT: true
1113
- }) : htmlContent;
1200
+ const purifiedHtml = purify
1201
+ ? purify.sanitize(htmlContent, {
1202
+ ALLOWED_TAGS: [
1203
+ 'svg',
1204
+ 'path',
1205
+ 'circle',
1206
+ 'rect',
1207
+ 'line',
1208
+ 'polyline',
1209
+ 'polygon',
1210
+ 'g',
1211
+ 'text',
1212
+ 'tspan',
1213
+ 'image',
1214
+ 'defs',
1215
+ 'use',
1216
+ 'linearGradient',
1217
+ 'radialGradient',
1218
+ 'stop',
1219
+ 'clipPath',
1220
+ 'mask',
1221
+ 'pattern',
1222
+ 'marker',
1223
+ 'symbol',
1224
+ 'a',
1225
+ 'div',
1226
+ 'span',
1227
+ 'p',
1228
+ 'br',
1229
+ 'button',
1230
+ 'img',
1231
+ 'iframe',
1232
+ 'video',
1233
+ 'audio',
1234
+ 'source',
1235
+ 'canvas',
1236
+ 'b',
1237
+ 'i',
1238
+ 'u',
1239
+ 'strong',
1240
+ 'em',
1241
+ 'sub',
1242
+ 'sup',
1243
+ 'mark',
1244
+ 's',
1245
+ 'small',
1246
+ 'del',
1247
+ 'ins',
1248
+ 'q',
1249
+ 'abbr',
1250
+ 'code',
1251
+ 'pre',
1252
+ 'blockquote',
1253
+ 'ul',
1254
+ 'ol',
1255
+ 'li',
1256
+ 'h1',
1257
+ 'h2',
1258
+ 'h3',
1259
+ 'h4',
1260
+ 'h5',
1261
+ 'h6',
1262
+ 'style',
1263
+ 'animate',
1264
+ 'animateTransform',
1265
+ 'animateMotion',
1266
+ 'set',
1267
+ 'filter',
1268
+ 'feGaussianBlur',
1269
+ 'feOffset',
1270
+ 'feMerge',
1271
+ 'feMergeNode',
1272
+ 'feColorMatrix',
1273
+ 'feComposite',
1274
+ 'feFlood',
1275
+ 'feMorphology',
1276
+ 'feBlend',
1277
+ 'feDropShadow',
1278
+ 'feTurbulence',
1279
+ 'feDisplacementMap',
1280
+ 'foreignObject',
1281
+ ],
1282
+ ALLOWED_ATTR: [
1283
+ 'class',
1284
+ 'id',
1285
+ 'style',
1286
+ 'width',
1287
+ 'height',
1288
+ 'viewBox',
1289
+ 'xmlns',
1290
+ 'xmlns:xlink',
1291
+ 'xlink:href',
1292
+ 'fill',
1293
+ 'stroke',
1294
+ 'stroke-width',
1295
+ 'stroke-linecap',
1296
+ 'stroke-linejoin',
1297
+ 'stroke-dasharray',
1298
+ 'stroke-dashoffset',
1299
+ 'd',
1300
+ 'cx',
1301
+ 'cy',
1302
+ 'r',
1303
+ 'x',
1304
+ 'y',
1305
+ 'x1',
1306
+ 'y1',
1307
+ 'x2',
1308
+ 'y2',
1309
+ 'points',
1310
+ 'href',
1311
+ 'src',
1312
+ 'alt',
1313
+ 'target',
1314
+ 'rel',
1315
+ 'data-*',
1316
+ 'aria-*',
1317
+ 'transform',
1318
+ 'opacity',
1319
+ 'fill-opacity',
1320
+ 'fill-rule',
1321
+ 'clip-rule',
1322
+ 'clip-path',
1323
+ 'clipPathUnits',
1324
+ 'mask',
1325
+ 'mask-type',
1326
+ 'maskUnits',
1327
+ 'maskContentUnits',
1328
+ 'patternUnits',
1329
+ 'patternContentUnits',
1330
+ 'patternTransform',
1331
+ 'gradientUnits',
1332
+ 'gradientTransform',
1333
+ 'spreadMethod',
1334
+ 'preserveAspectRatio',
1335
+ 'marker-start',
1336
+ 'marker-mid',
1337
+ 'marker-end',
1338
+ 'markerUnits',
1339
+ 'markerWidth',
1340
+ 'markerHeight',
1341
+ 'refX',
1342
+ 'refY',
1343
+ 'orient',
1344
+ 'paint-order',
1345
+ 'vector-effect',
1346
+ 'filter',
1347
+ 'filterUnits',
1348
+ 'primitiveUnits',
1349
+ 'in',
1350
+ 'in2',
1351
+ 'result',
1352
+ 'stdDeviation',
1353
+ 'flood-color',
1354
+ 'flood-opacity',
1355
+ 'stroke-opacity',
1356
+ 'font-size',
1357
+ 'font-family',
1358
+ 'font-weight',
1359
+ 'font-style',
1360
+ 'text-anchor',
1361
+ 'dominant-baseline',
1362
+ 'offset',
1363
+ 'stop-color',
1364
+ 'stop-opacity',
1365
+ 'frameborder',
1366
+ 'allowfullscreen',
1367
+ 'allow',
1368
+ 'title',
1369
+ 'attributeName',
1370
+ 'values',
1371
+ 'dur',
1372
+ 'begin',
1373
+ 'end',
1374
+ 'repeatCount',
1375
+ 'repeatDur',
1376
+ 'keyTimes',
1377
+ 'keySplines',
1378
+ 'calcMode',
1379
+ 'from',
1380
+ 'to',
1381
+ 'by',
1382
+ 'additive',
1383
+ 'accumulate',
1384
+ 'type',
1385
+ 'rotate',
1386
+ 'keyPoints',
1387
+ 'path',
1388
+ ],
1389
+ KEEP_CONTENT: true,
1390
+ })
1391
+ : htmlContent;
1114
1392
  // Inline simple SVG <style> rules as style="" attributes — class-scoped
1115
1393
  // declarations don't survive the editor preview's HMR `innerHTML` reparse
1116
1394
  // path (`htmlGenerator.ts:574`), but inline attributes always render.
@@ -1144,7 +1422,7 @@ async function renderNode(
1144
1422
  embedCssVariables = registerInteractiveStyles(ctx, elementClass, embedInteractiveStyles);
1145
1423
  }
1146
1424
 
1147
- let embedStyleAttr = buildCssVariableStyleAttr(embedCssVariables);
1425
+ const embedStyleAttr = buildCssVariableStyleAttr(embedCssVariables);
1148
1426
 
1149
1427
  // Add attribute className if present
1150
1428
  const attrClassName = (nodeAttributes.className || nodeAttributes.class || '') as string;
@@ -1191,7 +1469,11 @@ async function renderNode(
1191
1469
  // Link object - extract href and target
1192
1470
  // Unwrap nested link objects (e.g., {href: {href: "/path"}} from double-wrapped list item templates)
1193
1471
  let linkObj = rawValue as { href: unknown; target?: string };
1194
- while (typeof linkObj.href === 'object' && linkObj.href !== null && 'href' in (linkObj.href as Record<string, unknown>)) {
1472
+ while (
1473
+ typeof linkObj.href === 'object' &&
1474
+ linkObj.href !== null &&
1475
+ 'href' in (linkObj.href as Record<string, unknown>)
1476
+ ) {
1195
1477
  const nested = linkObj.href as { href: unknown; target?: string };
1196
1478
  if (!linkObj.target && nested.target) linkObj = { ...linkObj, target: nested.target };
1197
1479
  linkObj = { ...linkObj, href: nested.href };
@@ -1262,11 +1544,18 @@ async function renderNode(
1262
1544
  const classAttr = ` class="${escapeHtml(classNames.filter(Boolean).join(' '))}"`;
1263
1545
 
1264
1546
  const childrenHTML = Array.isArray(children)
1265
- ? (await Promise.all(children.map((child, index) => {
1266
- const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
1267
- return renderNode(child, { ...ctx, elementPath: childPath });
1268
- }))).join('')
1269
- : await renderNode(children as ComponentNode | string | number | null | undefined, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
1547
+ ? (
1548
+ await Promise.all(
1549
+ children.map((child, index) => {
1550
+ const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
1551
+ return renderNode(child, { ...ctx, elementPath: childPath });
1552
+ }),
1553
+ )
1554
+ ).join('')
1555
+ : await renderNode(children as ComponentNode | string | number | null | undefined, {
1556
+ ...ctx,
1557
+ elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0],
1558
+ });
1270
1559
 
1271
1560
  return `<a href="${escapeHtml(String(href))}"${classAttr}${olinkStyleAttr}${attrs}${editorAttrs(ctx, { isSlotContent: isSlotContent(node) })}>${childrenHTML}</a>`;
1272
1561
  }
@@ -1277,9 +1566,9 @@ async function renderNode(
1277
1566
  }
1278
1567
 
1279
1568
  // Extract tag or component name based on node type
1280
- let tag = isHtmlNode(node) ? node.tag : undefined;
1569
+ const tag = isHtmlNode(node) ? node.tag : undefined;
1281
1570
  const componentName = isComponentNode(node) ? node.component : undefined;
1282
- let nodeProps = isComponentNode(node) ? (node.props || {}) : {};
1571
+ let nodeProps = isComponentNode(node) ? node.props || {} : {};
1283
1572
 
1284
1573
  // Process CMS templates in props
1285
1574
  if (ctx.cmsContext?.cms && Object.keys(nodeProps).length > 0) {
@@ -1302,18 +1591,29 @@ async function renderNode(
1302
1591
 
1303
1592
  // Process CMS templates in attributes (e.g., href="{{cms.link}}")
1304
1593
  if (ctx.cmsContext?.cms && Object.keys(nodeAttributes).length > 0) {
1305
- nodeAttributes = processCMSPropsTemplate(nodeAttributes as Record<string, unknown>, ctx.cmsContext.cms, locale, i18nConfig) as Record<string, string | number | boolean>;
1594
+ nodeAttributes = processCMSPropsTemplate(
1595
+ nodeAttributes as Record<string, unknown>,
1596
+ ctx.cmsContext.cms,
1597
+ locale,
1598
+ i18nConfig,
1599
+ ) as Record<string, string | number | boolean>;
1306
1600
  }
1307
1601
 
1308
1602
  // Process item templates in attributes (for CMSList context)
1309
1603
  // Skip when in templateMode to preserve {{item.field}} placeholders
1310
1604
  if (!ctx.templateMode && templateCtx && Object.keys(nodeAttributes).length > 0) {
1311
- nodeAttributes = processItemPropsTemplate(nodeAttributes, templateCtx, i18nResolver) as Record<string, string | number | boolean>;
1605
+ nodeAttributes = processItemPropsTemplate(nodeAttributes, templateCtx, i18nResolver) as Record<
1606
+ string,
1607
+ string | number | boolean
1608
+ >;
1312
1609
  }
1313
1610
 
1314
1611
  // Auto-skip attributes that were entirely template expressions and resolved to ""
1315
1612
  if (Object.keys(nodeAttributes).length > 0) {
1316
- nodeAttributes = skipEmptyTemplateAttributes(originalAttributes, nodeAttributes) as Record<string, string | number | boolean>;
1613
+ nodeAttributes = skipEmptyTemplateAttributes(originalAttributes, nodeAttributes) as Record<
1614
+ string,
1615
+ string | number | boolean
1616
+ >;
1317
1617
  }
1318
1618
 
1319
1619
  if (!tag && !componentName) return '';
@@ -1327,7 +1627,8 @@ async function renderNode(
1327
1627
  // result. This matches the client (ComponentBuilder.mergeNodeStyles) so that
1328
1628
  // an instance override of e.g. marginBottom actually replaces the structure's
1329
1629
  // marginBottom instead of fighting it as a same-specificity utility class.
1330
- const isCustomComponentNode = nodeType === NODE_TYPE.COMPONENT && isComponentNode(node) && ssrComponentRegistry.has(node.component);
1630
+ const isCustomComponentNode =
1631
+ nodeType === NODE_TYPE.COMPONENT && isComponentNode(node) && ssrComponentRegistry.has(node.component);
1331
1632
  let deferredComponentStyle: StyleObject | ResponsiveStyleObject | undefined;
1332
1633
 
1333
1634
  if (nodeStyle) {
@@ -1343,16 +1644,23 @@ async function renderNode(
1343
1644
  } else if (nodeProps.style) {
1344
1645
  // If no node.style but props have style, keep it for backward compatibility
1345
1646
  if (isResponsiveStyle(nodeProps.style) && breakpoints && viewportWidth) {
1346
- resolvedStyle = mergeResponsiveStyles(nodeProps.style as ResponsiveStyleObject, 'viewport', viewportWidth, breakpoints);
1647
+ resolvedStyle = mergeResponsiveStyles(
1648
+ nodeProps.style as ResponsiveStyleObject,
1649
+ 'viewport',
1650
+ viewportWidth,
1651
+ breakpoints,
1652
+ );
1347
1653
  } else {
1348
1654
  resolvedStyle = nodeProps.style as StyleObject;
1349
1655
  }
1350
1656
  }
1351
1657
 
1352
1658
  // Generate element-specific class if node has interactive styles or generateElementClass flag
1353
- const nodeInteractiveStyles = 'interactiveStyles' in node ? node.interactiveStyles as InteractiveStyles | undefined : undefined;
1354
- const nodeGenerateElementClass = 'generateElementClass' in node ? node.generateElementClass as boolean | undefined : undefined;
1355
- const nodeLabel = 'label' in node ? node.label as string | undefined : undefined;
1659
+ const nodeInteractiveStyles =
1660
+ 'interactiveStyles' in node ? (node.interactiveStyles as InteractiveStyles | undefined) : undefined;
1661
+ const nodeGenerateElementClass =
1662
+ 'generateElementClass' in node ? (node.generateElementClass as boolean | undefined) : undefined;
1663
+ const nodeLabel = 'label' in node ? (node.label as string | undefined) : undefined;
1356
1664
  let elementClass = '';
1357
1665
 
1358
1666
  if ((nodeInteractiveStyles && nodeInteractiveStyles.length > 0) || nodeGenerateElementClass) {
@@ -1387,7 +1695,9 @@ async function renderNode(
1387
1695
  ...(mergedClassName ? { className: mergedClassName } : {}),
1388
1696
  ...(deferredComponentStyle
1389
1697
  ? { style: deferredComponentStyle }
1390
- : Object.keys(resolvedStyle).length > 0 ? { style: resolvedStyle } : {})
1698
+ : Object.keys(resolvedStyle).length > 0
1699
+ ? { style: resolvedStyle }
1700
+ : {}),
1391
1701
  };
1392
1702
 
1393
1703
  // Check if this is a custom component
@@ -1406,13 +1716,11 @@ async function renderNode(
1406
1716
  return renderLinkNode(propsWithStyleAndAttrs, children, ctx);
1407
1717
  }
1408
1718
 
1409
- // Regular HTML element (must have tag)
1410
- if (!tag) {
1411
- console.error('Missing tag for HTML element in SSR');
1412
- return '';
1413
- }
1414
-
1415
- return renderHtmlElement(tag, propsWithStyleAndAttrs, children, ctx);
1719
+ // Regular HTML element. `tag` is necessarily set here: the `!tag &&
1720
+ // !componentName` case returned at the top of this section, and every
1721
+ // `componentName` path has already returned (isComponentNode ⇒ type ===
1722
+ // 'component', which the two component branches above handle exhaustively).
1723
+ return renderHtmlElement(tag as string, propsWithStyleAndAttrs, children, ctx);
1416
1724
  }
1417
1725
 
1418
1726
  /**
@@ -1423,7 +1731,7 @@ async function renderComponent(
1423
1731
  propsWithStyleAndAttrs: Record<string, unknown>,
1424
1732
  children: unknown,
1425
1733
  nodeAttributes: Record<string, unknown>,
1426
- ctx: SSRContext
1734
+ ctx: SSRContext,
1427
1735
  ): Promise<string> {
1428
1736
  const { locale, i18nConfig, pagePath } = ctx;
1429
1737
  const componentDef = ssrComponentRegistry.get(componentName);
@@ -1439,7 +1747,7 @@ async function renderComponent(
1439
1747
  propsWithStyleAndAttrs,
1440
1748
  children as Array<ComponentNode | string> | string | ComponentNode | null | undefined,
1441
1749
  locale,
1442
- i18nConfig
1750
+ i18nConfig,
1443
1751
  );
1444
1752
 
1445
1753
  // Process the structure with resolved props
@@ -1447,7 +1755,9 @@ async function renderComponent(
1447
1755
  // Use preserveResponsiveStyles: true to keep responsive styles intact for SSR rendering
1448
1756
  // Mark children as slot content so they use page context for element class generation
1449
1757
  // Pass parent template context (itemContext) for nested cms-list template resolution
1450
- const markedChildren = children ? markAsSlotContent(children as ComponentNode | ComponentNode[] | string | number | null | undefined) : undefined;
1758
+ const markedChildren = children
1759
+ ? markAsSlotContent(children as ComponentNode | ComponentNode[] | string | number | null | undefined)
1760
+ : undefined;
1451
1761
 
1452
1762
  const processedStructure = processStructure(
1453
1763
  structuredComponentDef.structure,
@@ -1464,20 +1774,28 @@ async function renderComponent(
1464
1774
  },
1465
1775
  undefined,
1466
1776
  markedChildren,
1467
- true
1777
+ true,
1468
1778
  );
1469
1779
 
1470
1780
  if (!processedStructure) return '';
1471
1781
 
1472
1782
  // Type guard: ensure processedStructure is a ComponentNode
1473
- if (typeof processedStructure === 'string' || typeof processedStructure === 'number' || Array.isArray(processedStructure)) {
1783
+ if (
1784
+ typeof processedStructure === 'string' ||
1785
+ typeof processedStructure === 'number' ||
1786
+ Array.isArray(processedStructure)
1787
+ ) {
1474
1788
  return await renderNode(processedStructure as string | number | ComponentNode[], ctx);
1475
1789
  }
1476
1790
 
1477
1791
  // Merge instance style overrides, className, and attributes
1478
1792
  // processedStructure is typically an HTML node (the component's root element)
1479
1793
  // Handle component nodes, HTML nodes, and Link nodes (Link and HTML share top-level style/attributes)
1480
- const rootNode = processedStructure as ComponentNode & { props?: Record<string, unknown>; style?: any; attributes?: Record<string, string | number | boolean> };
1794
+ const rootNode = processedStructure as ComponentNode & {
1795
+ props?: Record<string, unknown>;
1796
+ style?: any;
1797
+ attributes?: Record<string, string | number | boolean>;
1798
+ };
1481
1799
  if (isComponentNode(rootNode) || isHtmlNode(rootNode) || isLinkNode(rootNode)) {
1482
1800
  if (!rootNode.props) {
1483
1801
  rootNode.props = {};
@@ -1499,7 +1817,7 @@ async function renderComponent(
1499
1817
  const existingClass = (rootNode.attributes.class || '') as string;
1500
1818
  rootNode.attributes.class = existingClass
1501
1819
  ? `${existingClass} ${propsWithStyleAndAttrs.className}`
1502
- : propsWithStyleAndAttrs.className as string;
1820
+ : (propsWithStyleAndAttrs.className as string);
1503
1821
  } else {
1504
1822
  const existingClassName = rootNode.props.className || '';
1505
1823
  rootNode.props.className = existingClassName
@@ -1527,9 +1845,7 @@ async function renderComponent(
1527
1845
  // Add defineVars data attributes for JS prop injection
1528
1846
  const defineVars = structuredComponentDef.defineVars;
1529
1847
  if (defineVars && componentName) {
1530
- const varsToExpose = defineVars === true
1531
- ? Object.keys(structuredComponentDef.interface || {})
1532
- : defineVars;
1848
+ const varsToExpose = defineVars === true ? Object.keys(structuredComponentDef.interface || {}) : defineVars;
1533
1849
 
1534
1850
  const propsForJS: Record<string, unknown> = {};
1535
1851
  for (const varName of varsToExpose) {
@@ -1543,18 +1859,21 @@ async function renderComponent(
1543
1859
  if (!processedRoot.attributes) {
1544
1860
  processedRoot.attributes = {};
1545
1861
  }
1546
- const existingComponent = (processedRoot.attributes?.['data-component'] as string)
1547
- || (processedRoot.props?.['data-component'] as string)
1548
- || '';
1862
+ const existingComponent =
1863
+ (processedRoot.attributes?.['data-component'] as string) ||
1864
+ (processedRoot.props?.['data-component'] as string) ||
1865
+ '';
1549
1866
  processedRoot.attributes['data-component'] = existingComponent
1550
1867
  ? `${existingComponent} ${componentName}`
1551
1868
  : componentName;
1552
1869
 
1553
1870
  let existingPropsMap: Record<string, unknown> = {};
1554
- const existingPropsStr = (processedRoot.attributes?.['data-props'] as string)
1555
- || (processedRoot.props?.['data-props'] as string);
1871
+ const existingPropsStr =
1872
+ (processedRoot.attributes?.['data-props'] as string) || (processedRoot.props?.['data-props'] as string);
1556
1873
  if (existingPropsStr) {
1557
- try { existingPropsMap = JSON.parse(existingPropsStr); } catch {}
1874
+ try {
1875
+ existingPropsMap = JSON.parse(existingPropsStr);
1876
+ } catch {}
1558
1877
  }
1559
1878
  existingPropsMap[componentName] = propsForJS;
1560
1879
  processedRoot.attributes['data-props'] = JSON.stringify(existingPropsMap);
@@ -1593,7 +1912,6 @@ async function renderComponent(
1593
1912
  componentRootPath: ctx.elementPath,
1594
1913
  componentResolvedProps: resolvedProps,
1595
1914
  });
1596
-
1597
1915
  } catch (error) {
1598
1916
  return renderNodeFallback(ctx, error, `component ${componentName}`);
1599
1917
  }
@@ -1605,7 +1923,7 @@ async function renderComponent(
1605
1923
  async function renderLinkNode(
1606
1924
  propsWithStyleAndAttrs: Record<string, unknown>,
1607
1925
  children: unknown,
1608
- ctx: SSRContext
1926
+ ctx: SSRContext,
1609
1927
  ): Promise<string> {
1610
1928
  const to = 'to' in propsWithStyleAndAttrs ? propsWithStyleAndAttrs.to : undefined;
1611
1929
  const restProps: Record<string, unknown> = { ...propsWithStyleAndAttrs };
@@ -1614,17 +1932,22 @@ async function renderLinkNode(
1614
1932
  const href = localizeHref(rawHref, ctx);
1615
1933
 
1616
1934
  // Build class attribute from utility classes
1617
- const linkClassAttr = restProps.className
1618
- ? ` class="${escapeHtml(String(restProps.className))}"`
1619
- : '';
1935
+ const linkClassAttr = restProps.className ? ` class="${escapeHtml(String(restProps.className))}"` : '';
1620
1936
 
1621
1937
  const attrs = buildAttributes(restProps, ['style', 'className', 'to']);
1622
1938
  const childrenHTML = Array.isArray(children)
1623
- ? (await Promise.all((children as (ComponentNode | string)[]).map((child, index) => {
1624
- const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
1625
- return renderNode(child, { ...ctx, elementPath: childPath });
1626
- }))).join('')
1627
- : await renderNode(children as ComponentNode, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
1939
+ ? (
1940
+ await Promise.all(
1941
+ (children as (ComponentNode | string)[]).map((child, index) => {
1942
+ const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
1943
+ return renderNode(child, { ...ctx, elementPath: childPath });
1944
+ }),
1945
+ )
1946
+ ).join('')
1947
+ : await renderNode(children as ComponentNode, {
1948
+ ...ctx,
1949
+ elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0],
1950
+ });
1628
1951
 
1629
1952
  return `<a href="${escapeHtml(String(href))}"${linkClassAttr}${attrs}${editorAttrs(ctx)}>${childrenHTML}</a>`;
1630
1953
  }
@@ -1636,7 +1959,7 @@ async function renderHtmlElement(
1636
1959
  tag: string,
1637
1960
  propsWithStyleAndAttrs: Record<string, unknown>,
1638
1961
  children: unknown,
1639
- ctx: SSRContext
1962
+ ctx: SSRContext,
1640
1963
  ): Promise<string> {
1641
1964
  // Build class attribute from utility classes
1642
1965
  let classValue = propsWithStyleAndAttrs.className ? String(propsWithStyleAndAttrs.className) : '';
@@ -1663,9 +1986,7 @@ async function renderHtmlElement(
1663
1986
 
1664
1987
  // For img tags, exclude image-specific props (handled specially in renderImageElement)
1665
1988
  const imageProps = ['src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset', 'fetchpriority'];
1666
- const excludeProps = tag.toLowerCase() === 'img'
1667
- ? ['style', 'className', ...imageProps]
1668
- : ['style', 'className'];
1989
+ const excludeProps = tag.toLowerCase() === 'img' ? ['style', 'className', ...imageProps] : ['style', 'className'];
1669
1990
  const attrs = buildAttributes(propsWithStyleAndAttrs, excludeProps);
1670
1991
  // `<style>` and `<script>` are HTML "raw text" elements: their content is
1671
1992
  // not parsed for entities or child elements. Running text children through
@@ -1682,22 +2003,55 @@ async function renderHtmlElement(
1682
2003
  if (typeof node === 'string') return node;
1683
2004
  if (typeof node === 'number') return String(node);
1684
2005
  if (Array.isArray(node)) return node.map(flatten).join('');
2006
+ // Verbatim-code markers ({ _code, expr }) are silent by design — they
2007
+ // render natively in the Astro build and have nothing to emit in
2008
+ // meno-core SSR (mirrors the renderNode top-level handling).
2009
+ if ((node as { _code?: unknown })._code === true) return '';
2010
+ // Raw-text elements can't contain child elements, so any other object
2011
+ // child is dropped from the output — correct per HTML semantics, but
2012
+ // authored content vanishing should be visible, so surface the drop.
2013
+ logRuntimeError(
2014
+ 'ssrRenderer.renderHtmlElement',
2015
+ new Error(`Element child dropped inside <${tag}> — raw-text elements render only string content`),
2016
+ { tag, droppedNodeType: String((node as { type?: unknown }).type ?? typeof node) },
2017
+ );
1685
2018
  return '';
1686
2019
  };
1687
2020
  childrenHTML = flatten(children as any);
1688
2021
  } else {
1689
2022
  childrenHTML = Array.isArray(children)
1690
- ? (await Promise.all((children as (ComponentNode | string)[]).map((child, index) => {
1691
- const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
1692
- return renderNode(child, { ...ctx, elementPath: childPath });
1693
- }))).join('')
1694
- : await renderNode(children as ComponentNode, { ...ctx, elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0] });
2023
+ ? (
2024
+ await Promise.all(
2025
+ (children as (ComponentNode | string)[]).map((child, index) => {
2026
+ const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
2027
+ return renderNode(child, { ...ctx, elementPath: childPath });
2028
+ }),
2029
+ )
2030
+ ).join('')
2031
+ : await renderNode(children as ComponentNode, {
2032
+ ...ctx,
2033
+ elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0],
2034
+ });
1695
2035
  }
1696
2036
 
1697
2037
  const ea = editorAttrs(ctx);
1698
2038
 
1699
2039
  // Self-closing tags
1700
- const voidElements = ['img', 'input', 'br', 'hr', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'];
2040
+ const voidElements = [
2041
+ 'img',
2042
+ 'input',
2043
+ 'br',
2044
+ 'hr',
2045
+ 'meta',
2046
+ 'link',
2047
+ 'area',
2048
+ 'base',
2049
+ 'col',
2050
+ 'embed',
2051
+ 'source',
2052
+ 'track',
2053
+ 'wbr',
2054
+ ];
1701
2055
  if (voidElements.includes(tag.toLowerCase())) {
1702
2056
  // Special handling for img tags - inject srcset and render as <picture> when AVIF available
1703
2057
  if (tag.toLowerCase() === 'img') {
@@ -1718,7 +2072,7 @@ function renderImageElement(
1718
2072
  classAttr: string,
1719
2073
  styleAttr: string,
1720
2074
  attrs: string,
1721
- ctx: SSRContext
2075
+ ctx: SSRContext,
1722
2076
  ): string {
1723
2077
  const imgProps = propsWithStyleAndAttrs;
1724
2078
  const src = imgProps.src as string | undefined;
@@ -1726,8 +2080,8 @@ function renderImageElement(
1726
2080
  const loading = imgProps.loading as string | undefined;
1727
2081
  const sizes = imgProps.sizes as string | undefined;
1728
2082
  const fetchpriority = imgProps.fetchpriority as string | undefined;
1729
- let width = imgProps.width as string | number | undefined;
1730
- let height = imgProps.height as string | number | undefined;
2083
+ const width = imgProps.width as string | number | undefined;
2084
+ const height = imgProps.height as string | number | undefined;
1731
2085
 
1732
2086
  // Get image metadata from manifest
1733
2087
  const metadata = src ? ctx.imageMetadataMap?.get(src) : undefined;
@@ -1766,14 +2120,23 @@ function renderImageElement(
1766
2120
  if (width !== undefined) imgAttrs += ` width="${escapeHtml(String(width))}"`;
1767
2121
  if (height !== undefined) imgAttrs += ` height="${escapeHtml(String(height))}"`;
1768
2122
 
1769
-
1770
2123
  // Render as <picture> element if AVIF is available and imageFormat is 'avif'
1771
2124
  // Layout classes go on <picture> (block container), image-specific classes on <img>
1772
2125
  if (metadata?.avifSrcset && ctx.imageFormat !== 'webp') {
1773
2126
  // Image-specific class prefixes that should stay on <img>
1774
2127
  const imgClassPrefixes = [
1775
- 'objf-', 'objp-', 'flt-', 'tm-', 'bs-',
1776
- 'br-', 'bt-', 'bb-', 'bl-', 'border-r-', 'bc-', 'b-'
2128
+ 'objf-',
2129
+ 'objp-',
2130
+ 'flt-',
2131
+ 'tm-',
2132
+ 'bs-',
2133
+ 'br-',
2134
+ 'bt-',
2135
+ 'bb-',
2136
+ 'bl-',
2137
+ 'border-r-',
2138
+ 'bc-',
2139
+ 'b-',
1777
2140
  ];
1778
2141
  // Opacity classes (o-NUMBER) go on img, but overflow (o-h, o-a, o-s, o-v) stays on picture
1779
2142
  const opacityPattern = /^o-\d/;
@@ -1785,25 +2148,23 @@ function renderImageElement(
1785
2148
  const pictureClasses: string[] = [];
1786
2149
 
1787
2150
  for (const cls of allClasses) {
1788
- if (imgClassPrefixes.some(prefix => cls.startsWith(prefix)) || opacityPattern.test(cls)) {
2151
+ if (imgClassPrefixes.some((prefix) => cls.startsWith(prefix)) || opacityPattern.test(cls)) {
1789
2152
  imgClasses.push(cls);
1790
2153
  } else {
1791
2154
  pictureClasses.push(cls);
1792
2155
  }
1793
2156
  }
1794
2157
 
1795
- const pictureClassAttr = pictureClasses.length > 0
1796
- ? ` class="${escapeHtml(pictureClasses.join(' '))}"`
1797
- : '';
1798
- const imgClassAttr = imgClasses.length > 0
1799
- ? ` class="${escapeHtml(imgClasses.join(' '))}"`
1800
- : '';
2158
+ const pictureClassAttr = pictureClasses.length > 0 ? ` class="${escapeHtml(pictureClasses.join(' '))}"` : '';
2159
+ const imgClassAttr = imgClasses.length > 0 ? ` class="${escapeHtml(imgClasses.join(' '))}"` : '';
1801
2160
 
1802
- return `<picture${pictureClassAttr}${styleAttr}>` +
2161
+ return (
2162
+ `<picture${pictureClassAttr}${styleAttr}>` +
1803
2163
  `<source type="image/avif" srcset="${escapeHtml(metadata.avifSrcset)}" sizes="${escapeHtml(sizesAttr)}" />` +
1804
2164
  `<source type="image/webp" srcset="${escapeHtml(metadata.srcset)}" sizes="${escapeHtml(sizesAttr)}" />` +
1805
2165
  `<img${imgClassAttr}${imgAttrs}${attrs} />` +
1806
- `</picture>`;
2166
+ `</picture>`
2167
+ );
1807
2168
  }
1808
2169
 
1809
2170
  // Fallback: regular img with WebP srcset
@@ -1839,8 +2200,7 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
1839
2200
  }
1840
2201
 
1841
2202
  const localeStyleOpts = {
1842
- fluidActive:
1843
- ctx.responsiveScales?.enabled === true && ctx.responsiveScales?.mode === 'fluid',
2203
+ fluidActive: ctx.responsiveScales?.enabled === true && ctx.responsiveScales?.mode === 'fluid',
1844
2204
  responsiveScales: ctx.responsiveScales,
1845
2205
  };
1846
2206
 
@@ -1866,18 +2226,14 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
1866
2226
  localeListStyleAttr = buildCssVariableStyleAttr(localeListCssVariables);
1867
2227
  }
1868
2228
 
1869
- const containerClassAttr = containerClasses.length > 0
1870
- ? ` class="${escapeHtml(containerClasses.join(' '))}"`
1871
- : '';
2229
+ const containerClassAttr = containerClasses.length > 0 ? ` class="${escapeHtml(containerClasses.join(' '))}"` : '';
1872
2230
 
1873
2231
  // Convert item styles to utility classes
1874
2232
  let itemClasses: string[] = [];
1875
2233
  if (node.itemStyle) {
1876
2234
  itemClasses = responsiveStylesToClasses(node.itemStyle as ResponsiveStyleObject, localeStyleOpts);
1877
2235
  }
1878
- const itemClassAttr = itemClasses.length > 0
1879
- ? ` class="${escapeHtml(itemClasses.join(' '))}"`
1880
- : '';
2236
+ const itemClassAttr = itemClasses.length > 0 ? ` class="${escapeHtml(itemClasses.join(' '))}"` : '';
1881
2237
 
1882
2238
  // Convert active item styles to utility classes
1883
2239
  let activeItemClasses: string[] = [];
@@ -1890,24 +2246,19 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
1890
2246
  if (node.separatorStyle) {
1891
2247
  separatorClasses = responsiveStylesToClasses(node.separatorStyle as ResponsiveStyleObject, localeStyleOpts);
1892
2248
  }
1893
- const separatorClassAttr = separatorClasses.length > 0
1894
- ? ` class="${escapeHtml(separatorClasses.join(' '))}"`
1895
- : '';
2249
+ const separatorClassAttr = separatorClasses.length > 0 ? ` class="${escapeHtml(separatorClasses.join(' '))}"` : '';
1896
2250
 
1897
2251
  // Convert flag styles to utility classes
1898
2252
  let flagClasses: string[] = [];
1899
2253
  if (node.flagStyle) {
1900
2254
  flagClasses = responsiveStylesToClasses(node.flagStyle as ResponsiveStyleObject, localeStyleOpts);
1901
2255
  }
1902
- const flagClassAttr = flagClasses.length > 0
1903
- ? ` class="${escapeHtml(flagClasses.join(' '))}"`
1904
- : '';
2256
+ const flagClassAttr = flagClasses.length > 0 ? ` class="${escapeHtml(flagClasses.join(' '))}"` : '';
1905
2257
 
1906
2258
  // Current item gets both itemStyle + activeItemStyle (additive/override)
1907
2259
  const currentItemClasses = [...itemClasses, ...activeItemClasses];
1908
- const currentItemClassAttr = currentItemClasses.length > 0
1909
- ? ` class="${escapeHtml(currentItemClasses.join(' '))}"`
1910
- : '';
2260
+ const currentItemClassAttr =
2261
+ currentItemClasses.length > 0 ? ` class="${escapeHtml(currentItemClasses.join(' '))}"` : '';
1911
2262
 
1912
2263
  // Build links HTML
1913
2264
  const links: string[] = [];
@@ -1940,13 +2291,13 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
1940
2291
  }
1941
2292
  linkContent += `<div>${escapeHtml(displayText)}</div>`;
1942
2293
 
1943
- links.push(`<a href="${escapeHtml(link.path)}"${hreflangAttr}${currentAttr} data-locale="${escapeHtml(link.locale)}"${classAttrForLink}>${linkContent}</a>`);
2294
+ links.push(
2295
+ `<a href="${escapeHtml(link.path)}"${hreflangAttr}${currentAttr} data-locale="${escapeHtml(link.locale)}"${classAttrForLink}>${linkContent}</a>`,
2296
+ );
1944
2297
  }
1945
2298
 
1946
2299
  // Join links with separator (empty span styled via separatorStyle) or concatenate directly
1947
- const linksHTML = showSeparator
1948
- ? links.join(`<span${separatorClassAttr}></span>`)
1949
- : links.join('');
2300
+ const linksHTML = showSeparator ? links.join(`<span${separatorClassAttr}></span>`) : links.join('');
1950
2301
 
1951
2302
  // Extract attributes from node
1952
2303
  const nodeAttributes = resolveI18nAttrs(extractAttributesFromNode(node), locale, i18nConfig);
@@ -1979,8 +2330,20 @@ export async function renderPageSSR(
1979
2330
  cmsContext?: CMSContext,
1980
2331
  cmsService?: CMSService,
1981
2332
  isProductionBuild?: boolean,
1982
- injectEditorAttrs?: boolean
1983
- ): Promise<{ html: string; meta: string; title: string; javascript: string; componentCSS?: string; locale: string; interactiveStylesMap: Map<string, InteractiveStyles>; preloadImages: PreloadImage[]; neededCollections: Set<string>; ssrFallbackCollector: Map<string, string>; processedRawHtmlCollector: Map<string, string> }> {
2333
+ injectEditorAttrs?: boolean,
2334
+ ): Promise<{
2335
+ html: string;
2336
+ meta: string;
2337
+ title: string;
2338
+ javascript: string;
2339
+ componentCSS?: string;
2340
+ locale: string;
2341
+ interactiveStylesMap: Map<string, InteractiveStyles>;
2342
+ preloadImages: PreloadImage[];
2343
+ neededCollections: Set<string>;
2344
+ ssrFallbackCollector: Map<string, string>;
2345
+ processedRawHtmlCollector: Map<string, string>;
2346
+ }> {
1984
2347
  // Extract page content
1985
2348
  const rootNode = pageData?.root || undefined;
1986
2349
  if (!rootNode) {
@@ -1988,7 +2351,7 @@ export async function renderPageSSR(
1988
2351
  }
1989
2352
 
1990
2353
  // Load i18n config if not provided
1991
- const config = i18nConfig || await loadI18nConfig();
2354
+ const config = i18nConfig || (await loadI18nConfig());
1992
2355
 
1993
2356
  // Extract locale from path if not explicitly provided
1994
2357
  const { locale: pathLocale, pathWithoutLocale } = extractLocaleFromPath(pagePath, config);
@@ -2008,7 +2371,10 @@ export async function renderPageSSR(
2008
2371
  meta = { ...meta, ogTitle: processCMSTemplate(meta.ogTitle, cmsContext.cms, effectiveLocale, config) };
2009
2372
  }
2010
2373
  if (typeof meta.ogDescription === 'string') {
2011
- meta = { ...meta, ogDescription: processCMSTemplate(meta.ogDescription, cmsContext.cms, effectiveLocale, config) };
2374
+ meta = {
2375
+ ...meta,
2376
+ ogDescription: processCMSTemplate(meta.ogDescription, cmsContext.cms, effectiveLocale, config),
2377
+ };
2012
2378
  }
2013
2379
  if (typeof meta.ogImage === 'string') {
2014
2380
  meta = { ...meta, ogImage: processCMSTemplate(meta.ogImage, cmsContext.cms, effectiveLocale, config) };
@@ -2019,9 +2385,27 @@ export async function renderPageSSR(
2019
2385
 
2020
2386
  // Render the component tree to HTML with i18n and CMS support
2021
2387
  // Also collect interactive styles, preload images, and needed collections during render
2022
- const { html: contentHTML, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector } = rootNode
2023
- ? await buildComponentHTML(rootNode, globalComponents, pageComponents, effectiveLocale, config, slugMappings, pagePath, cmsContext, cmsService, isProductionBuild, injectEditorAttrs)
2024
- : { html: '', interactiveStylesMap: new Map<string, InteractiveStyles>(), preloadImages: [], neededCollections: new Set<string>(), ssrFallbackCollector: new Map<string, string>(), processedRawHtmlCollector: new Map<string, string>() };
2388
+ // (rootNode is guaranteed non-null the guard at the top of this function throws otherwise)
2389
+ const {
2390
+ html: contentHTML,
2391
+ interactiveStylesMap,
2392
+ preloadImages,
2393
+ neededCollections,
2394
+ ssrFallbackCollector,
2395
+ processedRawHtmlCollector,
2396
+ } = await buildComponentHTML(
2397
+ rootNode,
2398
+ globalComponents,
2399
+ pageComponents,
2400
+ effectiveLocale,
2401
+ config,
2402
+ slugMappings,
2403
+ pagePath,
2404
+ cmsContext,
2405
+ cmsService,
2406
+ isProductionBuild,
2407
+ injectEditorAttrs,
2408
+ );
2025
2409
 
2026
2410
  // Collect JavaScript and CSS from all components
2027
2411
  const javascript = await collectComponentJavaScript(globalComponents, pageComponents);