meno-core 1.0.53 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (415) hide show
  1. package/.claude/settings.local.json +1 -3
  2. package/bin/cli.ts +66 -261
  3. package/build-astro.ts +150 -128
  4. package/dist/bin/cli.js +42 -185
  5. package/dist/bin/cli.js.map +2 -2
  6. package/dist/chunks/chunk-2AR55GYH.js +42 -0
  7. package/dist/chunks/chunk-2AR55GYH.js.map +7 -0
  8. package/dist/chunks/{chunk-JGWFTO6P.js → chunk-2IIQK7T3.js} +2624 -1389
  9. package/dist/chunks/chunk-2IIQK7T3.js.map +7 -0
  10. package/dist/chunks/chunk-4ZRU52J2.js +169 -0
  11. package/dist/chunks/chunk-4ZRU52J2.js.map +7 -0
  12. package/dist/chunks/{chunk-UB44F4Z2.js → chunk-5X4HCN7J.js} +5 -7
  13. package/dist/chunks/chunk-5X4HCN7J.js.map +7 -0
  14. package/dist/chunks/{chunk-WQFG7PAH.js → chunk-7CCVOL5M.js} +15 -19
  15. package/dist/chunks/chunk-7CCVOL5M.js.map +7 -0
  16. package/dist/chunks/{chunk-YBLHKYFF.js → chunk-AR7LHIFY.js} +8 -2
  17. package/dist/chunks/chunk-AR7LHIFY.js.map +7 -0
  18. package/dist/chunks/{chunk-O3NAGJP4.js → chunk-NUP7H7D3.js} +135 -41
  19. package/dist/chunks/chunk-NUP7H7D3.js.map +7 -0
  20. package/dist/chunks/{chunk-X754AHS5.js → chunk-QWTQZHG3.js} +194 -90
  21. package/dist/chunks/chunk-QWTQZHG3.js.map +7 -0
  22. package/dist/chunks/{chunk-H3GJ4H2U.js → chunk-XTKNX4FW.js} +216 -87
  23. package/dist/chunks/chunk-XTKNX4FW.js.map +7 -0
  24. package/dist/chunks/{fs-JGINUXGL.js → fs-G3ANWEFP.js} +2 -2
  25. package/dist/lib/client/index.js +1003 -475
  26. package/dist/lib/client/index.js.map +3 -3
  27. package/dist/lib/server/index.js +14393 -6371
  28. package/dist/lib/server/index.js.map +4 -4
  29. package/dist/lib/shared/index.js +342 -101
  30. package/dist/lib/shared/index.js.map +4 -4
  31. package/dist/lib/shared/richtext/index.js +1 -1
  32. package/dist/lib/test-utils/index.js +64 -80
  33. package/dist/lib/test-utils/index.js.map +2 -2
  34. package/entries/client-router.tsx +10 -8
  35. package/lib/client/ClientInitializer.ts +8 -8
  36. package/lib/client/ErrorBoundary.test.tsx +246 -186
  37. package/lib/client/ErrorBoundary.tsx +183 -132
  38. package/lib/client/componentRegistry.test.ts +96 -108
  39. package/lib/client/componentRegistry.ts +1 -3
  40. package/lib/client/contexts/ThemeContext.tsx +3 -2
  41. package/lib/client/core/ComponentBuilder.test.ts +663 -560
  42. package/lib/client/core/ComponentBuilder.ts +427 -155
  43. package/lib/client/core/ComponentRenderer.test.tsx +2 -4
  44. package/lib/client/core/ComponentRenderer.tsx +46 -33
  45. package/lib/client/core/builders/embedBuilder.ts +242 -54
  46. package/lib/client/core/builders/linkBuilder.ts +72 -45
  47. package/lib/client/core/builders/linkNodeBuilder.ts +79 -54
  48. package/lib/client/core/builders/listBuilder.ts +155 -89
  49. package/lib/client/core/builders/localeListBuilder.ts +97 -62
  50. package/lib/client/core/builders/types.ts +5 -5
  51. package/lib/client/core/cmsTemplateProcessor.ts +8 -9
  52. package/lib/client/elementRegistry.ts +3 -3
  53. package/lib/client/fontFamiliesService.test.ts +2 -10
  54. package/lib/client/fontFamiliesService.ts +3 -3
  55. package/lib/client/hmr/HMRManager.tsx +47 -30
  56. package/lib/client/hmrCssReload.ts +49 -14
  57. package/lib/client/hmrWebSocket.ts +14 -19
  58. package/lib/client/hooks/useColorVariables.test.ts +21 -21
  59. package/lib/client/hooks/useColorVariables.ts +23 -18
  60. package/lib/client/hooks/usePropertyAutocomplete.ts +4 -6
  61. package/lib/client/hooks/useVariables.ts +6 -4
  62. package/lib/client/hydration/HydrationUtils.test.ts +24 -25
  63. package/lib/client/hydration/HydrationUtils.ts +3 -4
  64. package/lib/client/i18nConfigService.test.ts +2 -7
  65. package/lib/client/i18nConfigService.ts +2 -2
  66. package/lib/client/meno-filter/MenoFilter.test.ts +47 -49
  67. package/lib/client/meno-filter/MenoFilter.ts +14 -15
  68. package/lib/client/meno-filter/bindings.ts +18 -43
  69. package/lib/client/meno-filter/init.ts +4 -4
  70. package/lib/client/meno-filter/renderer.ts +30 -32
  71. package/lib/client/meno-filter/script.generated.ts +1 -3
  72. package/lib/client/meno-filter/ui.ts +4 -6
  73. package/lib/client/meno-filter/updates.ts +18 -23
  74. package/lib/client/meno-filter/utils.ts +1 -1
  75. package/lib/client/navigation.test.ts +174 -178
  76. package/lib/client/navigation.ts +0 -1
  77. package/lib/client/responsiveStyleResolver.knownTokens.test.ts +71 -0
  78. package/lib/client/responsiveStyleResolver.test.ts +234 -232
  79. package/lib/client/responsiveStyleResolver.ts +74 -20
  80. package/lib/client/routing/RouteLoader.test.ts +24 -25
  81. package/lib/client/routing/RouteLoader.ts +27 -35
  82. package/lib/client/routing/Router.tsx +210 -153
  83. package/lib/client/scripts/ScriptExecutor.test.ts +157 -158
  84. package/lib/client/scripts/ScriptExecutor.ts +18 -36
  85. package/lib/client/services/PrefetchService.test.ts +3 -3
  86. package/lib/client/services/PrefetchService.ts +23 -29
  87. package/lib/client/styleProcessor.test.ts +9 -9
  88. package/lib/client/styleProcessor.ts +24 -15
  89. package/lib/client/styles/StyleInjector.test.ts +122 -115
  90. package/lib/client/styles/StyleInjector.ts +10 -7
  91. package/lib/client/styles/UtilityClassCollector.ts +62 -28
  92. package/lib/client/styles/cspNonce.test.ts +2 -5
  93. package/lib/client/templateEngine.test.ts +554 -488
  94. package/lib/client/templateEngine.ts +203 -73
  95. package/lib/client/theme.test.ts +1 -1
  96. package/lib/client/theme.ts +0 -1
  97. package/lib/client/utils/toast.ts +0 -1
  98. package/lib/server/__integration__/api-routes.test.ts +8 -4
  99. package/lib/server/__integration__/cms-integration.test.ts +1 -4
  100. package/lib/server/__integration__/server-lifecycle.test.ts +4 -5
  101. package/lib/server/__integration__/ssr-rendering.test.ts +47 -37
  102. package/lib/server/__integration__/static-assets.test.ts +3 -3
  103. package/lib/server/__integration__/test-helpers.ts +87 -74
  104. package/lib/server/ab/generateFunctions.ts +12 -10
  105. package/lib/server/astro/cmsPageEmitter.ts +26 -42
  106. package/lib/server/astro/componentEmitter.ts +35 -43
  107. package/lib/server/astro/cssCollector.ts +10 -26
  108. package/lib/server/astro/nodeToAstro.test.ts +1754 -34
  109. package/lib/server/astro/nodeToAstro.ts +232 -216
  110. package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +115 -119
  111. package/lib/server/astro/normalizeOrphanTemplateProps.ts +11 -18
  112. package/lib/server/astro/pageEmitter.ts +9 -13
  113. package/lib/server/astro/tailwindMapper.test.ts +10 -37
  114. package/lib/server/astro/tailwindMapper.ts +33 -40
  115. package/lib/server/astro/templateTransformer.ts +17 -20
  116. package/lib/server/createServer.ts +7 -19
  117. package/lib/server/cssGenerator.test.ts +35 -44
  118. package/lib/server/cssGenerator.ts +8 -19
  119. package/lib/server/fileWatcher.test.ts +164 -13
  120. package/lib/server/fileWatcher.ts +209 -124
  121. package/lib/server/index.ts +14 -17
  122. package/lib/server/jsonLoader.test.ts +39 -2
  123. package/lib/server/jsonLoader.ts +39 -34
  124. package/lib/server/middleware/cors.test.ts +21 -21
  125. package/lib/server/middleware/cors.ts +28 -4
  126. package/lib/server/middleware/errorHandler.test.ts +7 -5
  127. package/lib/server/middleware/errorHandler.ts +3 -8
  128. package/lib/server/middleware/index.ts +0 -1
  129. package/lib/server/middleware/logger.test.ts +10 -8
  130. package/lib/server/middleware/logger.ts +17 -29
  131. package/lib/server/migrateTemplates.ts +3 -3
  132. package/lib/server/pageCache.test.ts +76 -77
  133. package/lib/server/pageCache.ts +0 -1
  134. package/lib/server/projectContext.ts +5 -4
  135. package/lib/server/providers/fileSystemCMSProvider.test.ts +142 -99
  136. package/lib/server/providers/fileSystemCMSProvider.ts +62 -38
  137. package/lib/server/routes/api/cms.test.ts +26 -14
  138. package/lib/server/routes/api/cms.ts +9 -14
  139. package/lib/server/routes/api/colors.test.ts +2 -2
  140. package/lib/server/routes/api/components.ts +31 -35
  141. package/lib/server/routes/api/config.ts +3 -4
  142. package/lib/server/routes/api/core-routes.ts +49 -76
  143. package/lib/server/routes/api/enums.test.ts +2 -2
  144. package/lib/server/routes/api/functions.ts +9 -17
  145. package/lib/server/routes/api/index.ts +3 -6
  146. package/lib/server/routes/api/pages.ts +8 -11
  147. package/lib/server/routes/api/shared.test.ts +1 -1
  148. package/lib/server/routes/api/shared.ts +12 -9
  149. package/lib/server/routes/api/variables.test.ts +3 -5
  150. package/lib/server/routes/api/variables.ts +1 -1
  151. package/lib/server/routes/index.ts +25 -28
  152. package/lib/server/routes/pages.ts +35 -31
  153. package/lib/server/routes/static.ts +17 -6
  154. package/lib/server/runtime/bundler.ts +98 -61
  155. package/lib/server/runtime/fs.ts +16 -26
  156. package/lib/server/runtime/httpServer.ts +8 -12
  157. package/lib/server/services/ColorService.test.ts +3 -3
  158. package/lib/server/services/ColorService.ts +37 -29
  159. package/lib/server/services/EnumService.test.ts +4 -8
  160. package/lib/server/services/EnumService.ts +11 -3
  161. package/lib/server/services/VariableService.test.ts +8 -12
  162. package/lib/server/services/VariableService.ts +11 -3
  163. package/lib/server/services/cmsService.test.ts +158 -120
  164. package/lib/server/services/cmsService.ts +36 -146
  165. package/lib/server/services/componentService.test.ts +125 -45
  166. package/lib/server/services/componentService.ts +70 -46
  167. package/lib/server/services/configService.test.ts +22 -45
  168. package/lib/server/services/configService.ts +50 -27
  169. package/lib/server/services/fileWatcherService.ts +42 -8
  170. package/lib/server/services/index.ts +0 -1
  171. package/lib/server/services/pageService.test.ts +8 -10
  172. package/lib/server/services/pageService.ts +32 -18
  173. package/lib/server/ssr/attributeBuilder.ts +18 -8
  174. package/lib/server/ssr/buildErrorOverlay.ts +11 -12
  175. package/lib/server/ssr/clientDataInjector.ts +7 -21
  176. package/lib/server/ssr/cmsSSRProcessor.ts +4 -8
  177. package/lib/server/ssr/cssCollector.ts +1 -1
  178. package/lib/server/ssr/errorOverlay.test.ts +1 -1
  179. package/lib/server/ssr/errorOverlay.ts +3 -9
  180. package/lib/server/ssr/htmlGenerator.nonce.test.ts +9 -14
  181. package/lib/server/ssr/htmlGenerator.test.ts +120 -44
  182. package/lib/server/ssr/htmlGenerator.ts +126 -91
  183. package/lib/server/ssr/imageMetadata.test.ts +3 -1
  184. package/lib/server/ssr/imageMetadata.ts +27 -21
  185. package/lib/server/ssr/jsCollector.test.ts +8 -18
  186. package/lib/server/ssr/jsCollector.ts +3 -8
  187. package/lib/server/ssr/liveReloadIntegration.test.ts +76 -36
  188. package/lib/server/ssr/metaTagGenerator.ts +2 -2
  189. package/lib/server/ssr/ssrRenderer.branches.test.ts +1103 -0
  190. package/lib/server/ssr/ssrRenderer.test.ts +316 -256
  191. package/lib/server/ssr/ssrRenderer.ts +795 -243
  192. package/lib/server/ssrRenderer.test.ts +1048 -954
  193. package/lib/server/utils/jsonLineMapper.test.ts +28 -28
  194. package/lib/server/utils/jsonLineMapper.ts +7 -7
  195. package/lib/server/validateStyleCoverage.ts +18 -21
  196. package/lib/server/websocketManager.ts +28 -19
  197. package/lib/shared/attributeNodeUtils.test.ts +15 -15
  198. package/lib/shared/attributeNodeUtils.ts +5 -12
  199. package/lib/shared/breakpoints.test.ts +5 -5
  200. package/lib/shared/breakpoints.ts +4 -11
  201. package/lib/shared/cmsQuery.test.ts +81 -0
  202. package/lib/shared/cmsQuery.ts +92 -0
  203. package/lib/shared/cmsQueryParser.test.ts +50 -42
  204. package/lib/shared/cmsQueryParser.ts +60 -38
  205. package/lib/shared/colorConversions.ts +9 -9
  206. package/lib/shared/colorVariableUtils.test.ts +27 -5
  207. package/lib/shared/colorVariableUtils.ts +21 -12
  208. package/lib/shared/componentRefs.ts +1 -5
  209. package/lib/shared/constants.test.ts +3 -3
  210. package/lib/shared/constants.ts +8 -8
  211. package/lib/shared/cssGeneration.test.ts +285 -144
  212. package/lib/shared/cssGeneration.ts +217 -531
  213. package/lib/shared/cssNamedColors.ts +152 -30
  214. package/lib/shared/cssProperties.test.ts +69 -70
  215. package/lib/shared/cssProperties.ts +298 -114
  216. package/lib/shared/elementClassName.test.ts +109 -109
  217. package/lib/shared/elementClassName.ts +3 -3
  218. package/lib/shared/elementUtils.ts +12 -16
  219. package/lib/shared/errorLogger.test.ts +12 -12
  220. package/lib/shared/errorLogger.ts +2 -10
  221. package/lib/shared/errors.test.ts +2 -13
  222. package/lib/shared/errors.ts +2 -8
  223. package/lib/shared/expressionEvaluator.test.ts +145 -0
  224. package/lib/shared/expressionEvaluator.ts +52 -24
  225. package/lib/shared/fontLoader.test.ts +32 -18
  226. package/lib/shared/fontLoader.ts +2 -2
  227. package/lib/shared/friendlyError.ts +2 -3
  228. package/lib/shared/gradientUtils.test.ts +3 -7
  229. package/lib/shared/gradientUtils.ts +17 -14
  230. package/lib/shared/hrefRefs.test.ts +2 -2
  231. package/lib/shared/hrefRefs.ts +2 -10
  232. package/lib/shared/i18n.test.ts +4 -4
  233. package/lib/shared/i18n.ts +18 -38
  234. package/lib/shared/index.ts +27 -0
  235. package/lib/shared/inlineSvgStyleRules.ts +14 -13
  236. package/lib/shared/interactiveStyleMappings.test.ts +32 -54
  237. package/lib/shared/interactiveStyleMappings.ts +11 -24
  238. package/lib/shared/interactiveStyles.test.ts +165 -188
  239. package/lib/shared/interfaces/contentProvider.ts +1 -1
  240. package/lib/shared/itemTemplateUtils.test.ts +11 -17
  241. package/lib/shared/itemTemplateUtils.ts +27 -39
  242. package/lib/shared/jsonRepair.ts +16 -10
  243. package/lib/shared/libraryLoader.test.ts +21 -55
  244. package/lib/shared/libraryLoader.ts +7 -22
  245. package/lib/shared/linkUtils.ts +1 -1
  246. package/lib/shared/logger.test.ts +66 -0
  247. package/lib/shared/logger.ts +94 -0
  248. package/lib/shared/markdown.ts +25 -0
  249. package/lib/shared/netlifyLocale404.test.ts +179 -0
  250. package/lib/shared/netlifyLocale404.ts +110 -0
  251. package/lib/shared/nodeUtils.test.ts +28 -16
  252. package/lib/shared/nodeUtils.ts +76 -23
  253. package/lib/shared/pathArrayUtils.test.ts +1 -2
  254. package/lib/shared/pathArrayUtils.ts +1 -1
  255. package/lib/shared/pathSecurity.ts +2 -2
  256. package/lib/shared/pathUtils.test.ts +4 -6
  257. package/lib/shared/pathUtils.ts +42 -49
  258. package/lib/shared/paths/Path.test.ts +2 -2
  259. package/lib/shared/paths/Path.ts +0 -1
  260. package/lib/shared/paths/PathConverter.test.ts +1 -1
  261. package/lib/shared/paths/PathConverter.ts +17 -20
  262. package/lib/shared/paths/PathUtils.ts +13 -13
  263. package/lib/shared/paths/PathValidator.test.ts +2 -15
  264. package/lib/shared/paths/PathValidator.ts +12 -10
  265. package/lib/shared/paths/index.ts +1 -2
  266. package/lib/shared/permissions.test.ts +168 -0
  267. package/lib/shared/permissions.ts +162 -0
  268. package/lib/shared/propResolver.test.ts +240 -244
  269. package/lib/shared/propResolver.ts +15 -26
  270. package/lib/shared/pxToRem.test.ts +7 -6
  271. package/lib/shared/pxToRem.ts +2 -5
  272. package/lib/shared/registry/BaseNodeTypeRegistry.test.ts +12 -6
  273. package/lib/shared/registry/ClientRegistry.ts +0 -2
  274. package/lib/shared/registry/ComponentRegistry.test.ts +46 -32
  275. package/lib/shared/registry/ComponentRegistry.ts +12 -9
  276. package/lib/shared/registry/NodeTypeDefinition.ts +16 -9
  277. package/lib/shared/registry/RegistryManager.ts +1 -2
  278. package/lib/shared/registry/SSRRegistry.ts +0 -2
  279. package/lib/shared/registry/createNodeType.ts +7 -9
  280. package/lib/shared/registry/defineNodeType.ts +2 -6
  281. package/lib/shared/registry/fieldPresets.ts +3 -1
  282. package/lib/shared/registry/index.ts +0 -1
  283. package/lib/shared/registry/nodeTypes/ComponentInstanceNodeType.ts +14 -15
  284. package/lib/shared/registry/nodeTypes/CustomNodeType.ts +103 -0
  285. package/lib/shared/registry/nodeTypes/EmbedNodeType.ts +18 -11
  286. package/lib/shared/registry/nodeTypes/HtmlNodeType.ts +48 -19
  287. package/lib/shared/registry/nodeTypes/IslandNodeType.ts +129 -0
  288. package/lib/shared/registry/nodeTypes/LinkNodeType.ts +21 -19
  289. package/lib/shared/registry/nodeTypes/ListNodeType.ts +117 -77
  290. package/lib/shared/registry/nodeTypes/LocaleListNodeType.ts +27 -21
  291. package/lib/shared/registry/nodeTypes/MarkdownNodeType.ts +78 -0
  292. package/lib/shared/registry/nodeTypes/SlotMarkerType.ts +12 -8
  293. package/lib/shared/registry/nodeTypes/index.ts +26 -3
  294. package/lib/shared/responsiveScaling.test.ts +16 -34
  295. package/lib/shared/responsiveScaling.ts +61 -42
  296. package/lib/shared/responsiveStyleUtils.test.ts +0 -1
  297. package/lib/shared/responsiveStyleUtils.ts +11 -13
  298. package/lib/shared/richtext/htmlToTiptap.test.ts +139 -130
  299. package/lib/shared/richtext/htmlToTiptap.ts +2 -4
  300. package/lib/shared/richtext/tiptapToHtml.test.ts +5 -6
  301. package/lib/shared/richtext/types.ts +1 -8
  302. package/lib/shared/slugTranslator.test.ts +13 -13
  303. package/lib/shared/slugTranslator.ts +12 -16
  304. package/lib/shared/slugify.ts +9 -15
  305. package/lib/shared/styleNodeUtils.test.ts +8 -8
  306. package/lib/shared/styleNodeUtils.ts +18 -20
  307. package/lib/shared/styleUtils.test.ts +87 -61
  308. package/lib/shared/styleUtils.ts +5 -6
  309. package/lib/shared/themeDefaults.test.ts +11 -14
  310. package/lib/shared/themeDefaults.ts +3 -4
  311. package/lib/shared/tree/PathBuilder.test.ts +63 -145
  312. package/lib/shared/tree/PathBuilder.ts +59 -47
  313. package/lib/shared/treePathUtils.test.ts +51 -12
  314. package/lib/shared/treePathUtils.ts +91 -60
  315. package/lib/shared/types/api.ts +105 -8
  316. package/lib/shared/types/cms.test.ts +34 -0
  317. package/lib/shared/types/cms.ts +90 -34
  318. package/lib/shared/types/comment.ts +49 -16
  319. package/lib/shared/types/components.ts +51 -25
  320. package/lib/shared/types/errors.test.ts +1 -6
  321. package/lib/shared/types/errors.ts +3 -7
  322. package/lib/shared/types/experiments.ts +28 -28
  323. package/lib/shared/types/index.ts +25 -2
  324. package/lib/shared/types/permissions.ts +139 -0
  325. package/lib/shared/types/styles.ts +0 -1
  326. package/lib/shared/types/variables.test.ts +4 -13
  327. package/lib/shared/types/variables.ts +51 -30
  328. package/lib/shared/types.ts +1 -2
  329. package/lib/shared/utilityClassConfig.ts +751 -321
  330. package/lib/shared/utilityClassMapper.durability.test.ts +117 -0
  331. package/lib/shared/utilityClassMapper.test.ts +433 -95
  332. package/lib/shared/utilityClassMapper.ts +322 -249
  333. package/lib/shared/utilityClassNames.test.ts +38 -0
  334. package/lib/shared/utilityClassNames.ts +458 -0
  335. package/lib/shared/utils.test.ts +2 -10
  336. package/lib/shared/utils.ts +19 -10
  337. package/lib/shared/validation/cmsValidators.ts +2 -1
  338. package/lib/shared/validation/commentValidators.test.ts +53 -0
  339. package/lib/shared/validation/commentValidators.ts +12 -1
  340. package/lib/shared/validation/index.ts +1 -0
  341. package/lib/shared/validation/permissionsValidators.test.ts +49 -0
  342. package/lib/shared/validation/permissionsValidators.ts +49 -0
  343. package/lib/shared/validation/propValidator.test.ts +19 -21
  344. package/lib/shared/validation/propValidator.ts +13 -18
  345. package/lib/shared/validation/schemas.test.ts +64 -32
  346. package/lib/shared/validation/schemas.ts +616 -345
  347. package/lib/shared/validation/validators.test.ts +3 -8
  348. package/lib/shared/validation/validators.ts +89 -68
  349. package/lib/shared/viewportUnits.integration.test.ts +14 -10
  350. package/lib/shared/viewportUnits.test.ts +24 -23
  351. package/lib/shared/viewportUnits.ts +12 -5
  352. package/lib/test-utils/dom-setup.ts +1 -1
  353. package/lib/test-utils/factories/ConsoleMockFactory.ts +10 -14
  354. package/lib/test-utils/factories/DomMockFactory.ts +17 -23
  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 +6 -10
  358. package/lib/test-utils/factories/StoreMockFactory.ts +18 -29
  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 +13 -19
  365. package/lib/test-utils/mocks.ts +6 -4
  366. package/package.json +3 -5
  367. package/scripts/build-for-publish.mjs +2 -4
  368. package/scripts/build-meno-filter.ts +4 -7
  369. package/tsconfig.json +1 -0
  370. package/vite.config.ts +4 -4
  371. package/build-next.ts +0 -1361
  372. package/build-static.test.ts +0 -424
  373. package/build-static.ts +0 -1074
  374. package/dist/build-static.js +0 -36
  375. package/dist/chunks/chunk-2MHDV5BF.js +0 -251
  376. package/dist/chunks/chunk-2MHDV5BF.js.map +0 -7
  377. package/dist/chunks/chunk-AZQYF6KE.js +0 -559
  378. package/dist/chunks/chunk-AZQYF6KE.js.map +0 -7
  379. package/dist/chunks/chunk-GZHGVVW3.js +0 -322
  380. package/dist/chunks/chunk-GZHGVVW3.js.map +0 -7
  381. package/dist/chunks/chunk-H3GJ4H2U.js.map +0 -7
  382. package/dist/chunks/chunk-I7YIGZXT.js +0 -467
  383. package/dist/chunks/chunk-I7YIGZXT.js.map +0 -7
  384. package/dist/chunks/chunk-IGYR22T6.js +0 -6192
  385. package/dist/chunks/chunk-IGYR22T6.js.map +0 -7
  386. package/dist/chunks/chunk-JGP5A3Y5.js +0 -1136
  387. package/dist/chunks/chunk-JGP5A3Y5.js.map +0 -7
  388. package/dist/chunks/chunk-JGWFTO6P.js.map +0 -7
  389. package/dist/chunks/chunk-O3NAGJP4.js.map +0 -7
  390. package/dist/chunks/chunk-QB2LNO4W.js +0 -77
  391. package/dist/chunks/chunk-QB2LNO4W.js.map +0 -7
  392. package/dist/chunks/chunk-R6XHAFBF.js +0 -3821
  393. package/dist/chunks/chunk-R6XHAFBF.js.map +0 -7
  394. package/dist/chunks/chunk-UB44F4Z2.js.map +0 -7
  395. package/dist/chunks/chunk-WQFG7PAH.js.map +0 -7
  396. package/dist/chunks/chunk-X754AHS5.js.map +0 -7
  397. package/dist/chunks/chunk-YBLHKYFF.js.map +0 -7
  398. package/dist/chunks/configService-R3OGU2UD.js +0 -13
  399. package/dist/chunks/configService-R3OGU2UD.js.map +0 -7
  400. package/dist/chunks/constants-STK2YBIW.js +0 -46
  401. package/dist/chunks/constants-STK2YBIW.js.map +0 -7
  402. package/dist/chunks/fs-JGINUXGL.js.map +0 -7
  403. package/dist/entries/server-router.js +0 -67
  404. package/dist/entries/server-router.js.map +0 -7
  405. package/entries/server-router.tsx +0 -81
  406. package/lib/server/providers/fileSystemPageProvider.ts +0 -160
  407. package/lib/server/webflow/buildWebflow.ts +0 -635
  408. package/lib/server/webflow/index.ts +0 -23
  409. package/lib/server/webflow/nodeToWebflow.test.ts +0 -3170
  410. package/lib/server/webflow/nodeToWebflow.ts +0 -2435
  411. package/lib/server/webflow/styleMapper.test.ts +0 -389
  412. package/lib/server/webflow/styleMapper.ts +0 -695
  413. package/lib/server/webflow/templateWrapper.ts +0 -49
  414. package/lib/server/webflow/types.ts +0 -396
  415. /package/dist/{build-static.js.map → chunks/fs-G3ANWEFP.js.map} +0 -0
@@ -3,21 +3,51 @@
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 { ComponentNode, ComponentDefinition, JSONPage, CMSItem, CMSFilterCondition } from '../../shared/types';
7
7
  import type { TemplateContext, NestedCMSListConfig } from '../../shared/types/cms';
8
- import { processItemTemplate, processItemPropsTemplate, hasItemTemplates, buildTemplateContext, resolveItemsTemplate, resolveTemplateRawValue, addItemUrl, getNestedValue, type ValueResolver } from '../../shared/itemTemplateUtils';
8
+ import {
9
+ processItemTemplate,
10
+ processItemPropsTemplate,
11
+ hasItemTemplates,
12
+ buildTemplateContext,
13
+ resolveItemsTemplate,
14
+ resolveTemplateRawValue,
15
+ addItemUrl,
16
+ getNestedValue,
17
+ type ValueResolver,
18
+ } from '../../shared/itemTemplateUtils';
9
19
  import { singularize, isItemDraftForLocale } from '../../shared/types/cms';
20
+ import { applyCmsFilters, applyCmsSorting } from '../../shared/cmsQuery';
10
21
  import type { ResponsiveStyleObject, StyleObject } from '../../shared/types';
11
22
  import type { BreakpointConfig } from '../../shared/breakpoints';
12
23
  import type { ResponsiveScales } from '../../shared/responsiveScaling';
13
- import { evaluateTemplate, processStructure, isResponsiveStyle, isHtmlMapping, resolveHtmlMapping } from '../../client/templateEngine';
14
- import { resolvePropsFromDefinition, isRichTextMarker, richTextMarkerToHtml } from '../../shared/propResolver';
24
+ import { processStructure, isResponsiveStyle, isHtmlMapping, resolveHtmlMapping } from '../../client/templateEngine';
25
+ import { resolvePropsFromDefinition } from '../../shared/propResolver';
15
26
  import { loadBreakpointConfig, loadI18nConfig } from '../jsonLoader';
16
27
  import { configService } from '../services/configService';
17
28
  import type { I18nConfig } from '../../shared/types/components';
18
- import { extractLocaleFromPath, DEFAULT_I18N_CONFIG, resolveI18nValue, buildLocalizedPath, isI18nValue } from '../../shared/i18n';
29
+ import {
30
+ extractLocaleFromPath,
31
+ DEFAULT_I18N_CONFIG,
32
+ resolveI18nValue,
33
+ buildLocalizedPath,
34
+ isI18nValue,
35
+ } from '../../shared/i18n';
19
36
  import { NODE_TYPE } from '../../shared/constants';
20
- import { isComponentNode, isHtmlNode, isLinkNode, isEmbedNode, isLocaleListNode, isListNode, markAsSlotContent, isBooleanMapping, hasIf, isSlotContent, hasChildren } from '../../shared/nodeUtils';
37
+ import {
38
+ isComponentNode,
39
+ isHtmlNode,
40
+ isLinkNode,
41
+ isEmbedNode,
42
+ isLocaleListNode,
43
+ isListNode,
44
+ isIslandNode,
45
+ isCustomNode,
46
+ markAsSlotContent,
47
+ isBooleanMapping,
48
+ hasIf,
49
+ isSlotContent,
50
+ } from '../../shared/nodeUtils';
21
51
  import type { ListNode } from '../../shared/registry/nodeTypes/ListNodeType';
22
52
  import type { CMSService } from '../services/cmsService';
23
53
  import { extractAttributesFromNode, skipEmptyTemplateAttributes } from '../../shared/attributeNodeUtils';
@@ -28,14 +58,18 @@ import { mergeResponsiveStyles } from '../../shared/responsiveStyleUtils';
28
58
  // Lazy-loaded: jsdom (used by isomorphic-dompurify on the server) can't be bundled by esbuild
29
59
  // because it reads CSS files relative to __dirname at init time. When the module is unavailable
30
60
  // (packaged Electron app), embed HTML is returned unsanitized — acceptable since content is local.
31
- let _DOMPurify: any = null;
61
+ /** The one DOMPurify method the renderer calls. */
62
+ interface DOMPurifyLike {
63
+ sanitize: (html: string, opts?: Record<string, unknown>) => string;
64
+ }
65
+ let _DOMPurify: DOMPurifyLike | null = null;
32
66
  let _DOMPurifyLoaded = false;
33
- function getDOMPurify() {
67
+ function getDOMPurify(): DOMPurifyLike | null {
34
68
  if (!_DOMPurifyLoaded) {
35
69
  _DOMPurifyLoaded = true;
36
70
  try {
37
- _DOMPurify = require('isomorphic-dompurify');
38
- if (_DOMPurify.default) _DOMPurify = _DOMPurify.default;
71
+ const mod = require('isomorphic-dompurify') as DOMPurifyLike & { default?: DOMPurifyLike };
72
+ _DOMPurify = mod.default ?? mod;
39
73
  } catch {
40
74
  // Not available in bundled environment
41
75
  }
@@ -46,20 +80,31 @@ import { responsiveStylesToClasses } from '../../shared/utilityClassMapper';
46
80
  import { validateStyleCoverage } from '../validateStyleCoverage';
47
81
  import { generateElementClassName, type ElementClassContext } from '../../shared/elementClassName';
48
82
  import type { InteractiveStyles } from '../../shared/types/styles';
49
- import { extractInteractiveStyleMappings, resolveExtractedMappings, hasInteractiveStyleMappings } from '../../shared/interactiveStyleMappings';
83
+ import {
84
+ extractInteractiveStyleMappings,
85
+ resolveExtractedMappings,
86
+ hasInteractiveStyleMappings,
87
+ } from '../../shared/interactiveStyleMappings';
50
88
  import { isCurrentLink } from '../../shared/linkUtils';
51
89
  import type { SlugMap } from '../../shared/slugTranslator';
52
90
  import { buildSlugIndex, getLocaleLinks, translatePath } from '../../shared/slugTranslator';
53
91
 
54
92
  // Import from modularized files
55
93
  import { escapeHtml, buildAttributes, buildEditorAttrs, styleToString } from './attributeBuilder';
94
+ import { logRuntimeError } from '../../shared/errorLogger';
56
95
  import { toFriendlyError } from '../../shared/friendlyError';
57
96
  import { rewriteViewportUnits } from '../../shared/viewportUnits';
58
97
  import { extractPageMeta, generateMetaTags } from './metaTagGenerator';
59
98
  import { collectComponentCSS } from './cssCollector';
60
99
  import { collectComponentJavaScript } from './jsCollector';
61
- import { CMSContext, processCMSTemplate, processCMSPropsTemplate, createI18nResolver, RAW_HTML_PREFIX } from './cmsSSRProcessor';
62
- import { ImageMetadataMap, DEFAULT_SIZES, buildImageMetadataMap, rewriteRichTextImages } from './imageMetadata';
100
+ import {
101
+ type CMSContext,
102
+ processCMSTemplate,
103
+ processCMSPropsTemplate,
104
+ createI18nResolver,
105
+ RAW_HTML_PREFIX,
106
+ } from './cmsSSRProcessor';
107
+ import { type ImageMetadataMap, DEFAULT_SIZES, buildImageMetadataMap, rewriteRichTextImages } from './imageMetadata';
63
108
 
64
109
  /**
65
110
  * Image preload info for generating <link rel="preload"> tags in head
@@ -86,7 +131,7 @@ export type { CMSContext } from './cmsSSRProcessor';
86
131
  function resolveI18nAttrs<T extends Record<string, unknown>>(
87
132
  attrs: T,
88
133
  locale: string | undefined,
89
- i18nConfig: I18nConfig | undefined
134
+ i18nConfig: I18nConfig | undefined,
90
135
  ): T {
91
136
  let mutated: Record<string, unknown> | null = null;
92
137
  const config = i18nConfig ?? DEFAULT_I18N_CONFIG;
@@ -243,47 +288,37 @@ function localizeHref(href: string, ctx: SSRContext): string {
243
288
  */
244
289
  function localizeRichTextLinks(html: string, ctx: SSRContext): string {
245
290
  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
- );
291
+ return html.replace(/<a\b([^>]*?)href=(["'])([^"']*?)\2([^>]*?)>/gi, (match, before, quote, href, after) => {
292
+ if (!href.startsWith('/') || href.startsWith('//')) return match;
293
+ const localized = localizeHref(href, ctx);
294
+ return localized === href ? match : `<a${before}href=${quote}${localized}${quote}${after}>`;
295
+ });
254
296
  }
255
297
 
256
298
  /**
257
299
  * Process style templates and convert to utility classes.
258
300
  * Handles {{item.field}} patterns within list contexts.
259
301
  */
260
- function processStyleToClasses(
261
- style: StyleObject | ResponsiveStyleObject | undefined,
262
- ctx: SSRContext
263
- ): string[] {
302
+ function processStyleToClasses(style: StyleObject | ResponsiveStyleObject | undefined, ctx: SSRContext): string[] {
264
303
  if (!style) return [];
265
304
 
266
305
  let processedStyle = style;
267
306
  const templateCtx = getTemplateContext(ctx);
268
307
  if (templateCtx && !ctx.templateMode) {
269
- processedStyle = processItemPropsTemplate(
270
- style as Record<string, unknown>,
271
- templateCtx,
272
- getI18nResolver(ctx)
273
- ) as StyleObject | ResponsiveStyleObject;
308
+ processedStyle = processItemPropsTemplate(style as Record<string, unknown>, templateCtx, getI18nResolver(ctx)) as
309
+ | StyleObject
310
+ | ResponsiveStyleObject;
274
311
  }
275
312
 
276
313
  // Fluid container pattern: in fluid mode `responsiveStylesToClasses` rewrites
277
314
  // any tier whose `width === maxWidth` to use `calc(100% - var(--site-margin)*2)`
278
315
  // with auto margins.
279
- const fluidActive =
280
- ctx.responsiveScales?.enabled === true &&
281
- ctx.responsiveScales?.mode === 'fluid';
316
+ const fluidActive = ctx.responsiveScales?.enabled === true && ctx.responsiveScales?.mode === 'fluid';
282
317
 
283
- return responsiveStylesToClasses(
284
- processedStyle as ResponsiveStyleObject,
285
- { fluidActive, responsiveScales: ctx.responsiveScales }
286
- );
318
+ return responsiveStylesToClasses(processedStyle as ResponsiveStyleObject, {
319
+ fluidActive,
320
+ responsiveScales: ctx.responsiveScales,
321
+ });
287
322
  }
288
323
 
289
324
  /**
@@ -344,50 +379,88 @@ function evaluateIfCondition(node: ComponentNode, ctx: SSRContext): boolean {
344
379
  return true;
345
380
  }
346
381
 
382
+ /**
383
+ * Sentinel returned by resolveFilterValue when a {{...}} template value can't
384
+ * be resolved from the available scope. Lets resolveFilterTemplates tell
385
+ * "resolved to a real value" apart from "unresolvable" so the condition can be
386
+ * dropped from the CMS query instead of being sent as a literal template
387
+ * string — which would never match a real value and silently return nothing.
388
+ */
389
+ const UNRESOLVED_FILTER_VALUE = Symbol('unresolved-filter-value');
390
+
347
391
  /**
348
392
  * 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.
393
+ * Returns the original value for non-template values, and
394
+ * UNRESOLVED_FILTER_VALUE when a template can't be resolved.
350
395
  */
351
396
  function resolveFilterValue(value: unknown, scope: Record<string, unknown> | undefined): unknown {
352
- if (!scope || typeof value !== 'string' || !value.startsWith('{{') || !value.endsWith('}}')) {
397
+ if (typeof value !== 'string' || !value.startsWith('{{') || !value.endsWith('}}')) {
353
398
  return value;
354
399
  }
355
400
  const path = value.slice(2, -2).trim();
356
- const resolved = getNestedValue(scope, path);
357
- return resolved !== undefined ? resolved : value;
401
+ const resolved = scope ? getNestedValue(scope, path) : undefined;
402
+ return resolved !== undefined ? resolved : UNRESOLVED_FILTER_VALUE;
358
403
  }
359
404
 
360
405
  /**
361
406
  * Resolve template values in CMS filter for nested cms-list support.
362
407
  * Handles filter values like "{{category.slug}}" against a flat scope built from
363
408
  * parent template context overlaid on host-component resolved props.
409
+ *
410
+ * Conditions whose template value can't be resolved are dropped from the query
411
+ * (a verbatim "{{x.y}}" string would match nothing) and surfaced via
412
+ * logRuntimeError — dropping a condition changes results, so users must be
413
+ * able to see why.
364
414
  */
365
415
  function resolveFilterTemplates(
366
416
  filter: CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined,
367
- scope: Record<string, unknown> | undefined
417
+ scope: Record<string, unknown> | undefined,
418
+ collection: string,
368
419
  ): CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined {
369
- if (!filter || !scope) return filter;
420
+ if (!filter) return filter;
421
+
422
+ const reportDropped = (field: unknown, template: unknown): void => {
423
+ logRuntimeError(
424
+ 'ssrRenderer.resolveFilterTemplates',
425
+ new Error(`Unresolved filter template ${String(template)} — condition dropped from "${collection}" query`),
426
+ { collection, field: String(field), template: String(template) },
427
+ );
428
+ };
370
429
 
371
430
  // Handle array of conditions
372
431
  if (Array.isArray(filter)) {
373
- return filter.map(cond => ({
374
- ...cond,
375
- value: resolveFilterValue(cond.value, scope)
376
- }));
432
+ const conditions: CMSFilterCondition[] = [];
433
+ for (const cond of filter) {
434
+ const value = resolveFilterValue(cond.value, scope);
435
+ if (value === UNRESOLVED_FILTER_VALUE) {
436
+ reportDropped(cond.field, cond.value);
437
+ continue;
438
+ }
439
+ conditions.push({ ...cond, value } as CMSFilterCondition);
440
+ }
441
+ return conditions;
377
442
  }
378
443
 
379
444
  // Handle single condition object with field/value
380
445
  if ('field' in filter && 'value' in filter) {
381
- return {
382
- ...(filter as CMSFilterCondition),
383
- value: resolveFilterValue((filter as CMSFilterCondition).value, scope)
384
- };
446
+ const cond = filter as CMSFilterCondition;
447
+ const value = resolveFilterValue(cond.value, scope);
448
+ if (value === UNRESOLVED_FILTER_VALUE) {
449
+ reportDropped(cond.field, cond.value);
450
+ return undefined;
451
+ }
452
+ return { ...cond, value } as CMSFilterCondition;
385
453
  }
386
454
 
387
455
  // Handle simple object filter: { category: "{{category.slug}}" }
388
456
  const resolved: Record<string, unknown> = {};
389
457
  for (const [key, value] of Object.entries(filter)) {
390
- resolved[key] = resolveFilterValue(value, scope);
458
+ const resolvedValue = resolveFilterValue(value, scope);
459
+ if (resolvedValue === UNRESOLVED_FILTER_VALUE) {
460
+ reportDropped(key, value);
461
+ continue;
462
+ }
463
+ resolved[key] = resolvedValue;
391
464
  }
392
465
  return resolved;
393
466
  }
@@ -405,38 +478,47 @@ async function expandRichTextComponents(html: string, ctx: SSRContext): Promise<
405
478
  const markerRegex = /<div\s+data-meno-component="([^"]+)"\s+data-meno-props="([^"]*)"[^>]*><\/div>/g;
406
479
  const parts: (string | Promise<string>)[] = [];
407
480
  let lastIndex = 0;
408
- let match: RegExpExecArray | null;
481
+ let match: RegExpExecArray | null = markerRegex.exec(html);
409
482
 
410
- while ((match = markerRegex.exec(html)) !== null) {
483
+ while (match !== null) {
484
+ const currentMatch = match;
485
+ match = markerRegex.exec(html);
411
486
  // Add text before this match
412
- if (match.index > lastIndex) {
413
- parts.push(html.slice(lastIndex, match.index));
487
+ if (currentMatch.index > lastIndex) {
488
+ parts.push(html.slice(lastIndex, currentMatch.index));
414
489
  }
415
490
 
416
- const componentName = match[1];
491
+ const componentName = currentMatch[1];
492
+ if (componentName === undefined) {
493
+ // Keep marker; mirror the unknown-component fallback below.
494
+ parts.push(currentMatch[0]);
495
+ lastIndex = currentMatch.index + currentMatch[0].length;
496
+ continue;
497
+ }
417
498
  let props: Record<string, unknown> = {};
418
499
  try {
419
500
  // Unescape HTML entities in the JSON
420
- const propsStr = match[2]
501
+ const propsStr = (currentMatch[2] ?? '')
421
502
  .replace(/&quot;/g, '"')
422
503
  .replace(/&#039;/g, "'")
423
504
  .replace(/&amp;/g, '&');
424
505
  props = JSON.parse(propsStr);
425
- } catch {
426
- // ignore parse errors
506
+ } catch (error) {
507
+ // Malformed data-meno-props JSON: fall back to interface defaults so the
508
+ // marker never crashes the page, but surface the parse failure — the
509
+ // component silently losing its instance props is otherwise invisible.
510
+ logRuntimeError('ssrRenderer.expandRichTextComponents', error, { component: componentName });
427
511
  }
428
512
 
429
513
  // Render the component using SSR registry
430
514
  if (ssrComponentRegistry.has(componentName)) {
431
- parts.push(
432
- renderComponent(componentName, props, [], {}, ctx)
433
- );
515
+ parts.push(renderComponent(componentName, props, [], {}, ctx));
434
516
  } else {
435
517
  // Keep marker for unknown components
436
- parts.push(match[0]);
518
+ parts.push(currentMatch[0]);
437
519
  }
438
520
 
439
- lastIndex = match.index + match[0].length;
521
+ lastIndex = currentMatch.index + currentMatch[0].length;
440
522
  }
441
523
 
442
524
  // Add remaining text
@@ -470,8 +552,15 @@ export async function buildComponentHTML(
470
552
  cmsContext?: CMSContext,
471
553
  cmsService?: CMSService,
472
554
  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> }> {
555
+ injectEditorAttrs?: boolean,
556
+ ): Promise<{
557
+ html: string;
558
+ interactiveStylesMap: Map<string, InteractiveStyles>;
559
+ preloadImages: PreloadImage[];
560
+ neededCollections: Set<string>;
561
+ ssrFallbackCollector: Map<string, string>;
562
+ processedRawHtmlCollector: Map<string, string>;
563
+ }> {
475
564
  // Create map to collect interactive styles during render
476
565
  const interactiveStylesMap = new Map<string, InteractiveStyles>();
477
566
  // Create array to collect high-priority images for preloading
@@ -483,7 +572,15 @@ export async function buildComponentHTML(
483
572
  // Create map to collect raw-HTML → processed-HTML for Astro exporter parity
484
573
  const processedRawHtmlCollector = new Map<string, string>();
485
574
 
486
- if (!node) return { html: '', interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector };
575
+ if (!node)
576
+ return {
577
+ html: '',
578
+ interactiveStylesMap,
579
+ preloadImages,
580
+ neededCollections,
581
+ ssrFallbackCollector,
582
+ processedRawHtmlCollector,
583
+ };
487
584
 
488
585
  // Register components for this render
489
586
  ssrComponentRegistry.merge(globalComponents);
@@ -523,7 +620,14 @@ export async function buildComponentHTML(
523
620
 
524
621
  const html = await renderNode(node, ctx);
525
622
 
526
- return { html, interactiveStylesMap, preloadImages, neededCollections, ssrFallbackCollector, processedRawHtmlCollector };
623
+ return {
624
+ html,
625
+ interactiveStylesMap,
626
+ preloadImages,
627
+ neededCollections,
628
+ ssrFallbackCollector,
629
+ processedRawHtmlCollector,
630
+ };
527
631
  }
528
632
 
529
633
  /**
@@ -531,10 +635,7 @@ export async function buildComponentHTML(
531
635
  * Used when parent list has emitTemplate: true.
532
636
  * The placeholder contains the configuration and template for MenoFilter to hydrate.
533
637
  */
534
- async function renderNestedListPlaceholder(
535
- node: ListNode,
536
- ctx: SSRContext
537
- ): Promise<string> {
638
+ async function renderNestedListPlaceholder(node: ListNode, ctx: SSRContext): Promise<string> {
538
639
  // Get source - handle both string and pre-resolved array sources
539
640
  const sourceValue = node.source || node.collection;
540
641
  const sourceStr = typeof sourceValue === 'string' ? sourceValue : '';
@@ -563,17 +664,17 @@ async function renderNestedListPlaceholder(
563
664
  templateContext: undefined, // Clear context for inner item templates
564
665
  nestedCMSListMode: false, // Reset for deeper nesting levels
565
666
  };
566
- const templateContent = node.children
567
- ? await renderChildrenAsync(node.children, childTemplateCtx)
568
- : '';
667
+ const templateContent = node.children ? await renderChildrenAsync(node.children, childTemplateCtx) : '';
569
668
 
570
669
  // Escape config for attribute
571
670
  const configJson = escapeHtml(JSON.stringify(config));
572
671
 
573
672
  // 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 })}>` +
673
+ return (
674
+ `<div data-cms-list-nested="true" data-collection="${escapeHtml(sourceStr)}" data-cms-config="${configJson}"${editorAttrs(ctx, { isCMSListContainer: true })}>` +
575
675
  `<template data-nested-template>${templateContent}</template>` +
576
- `</div>`;
676
+ `</div>`
677
+ );
577
678
  }
578
679
 
579
680
  /**
@@ -606,7 +707,7 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
606
707
  // Determine source type (default to 'prop', but handle legacy 'cms-list' type)
607
708
  const nodeType = node.type as string;
608
709
  const isLegacyCMSList = nodeType === 'cms-list';
609
- const sourceType = isLegacyCMSList ? 'collection' : (node.sourceType || 'prop');
710
+ const sourceType = isLegacyCMSList ? 'collection' : node.sourceType || 'prop';
610
711
 
611
712
  // For collection mode, check CMS service availability
612
713
  if (sourceType === 'collection' && !ctx.cmsService) {
@@ -642,6 +743,19 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
642
743
  if (sourceType === 'collection') {
643
744
  // Collection mode: Query from CMS service
644
745
  items = await getCollectionItems(node, source, ctx);
746
+ } else if (sourceType === 'remote') {
747
+ // Remote mode: fetch the endpoint and apply the query (filter/sort/offset/limit), mirroring
748
+ // the meno-astro runtime's getRemoteData so the editor SSR matches the real Astro build.
749
+ items = await getRemoteItems(node, ctx);
750
+ } else if (sourceType === 'sanity') {
751
+ // Sanity mode: fetch the document type via GROQ and apply the query, mirroring the
752
+ // meno-astro runtime's getSanityData so the editor SSR matches the real Astro build.
753
+ items = await getSanityItems(node);
754
+ } else if (sourceType === 'expression') {
755
+ // Expression mode: the source is a verbatim JS expression (e.g. an Astro Action / supabase
756
+ // result) the editor has no data for — it resolves only in the real Astro build/play. Render
757
+ // empty here (the design-canvas SSR) rather than guessing. Real Astro play renders it for real.
758
+ items = [];
645
759
  } else {
646
760
  // Prop mode: Read from component props or template context
647
761
  // If source was already resolved to an array by processStructure (e.g., from {{features}}),
@@ -676,26 +790,16 @@ async function processList(node: ListNode, ctx: SSRContext): Promise<string> {
676
790
  }
677
791
 
678
792
  // Get schema for URL computation (collection mode only)
679
- const schema = sourceType === 'collection' && ctx.cmsService
680
- ? ctx.cmsService.getSchema(source)
681
- : undefined;
793
+ const schema = sourceType === 'collection' && ctx.cmsService ? ctx.cmsService.getSchema(source) : undefined;
682
794
 
683
795
  // Render children for each item
684
796
  const renderedItems: string[] = [];
685
797
 
686
798
  for (let i = 0; i < items.length; i++) {
687
799
  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
- );
800
+ const item = schema ? addItemUrl(rawItem as CMSItem, schema, ctx.locale, ctx.i18nConfig) : rawItem;
801
+
802
+ const templateContext = buildTemplateContext(variableName, item as CMSItem, i, items.length, ctx.templateContext);
699
803
 
700
804
  const itemCtx: SSRContext = {
701
805
  ...ctx,
@@ -762,7 +866,7 @@ async function getCollectionItems(node: ListNode, source: string, ctx: SSRContex
762
866
  }
763
867
  }
764
868
  if (value !== null && value !== undefined) {
765
- resolvedIds = Array.isArray(value) ? value.map(v => String(v)) : String(value);
869
+ resolvedIds = Array.isArray(value) ? value.map((v) => String(v)) : String(value);
766
870
  }
767
871
  } else {
768
872
  // Otherwise resolve from a merged scope: parent list loop variables (highest precedence)
@@ -786,14 +890,18 @@ async function getCollectionItems(node: ListNode, source: string, ctx: SSRContex
786
890
  }
787
891
  // Filter draft items from ID-based queries
788
892
  if (ctx.locale) {
789
- items = items.filter(item => !isItemDraftForLocale(item, ctx.locale!));
893
+ items = items.filter((item) => !isItemDraftForLocale(item, ctx.locale!));
790
894
  }
791
895
  } else {
792
896
  // Build query from node props (resolve filter templates against parent loop variables
793
897
  // overlaid on host-component resolved props, with parent loop variables winning).
794
898
  const query = {
795
899
  collection: source,
796
- filter: resolveFilterTemplates(node.filter as CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined, buildListResolutionScope(ctx)),
900
+ filter: resolveFilterTemplates(
901
+ node.filter as CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown> | undefined,
902
+ buildListResolutionScope(ctx),
903
+ source,
904
+ ),
797
905
  sort: node.sort,
798
906
  limit: node.limit,
799
907
  offset: node.offset,
@@ -805,12 +913,140 @@ async function getCollectionItems(node: ListNode, source: string, ctx: SSRContex
805
913
  // Exclude current item if option is set and we have a current CMS context
806
914
  if (node.excludeCurrentItem && ctx.cmsContext?.cms?._id) {
807
915
  const currentId = ctx.cmsContext.cms._id as string;
808
- items = items.filter(item => item._id !== currentId);
916
+ items = items.filter((item) => item._id !== currentId);
917
+ }
918
+
919
+ return items;
920
+ }
921
+
922
+ /**
923
+ * Get items for a remote-data list (`sourceType: 'remote'`). Fetches the endpoint at render
924
+ * time, navigates the optional dot-`path` to the items array, synthesizes a stable `_id` per
925
+ * item, and applies the node's filter/sort/offset/limit. This mirrors the meno-astro runtime's
926
+ * `getRemoteData` (the build/preview rendering path) so the editor's own SSR matches the real
927
+ * Astro output. Graceful `[]` on any network/parse error, exactly like the runtime.
928
+ */
929
+ async function getRemoteItems(node: ListNode, _ctx: SSRContext): Promise<CMSItem[]> {
930
+ const url = typeof node.url === 'string' ? node.url : '';
931
+ if (!url) return [];
932
+
933
+ let payload: unknown;
934
+ try {
935
+ const res = await fetch(url, { headers: { accept: 'application/json' } });
936
+ if (!res.ok) return [];
937
+ payload = await res.json();
938
+ } catch {
939
+ return [];
940
+ }
941
+
942
+ // Navigate the dot-path (e.g. "data.items"); empty/absent = the response is the array.
943
+ let raw: unknown = payload;
944
+ if (node.path) {
945
+ for (const seg of node.path.split('.')) {
946
+ if (raw && typeof raw === 'object') raw = (raw as Record<string, unknown>)[seg];
947
+ else {
948
+ raw = undefined;
949
+ break;
950
+ }
951
+ }
952
+ }
953
+ if (!Array.isArray(raw)) return [];
954
+
955
+ // Synthesize `_id` (item id/_id else index; primitives → { _id, value }) so card bindings
956
+ // ({{item._id}}) and React keys resolve — same shape getRemoteData produces.
957
+ let items: CMSItem[] = raw.map((it, i) => {
958
+ if (it && typeof it === 'object' && !Array.isArray(it)) {
959
+ const rec = it as Record<string, unknown>;
960
+ return { ...rec, _id: String(rec._id ?? rec.id ?? i) } as CMSItem;
961
+ }
962
+ return { _id: String(i), value: it } as unknown as CMSItem;
963
+ });
964
+
965
+ // Apply the query in the same order as the runtime (filter → sort → offset → limit).
966
+ if (node.filter) {
967
+ items = applyCmsFilters(items, node.filter as CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown>);
968
+ }
969
+ if (node.sort) items = applyCmsSorting(items, node.sort);
970
+ if (node.offset && node.offset > 0) items = items.slice(node.offset);
971
+ if (node.limit && node.limit > 0) items = items.slice(0, node.limit);
972
+
973
+ return items;
974
+ }
975
+
976
+ /**
977
+ * Get items for a Sanity list (`sourceType: 'sanity'`). Reads the project's Sanity connection
978
+ * (`integrations.sanity`) from the config service, fetches the document type via GROQ at render
979
+ * time, reads `payload.result`, flattens slug-typed fields, synthesizes `_id`, and applies the
980
+ * node's filter/sort/offset/limit. Mirrors the meno-astro runtime's `getSanityData` so the
981
+ * editor's own SSR matches the real Astro build. Graceful `[]` on missing config / any error.
982
+ */
983
+ async function getSanityItems(node: ListNode): Promise<CMSItem[]> {
984
+ const documentType = typeof node.documentType === 'string' ? node.documentType : '';
985
+ if (!documentType) return [];
986
+ const cfg = configService.getSanity();
987
+ if (!cfg) return [];
988
+
989
+ const host = cfg.useCdn === false ? 'api' : 'apicdn';
990
+ const version = (cfg.apiVersion ?? '2024-01-01').replace(/^v/, '');
991
+ const groq = `*[_type == ${JSON.stringify(documentType)}]`;
992
+ const url = `https://${cfg.projectId}.${host}.sanity.io/v${version}/data/query/${cfg.dataset}?query=${encodeURIComponent(groq)}`;
993
+
994
+ let payload: unknown;
995
+ try {
996
+ const res = await fetch(url, { headers: { accept: 'application/json' } });
997
+ if (!res.ok) return [];
998
+ payload = await res.json();
999
+ } catch {
1000
+ return [];
809
1001
  }
810
1002
 
1003
+ const raw = (payload as { result?: unknown } | null)?.result;
1004
+ if (!Array.isArray(raw)) return [];
1005
+
1006
+ let items: CMSItem[] = raw.map((it, i) => {
1007
+ if (it && typeof it === 'object' && !Array.isArray(it)) {
1008
+ const rec = resolveSanityScalars(cfg.projectId, cfg.dataset, it) as Record<string, unknown>;
1009
+ return { ...rec, _id: String(rec._id ?? i) } as CMSItem;
1010
+ }
1011
+ return { _id: String(i), value: it } as unknown as CMSItem;
1012
+ });
1013
+
1014
+ if (node.filter) {
1015
+ items = applyCmsFilters(items, node.filter as CMSFilterCondition | CMSFilterCondition[] | Record<string, unknown>);
1016
+ }
1017
+ if (node.sort) items = applyCmsSorting(items, node.sort);
1018
+ if (node.offset && node.offset > 0) items = items.slice(node.offset);
1019
+ if (node.limit && node.limit > 0) items = items.slice(0, node.limit);
1020
+
811
1021
  return items;
812
1022
  }
813
1023
 
1024
+ /**
1025
+ * Resolve Sanity scalar shapes for the editor's SSR LIST preview: slug → `current`, image/file
1026
+ * asset → cdn URL (deterministic from the ref + projectId/dataset). Recursive. Mirrors meno-astro's
1027
+ * `resolveScalars`, minus Portable Text (no serializer dep in core) and reference joins (no extra
1028
+ * fetch) — the authoritative build/CMS-template paths in meno-astro do the full resolution.
1029
+ */
1030
+ function resolveSanityScalars(projectId: string, dataset: string, value: unknown): unknown {
1031
+ if (Array.isArray(value)) return value.map((v) => resolveSanityScalars(projectId, dataset, v));
1032
+ if (value !== null && typeof value === 'object') {
1033
+ const o = value as Record<string, unknown>;
1034
+ if (o._type === 'slug' && typeof o.current === 'string') return o.current;
1035
+ if ((o._type === 'image' || o._type === 'file') && o.asset && typeof o.asset === 'object') {
1036
+ const ref = (o.asset as { _ref?: unknown })._ref;
1037
+ if (typeof ref === 'string') {
1038
+ const m = /^(image|file)-(.+)-([a-z0-9]+)$/i.exec(ref);
1039
+ if (m)
1040
+ return `https://cdn.sanity.io/${m[1] === 'image' ? 'images' : 'files'}/${projectId}/${dataset}/${m[2]}.${m[3]}`;
1041
+ }
1042
+ }
1043
+ const out: Record<string, unknown> = {};
1044
+ for (const [k, v] of Object.entries(o)) out[k] = resolveSanityScalars(projectId, dataset, v);
1045
+ return out;
1046
+ }
1047
+ return value;
1048
+ }
1049
+
814
1050
  /**
815
1051
  * Get items from component props or template context (for sourceType: 'prop')
816
1052
  */
@@ -866,25 +1102,21 @@ async function renderChildrenAsync(children: unknown[], ctx: SSRContext): Promis
866
1102
  * Build element class name for a node based on its SSR context.
867
1103
  * Used by embed, link, locale-list, and regular HTML nodes.
868
1104
  */
869
- function buildNodeElementClass(
870
- ctx: SSRContext,
871
- label: string | undefined,
872
- isSlotContent?: boolean
873
- ): string {
1105
+ function buildNodeElementClass(ctx: SSRContext, label: string | undefined, isSlotContent?: boolean): string {
874
1106
  const { pagePath } = ctx;
875
1107
  // Slot content uses page context (defined in page, not component)
876
1108
  const useComponentContext = !isSlotContent && Boolean(ctx.componentContext);
877
1109
  const effectiveFileType = useComponentContext ? 'component' : 'page';
878
- const effectiveFileName = useComponentContext ? ctx.componentContext : (pagePath
879
- ? pagePath.replace(/^\//, '').replace(/\//g, '_') || 'index'
880
- : 'page');
1110
+ const effectiveFileName = useComponentContext
1111
+ ? ctx.componentContext
1112
+ : pagePath
1113
+ ? pagePath.replace(/^\//, '').replace(/\//g, '_') || 'index'
1114
+ : 'page';
881
1115
 
882
1116
  // Slice off the component-instance prefix so class hashes stay stable across
883
1117
  // instances of the same component (mirrors ComponentBuilder.ts:678-680).
884
1118
  const rawPath = ctx.elementPath || [];
885
- const path = useComponentContext && ctx.componentRootPath
886
- ? rawPath.slice(ctx.componentRootPath.length)
887
- : rawPath;
1119
+ const path = useComponentContext && ctx.componentRootPath ? rawPath.slice(ctx.componentRootPath.length) : rawPath;
888
1120
 
889
1121
  const elementClassCtx: ElementClassContext = {
890
1122
  fileType: effectiveFileType,
@@ -902,7 +1134,7 @@ function buildNodeElementClass(
902
1134
  function registerInteractiveStyles(
903
1135
  ctx: SSRContext,
904
1136
  elementClass: string,
905
- interactiveStyles: InteractiveStyles
1137
+ interactiveStyles: InteractiveStyles,
906
1138
  ): Record<string, string> {
907
1139
  if (!ctx.interactiveStylesMap) return {};
908
1140
 
@@ -939,20 +1171,18 @@ function arraysEqual(a: number[] | undefined, b: number[] | undefined): boolean
939
1171
  * Compute editor-only attributes for the current context.
940
1172
  * Returns '' when injectEditorAttrs is off — preview-only feature.
941
1173
  */
942
- function editorAttrs(
943
- ctx: SSRContext,
944
- opts: { isSlotContent?: boolean; isCMSListContainer?: boolean } = {}
945
- ): string {
1174
+ function editorAttrs(ctx: SSRContext, opts: { isSlotContent?: boolean; isCMSListContainer?: boolean } = {}): string {
946
1175
  if (!ctx.injectEditorAttrs) return '';
947
1176
  // 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);
1177
+ const isComponentRoot =
1178
+ !!ctx.componentContext && !opts.isSlotContent && arraysEqual(ctx.elementPath, ctx.componentRootPath);
951
1179
  // Mirror client semantics: at component root, parent is the outer component;
952
1180
  // for descendants of a component, parent is the current component itself.
953
1181
  const effectiveParent = opts.isSlotContent
954
1182
  ? ctx.parentComponentName
955
- : (isComponentRoot ? ctx.parentComponentName : (ctx.componentContext ?? ctx.parentComponentName));
1183
+ : isComponentRoot
1184
+ ? ctx.parentComponentName
1185
+ : (ctx.componentContext ?? ctx.parentComponentName);
956
1186
  return buildEditorAttrs({
957
1187
  elementPath: ctx.elementPath,
958
1188
  cmsItemIndexPath: ctx.cmsItemIndexPath,
@@ -970,9 +1200,9 @@ function editorAttrs(
970
1200
  */
971
1201
  async function renderNode(
972
1202
  node: ComponentNode | ComponentNode[] | string | number | null | undefined,
973
- ctx: SSRContext
1203
+ ctx: SSRContext,
974
1204
  ): Promise<string> {
975
- const { breakpoints, viewportWidth, locale, i18nConfig, slugMappings, pagePath } = ctx;
1205
+ const { breakpoints, viewportWidth, locale, i18nConfig, slugMappings: _slugMappings, pagePath } = ctx;
976
1206
 
977
1207
  if (node === null || node === undefined) return '';
978
1208
 
@@ -1028,10 +1258,14 @@ async function renderNode(
1028
1258
  }
1029
1259
 
1030
1260
  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('');
1261
+ return (
1262
+ await Promise.all(
1263
+ node.map((child, index) => {
1264
+ const childPath = ctx.elementPath ? [...ctx.elementPath.slice(0, -1), index] : [index];
1265
+ return renderNode(child, { ...ctx, elementPath: childPath });
1266
+ }),
1267
+ )
1268
+ ).join('');
1035
1269
  }
1036
1270
 
1037
1271
  if (typeof node !== 'object') return '';
@@ -1045,10 +1279,7 @@ async function renderNode(
1045
1279
  const i18nResolveConfig = i18nConfig ?? DEFAULT_I18N_CONFIG;
1046
1280
  const i18nEffectiveLocale = locale || i18nResolveConfig.defaultLocale;
1047
1281
  const resolved = resolveI18nValue(node, i18nEffectiveLocale, i18nResolveConfig);
1048
- return renderNode(
1049
- resolved as ComponentNode | string | number | null | undefined,
1050
- ctx
1051
- );
1282
+ return renderNode(resolved as ComponentNode | string | number | null | undefined, ctx);
1052
1283
  }
1053
1284
 
1054
1285
  // Check if condition - skip rendering if false
@@ -1069,8 +1300,33 @@ async function renderNode(
1069
1300
  }
1070
1301
 
1071
1302
  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 || []) : [];
1303
+ const nodeStyle = 'style' in node ? (node.style as StyleObject | ResponsiveStyleObject | undefined) : undefined;
1304
+ const children = 'children' in node ? node.children || [] : [];
1305
+
1306
+ // Islands (BYO framework components) and Custom nodes (opaque hand-authored
1307
+ // `.astro` black boxes) can't run in meno-core's SSR — real rendering happens
1308
+ // only in a real `astro build` / play preview (the meno-astro codec emits the
1309
+ // `<Component client:* />` / `<Fancy />` markup). On the design canvas we render
1310
+ // their slotted children as normal content (so pages built from them read
1311
+ // cleanly) and fall back to a quiet inline marker when there are none. The
1312
+ // wrapper carries `data-element-path` (via editorAttrs) so a click selects the
1313
+ // node like any other, plus `data-island`/`data-custom` for identification —
1314
+ // no loud highlight box.
1315
+ if (isIslandNode(node) || isCustomNode(node)) {
1316
+ const src = typeof node.src === 'string' ? node.src : '';
1317
+ const extPattern = isIslandNode(node) ? /\.(tsx|jsx|vue|svelte)$/i : /\.astro$/i;
1318
+ const name = src.split('/').pop()?.replace(extPattern, '') || '?';
1319
+ const dataAttr = isIslandNode(node) ? 'data-island' : 'data-custom';
1320
+ const blackBoxChildren = Array.isArray(node.children)
1321
+ ? node.children
1322
+ : typeof node.children === 'string'
1323
+ ? [node.children]
1324
+ : [];
1325
+ const inner = blackBoxChildren.length
1326
+ ? await renderChildrenAsync(blackBoxChildren, ctx)
1327
+ : `<span style="color:#9ca3af;font-size:13px">${escapeHtml(name)}</span>`;
1328
+ return `<div ${dataAttr}="${escapeHtml(src)}" style="display:contents"${editorAttrs(ctx, { isSlotContent: isSlotContent(node) })}>${inner}</div>`;
1329
+ }
1074
1330
 
1075
1331
  // Handle embed nodes - render custom HTML content
1076
1332
  if (isEmbedNode(node)) {
@@ -1106,11 +1362,198 @@ async function renderNode(
1106
1362
 
1107
1363
  // Sanitize HTML with allowlist for SVG, rich-text formatting, and common elements (same as client)
1108
1364
  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;
1365
+ const purifiedHtml = purify
1366
+ ? purify.sanitize(htmlContent, {
1367
+ ALLOWED_TAGS: [
1368
+ 'svg',
1369
+ 'path',
1370
+ 'circle',
1371
+ 'rect',
1372
+ 'line',
1373
+ 'polyline',
1374
+ 'polygon',
1375
+ 'g',
1376
+ 'text',
1377
+ 'tspan',
1378
+ 'image',
1379
+ 'defs',
1380
+ 'use',
1381
+ 'linearGradient',
1382
+ 'radialGradient',
1383
+ 'stop',
1384
+ 'clipPath',
1385
+ 'mask',
1386
+ 'pattern',
1387
+ 'marker',
1388
+ 'symbol',
1389
+ 'a',
1390
+ 'div',
1391
+ 'span',
1392
+ 'p',
1393
+ 'br',
1394
+ 'button',
1395
+ 'img',
1396
+ 'iframe',
1397
+ 'video',
1398
+ 'audio',
1399
+ 'source',
1400
+ 'canvas',
1401
+ 'b',
1402
+ 'i',
1403
+ 'u',
1404
+ 'strong',
1405
+ 'em',
1406
+ 'sub',
1407
+ 'sup',
1408
+ 'mark',
1409
+ 's',
1410
+ 'small',
1411
+ 'del',
1412
+ 'ins',
1413
+ 'q',
1414
+ 'abbr',
1415
+ 'code',
1416
+ 'pre',
1417
+ 'blockquote',
1418
+ 'ul',
1419
+ 'ol',
1420
+ 'li',
1421
+ 'h1',
1422
+ 'h2',
1423
+ 'h3',
1424
+ 'h4',
1425
+ 'h5',
1426
+ 'h6',
1427
+ 'style',
1428
+ 'animate',
1429
+ 'animateTransform',
1430
+ 'animateMotion',
1431
+ 'set',
1432
+ 'filter',
1433
+ 'feGaussianBlur',
1434
+ 'feOffset',
1435
+ 'feMerge',
1436
+ 'feMergeNode',
1437
+ 'feColorMatrix',
1438
+ 'feComposite',
1439
+ 'feFlood',
1440
+ 'feMorphology',
1441
+ 'feBlend',
1442
+ 'feDropShadow',
1443
+ 'feTurbulence',
1444
+ 'feDisplacementMap',
1445
+ 'foreignObject',
1446
+ ],
1447
+ ALLOWED_ATTR: [
1448
+ 'class',
1449
+ 'id',
1450
+ 'style',
1451
+ 'width',
1452
+ 'height',
1453
+ 'viewBox',
1454
+ 'xmlns',
1455
+ 'xmlns:xlink',
1456
+ 'xlink:href',
1457
+ 'fill',
1458
+ 'stroke',
1459
+ 'stroke-width',
1460
+ 'stroke-linecap',
1461
+ 'stroke-linejoin',
1462
+ 'stroke-dasharray',
1463
+ 'stroke-dashoffset',
1464
+ 'd',
1465
+ 'cx',
1466
+ 'cy',
1467
+ 'r',
1468
+ 'x',
1469
+ 'y',
1470
+ 'x1',
1471
+ 'y1',
1472
+ 'x2',
1473
+ 'y2',
1474
+ 'points',
1475
+ 'href',
1476
+ 'src',
1477
+ 'alt',
1478
+ 'target',
1479
+ 'rel',
1480
+ 'data-*',
1481
+ 'aria-*',
1482
+ 'transform',
1483
+ 'opacity',
1484
+ 'fill-opacity',
1485
+ 'fill-rule',
1486
+ 'clip-rule',
1487
+ 'clip-path',
1488
+ 'clipPathUnits',
1489
+ 'mask',
1490
+ 'mask-type',
1491
+ 'maskUnits',
1492
+ 'maskContentUnits',
1493
+ 'patternUnits',
1494
+ 'patternContentUnits',
1495
+ 'patternTransform',
1496
+ 'gradientUnits',
1497
+ 'gradientTransform',
1498
+ 'spreadMethod',
1499
+ 'preserveAspectRatio',
1500
+ 'marker-start',
1501
+ 'marker-mid',
1502
+ 'marker-end',
1503
+ 'markerUnits',
1504
+ 'markerWidth',
1505
+ 'markerHeight',
1506
+ 'refX',
1507
+ 'refY',
1508
+ 'orient',
1509
+ 'paint-order',
1510
+ 'vector-effect',
1511
+ 'filter',
1512
+ 'filterUnits',
1513
+ 'primitiveUnits',
1514
+ 'in',
1515
+ 'in2',
1516
+ 'result',
1517
+ 'stdDeviation',
1518
+ 'flood-color',
1519
+ 'flood-opacity',
1520
+ 'stroke-opacity',
1521
+ 'font-size',
1522
+ 'font-family',
1523
+ 'font-weight',
1524
+ 'font-style',
1525
+ 'text-anchor',
1526
+ 'dominant-baseline',
1527
+ 'offset',
1528
+ 'stop-color',
1529
+ 'stop-opacity',
1530
+ 'frameborder',
1531
+ 'allowfullscreen',
1532
+ 'allow',
1533
+ 'title',
1534
+ 'attributeName',
1535
+ 'values',
1536
+ 'dur',
1537
+ 'begin',
1538
+ 'end',
1539
+ 'repeatCount',
1540
+ 'repeatDur',
1541
+ 'keyTimes',
1542
+ 'keySplines',
1543
+ 'calcMode',
1544
+ 'from',
1545
+ 'to',
1546
+ 'by',
1547
+ 'additive',
1548
+ 'accumulate',
1549
+ 'type',
1550
+ 'rotate',
1551
+ 'keyPoints',
1552
+ 'path',
1553
+ ],
1554
+ KEEP_CONTENT: true,
1555
+ })
1556
+ : htmlContent;
1114
1557
  // Inline simple SVG <style> rules as style="" attributes — class-scoped
1115
1558
  // declarations don't survive the editor preview's HMR `innerHTML` reparse
1116
1559
  // path (`htmlGenerator.ts:574`), but inline attributes always render.
@@ -1144,7 +1587,7 @@ async function renderNode(
1144
1587
  embedCssVariables = registerInteractiveStyles(ctx, elementClass, embedInteractiveStyles);
1145
1588
  }
1146
1589
 
1147
- let embedStyleAttr = buildCssVariableStyleAttr(embedCssVariables);
1590
+ const embedStyleAttr = buildCssVariableStyleAttr(embedCssVariables);
1148
1591
 
1149
1592
  // Add attribute className if present
1150
1593
  const attrClassName = (nodeAttributes.className || nodeAttributes.class || '') as string;
@@ -1191,7 +1634,11 @@ async function renderNode(
1191
1634
  // Link object - extract href and target
1192
1635
  // Unwrap nested link objects (e.g., {href: {href: "/path"}} from double-wrapped list item templates)
1193
1636
  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>)) {
1637
+ while (
1638
+ typeof linkObj.href === 'object' &&
1639
+ linkObj.href !== null &&
1640
+ 'href' in (linkObj.href as Record<string, unknown>)
1641
+ ) {
1195
1642
  const nested = linkObj.href as { href: unknown; target?: string };
1196
1643
  if (!linkObj.target && nested.target) linkObj = { ...linkObj, target: nested.target };
1197
1644
  linkObj = { ...linkObj, href: nested.href };
@@ -1262,11 +1709,18 @@ async function renderNode(
1262
1709
  const classAttr = ` class="${escapeHtml(classNames.filter(Boolean).join(' '))}"`;
1263
1710
 
1264
1711
  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] });
1712
+ ? (
1713
+ await Promise.all(
1714
+ children.map((child, index) => {
1715
+ const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
1716
+ return renderNode(child, { ...ctx, elementPath: childPath });
1717
+ }),
1718
+ )
1719
+ ).join('')
1720
+ : await renderNode(children as ComponentNode | string | number | null | undefined, {
1721
+ ...ctx,
1722
+ elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0],
1723
+ });
1270
1724
 
1271
1725
  return `<a href="${escapeHtml(String(href))}"${classAttr}${olinkStyleAttr}${attrs}${editorAttrs(ctx, { isSlotContent: isSlotContent(node) })}>${childrenHTML}</a>`;
1272
1726
  }
@@ -1277,9 +1731,9 @@ async function renderNode(
1277
1731
  }
1278
1732
 
1279
1733
  // Extract tag or component name based on node type
1280
- let tag = isHtmlNode(node) ? node.tag : undefined;
1734
+ const tag = isHtmlNode(node) ? node.tag : undefined;
1281
1735
  const componentName = isComponentNode(node) ? node.component : undefined;
1282
- let nodeProps = isComponentNode(node) ? (node.props || {}) : {};
1736
+ let nodeProps = isComponentNode(node) ? node.props || {} : {};
1283
1737
 
1284
1738
  // Process CMS templates in props
1285
1739
  if (ctx.cmsContext?.cms && Object.keys(nodeProps).length > 0) {
@@ -1302,18 +1756,29 @@ async function renderNode(
1302
1756
 
1303
1757
  // Process CMS templates in attributes (e.g., href="{{cms.link}}")
1304
1758
  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>;
1759
+ nodeAttributes = processCMSPropsTemplate(
1760
+ nodeAttributes as Record<string, unknown>,
1761
+ ctx.cmsContext.cms,
1762
+ locale,
1763
+ i18nConfig,
1764
+ ) as Record<string, string | number | boolean>;
1306
1765
  }
1307
1766
 
1308
1767
  // Process item templates in attributes (for CMSList context)
1309
1768
  // Skip when in templateMode to preserve {{item.field}} placeholders
1310
1769
  if (!ctx.templateMode && templateCtx && Object.keys(nodeAttributes).length > 0) {
1311
- nodeAttributes = processItemPropsTemplate(nodeAttributes, templateCtx, i18nResolver) as Record<string, string | number | boolean>;
1770
+ nodeAttributes = processItemPropsTemplate(nodeAttributes, templateCtx, i18nResolver) as Record<
1771
+ string,
1772
+ string | number | boolean
1773
+ >;
1312
1774
  }
1313
1775
 
1314
1776
  // Auto-skip attributes that were entirely template expressions and resolved to ""
1315
1777
  if (Object.keys(nodeAttributes).length > 0) {
1316
- nodeAttributes = skipEmptyTemplateAttributes(originalAttributes, nodeAttributes) as Record<string, string | number | boolean>;
1778
+ nodeAttributes = skipEmptyTemplateAttributes(originalAttributes, nodeAttributes) as Record<
1779
+ string,
1780
+ string | number | boolean
1781
+ >;
1317
1782
  }
1318
1783
 
1319
1784
  if (!tag && !componentName) return '';
@@ -1327,7 +1792,8 @@ async function renderNode(
1327
1792
  // result. This matches the client (ComponentBuilder.mergeNodeStyles) so that
1328
1793
  // an instance override of e.g. marginBottom actually replaces the structure's
1329
1794
  // marginBottom instead of fighting it as a same-specificity utility class.
1330
- const isCustomComponentNode = nodeType === NODE_TYPE.COMPONENT && isComponentNode(node) && ssrComponentRegistry.has(node.component);
1795
+ const isCustomComponentNode =
1796
+ nodeType === NODE_TYPE.COMPONENT && isComponentNode(node) && ssrComponentRegistry.has(node.component);
1331
1797
  let deferredComponentStyle: StyleObject | ResponsiveStyleObject | undefined;
1332
1798
 
1333
1799
  if (nodeStyle) {
@@ -1343,16 +1809,23 @@ async function renderNode(
1343
1809
  } else if (nodeProps.style) {
1344
1810
  // If no node.style but props have style, keep it for backward compatibility
1345
1811
  if (isResponsiveStyle(nodeProps.style) && breakpoints && viewportWidth) {
1346
- resolvedStyle = mergeResponsiveStyles(nodeProps.style as ResponsiveStyleObject, 'viewport', viewportWidth, breakpoints);
1812
+ resolvedStyle = mergeResponsiveStyles(
1813
+ nodeProps.style as ResponsiveStyleObject,
1814
+ 'viewport',
1815
+ viewportWidth,
1816
+ breakpoints,
1817
+ );
1347
1818
  } else {
1348
1819
  resolvedStyle = nodeProps.style as StyleObject;
1349
1820
  }
1350
1821
  }
1351
1822
 
1352
1823
  // 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;
1824
+ const nodeInteractiveStyles =
1825
+ 'interactiveStyles' in node ? (node.interactiveStyles as InteractiveStyles | undefined) : undefined;
1826
+ const nodeGenerateElementClass =
1827
+ 'generateElementClass' in node ? (node.generateElementClass as boolean | undefined) : undefined;
1828
+ const nodeLabel = 'label' in node ? (node.label as string | undefined) : undefined;
1356
1829
  let elementClass = '';
1357
1830
 
1358
1831
  if ((nodeInteractiveStyles && nodeInteractiveStyles.length > 0) || nodeGenerateElementClass) {
@@ -1387,7 +1860,9 @@ async function renderNode(
1387
1860
  ...(mergedClassName ? { className: mergedClassName } : {}),
1388
1861
  ...(deferredComponentStyle
1389
1862
  ? { style: deferredComponentStyle }
1390
- : Object.keys(resolvedStyle).length > 0 ? { style: resolvedStyle } : {})
1863
+ : Object.keys(resolvedStyle).length > 0
1864
+ ? { style: resolvedStyle }
1865
+ : {}),
1391
1866
  };
1392
1867
 
1393
1868
  // Check if this is a custom component
@@ -1406,13 +1881,11 @@ async function renderNode(
1406
1881
  return renderLinkNode(propsWithStyleAndAttrs, children, ctx);
1407
1882
  }
1408
1883
 
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);
1884
+ // Regular HTML element. `tag` is necessarily set here: the `!tag &&
1885
+ // !componentName` case returned at the top of this section, and every
1886
+ // `componentName` path has already returned (isComponentNode ⇒ type ===
1887
+ // 'component', which the two component branches above handle exhaustively).
1888
+ return renderHtmlElement(tag as string, propsWithStyleAndAttrs, children, ctx);
1416
1889
  }
1417
1890
 
1418
1891
  /**
@@ -1423,9 +1896,9 @@ async function renderComponent(
1423
1896
  propsWithStyleAndAttrs: Record<string, unknown>,
1424
1897
  children: unknown,
1425
1898
  nodeAttributes: Record<string, unknown>,
1426
- ctx: SSRContext
1899
+ ctx: SSRContext,
1427
1900
  ): Promise<string> {
1428
- const { locale, i18nConfig, pagePath } = ctx;
1901
+ const { locale, i18nConfig, pagePath: _pagePath } = ctx;
1429
1902
  const componentDef = ssrComponentRegistry.get(componentName);
1430
1903
  if (!componentDef) return '';
1431
1904
 
@@ -1439,7 +1912,7 @@ async function renderComponent(
1439
1912
  propsWithStyleAndAttrs,
1440
1913
  children as Array<ComponentNode | string> | string | ComponentNode | null | undefined,
1441
1914
  locale,
1442
- i18nConfig
1915
+ i18nConfig,
1443
1916
  );
1444
1917
 
1445
1918
  // Process the structure with resolved props
@@ -1447,7 +1920,9 @@ async function renderComponent(
1447
1920
  // Use preserveResponsiveStyles: true to keep responsive styles intact for SSR rendering
1448
1921
  // Mark children as slot content so they use page context for element class generation
1449
1922
  // 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;
1923
+ const markedChildren = children
1924
+ ? markAsSlotContent(children as ComponentNode | ComponentNode[] | string | number | null | undefined)
1925
+ : undefined;
1451
1926
 
1452
1927
  const processedStructure = processStructure(
1453
1928
  structuredComponentDef.structure,
@@ -1464,20 +1939,28 @@ async function renderComponent(
1464
1939
  },
1465
1940
  undefined,
1466
1941
  markedChildren,
1467
- true
1942
+ true,
1468
1943
  );
1469
1944
 
1470
1945
  if (!processedStructure) return '';
1471
1946
 
1472
1947
  // Type guard: ensure processedStructure is a ComponentNode
1473
- if (typeof processedStructure === 'string' || typeof processedStructure === 'number' || Array.isArray(processedStructure)) {
1948
+ if (
1949
+ typeof processedStructure === 'string' ||
1950
+ typeof processedStructure === 'number' ||
1951
+ Array.isArray(processedStructure)
1952
+ ) {
1474
1953
  return await renderNode(processedStructure as string | number | ComponentNode[], ctx);
1475
1954
  }
1476
1955
 
1477
1956
  // Merge instance style overrides, className, and attributes
1478
1957
  // processedStructure is typically an HTML node (the component's root element)
1479
1958
  // 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> };
1959
+ const rootNode = processedStructure as ComponentNode & {
1960
+ props?: Record<string, unknown>;
1961
+ style?: Record<string, unknown>;
1962
+ attributes?: Record<string, string | number | boolean>;
1963
+ };
1481
1964
  if (isComponentNode(rootNode) || isHtmlNode(rootNode) || isLinkNode(rootNode)) {
1482
1965
  if (!rootNode.props) {
1483
1966
  rootNode.props = {};
@@ -1499,7 +1982,7 @@ async function renderComponent(
1499
1982
  const existingClass = (rootNode.attributes.class || '') as string;
1500
1983
  rootNode.attributes.class = existingClass
1501
1984
  ? `${existingClass} ${propsWithStyleAndAttrs.className}`
1502
- : propsWithStyleAndAttrs.className as string;
1985
+ : (propsWithStyleAndAttrs.className as string);
1503
1986
  } else {
1504
1987
  const existingClassName = rootNode.props.className || '';
1505
1988
  rootNode.props.className = existingClassName
@@ -1527,9 +2010,7 @@ async function renderComponent(
1527
2010
  // Add defineVars data attributes for JS prop injection
1528
2011
  const defineVars = structuredComponentDef.defineVars;
1529
2012
  if (defineVars && componentName) {
1530
- const varsToExpose = defineVars === true
1531
- ? Object.keys(structuredComponentDef.interface || {})
1532
- : defineVars;
2013
+ const varsToExpose = defineVars === true ? Object.keys(structuredComponentDef.interface || {}) : defineVars;
1533
2014
 
1534
2015
  const propsForJS: Record<string, unknown> = {};
1535
2016
  for (const varName of varsToExpose) {
@@ -1543,18 +2024,25 @@ async function renderComponent(
1543
2024
  if (!processedRoot.attributes) {
1544
2025
  processedRoot.attributes = {};
1545
2026
  }
1546
- const existingComponent = (processedRoot.attributes?.['data-component'] as string)
1547
- || (processedRoot.props?.['data-component'] as string)
1548
- || '';
2027
+ const existingComponent =
2028
+ (processedRoot.attributes?.['data-component'] as string) ||
2029
+ (processedRoot.props?.['data-component'] as string) ||
2030
+ '';
1549
2031
  processedRoot.attributes['data-component'] = existingComponent
1550
2032
  ? `${existingComponent} ${componentName}`
1551
2033
  : componentName;
1552
2034
 
1553
2035
  let existingPropsMap: Record<string, unknown> = {};
1554
- const existingPropsStr = (processedRoot.attributes?.['data-props'] as string)
1555
- || (processedRoot.props?.['data-props'] as string);
2036
+ const existingPropsStr =
2037
+ (processedRoot.attributes?.['data-props'] as string) || (processedRoot.props?.['data-props'] as string);
1556
2038
  if (existingPropsStr) {
1557
- try { existingPropsMap = JSON.parse(existingPropsStr); } catch {}
2039
+ try {
2040
+ existingPropsMap = JSON.parse(existingPropsStr);
2041
+ } catch (e) {
2042
+ // Malformed existing data-props — recover with an empty map, but log it
2043
+ // (otherwise a sibling component's hydration props vanish silently).
2044
+ logRuntimeError('ssrRenderer.parseExistingDataProps', e);
2045
+ }
1558
2046
  }
1559
2047
  existingPropsMap[componentName] = propsForJS;
1560
2048
  processedRoot.attributes['data-props'] = JSON.stringify(existingPropsMap);
@@ -1593,7 +2081,6 @@ async function renderComponent(
1593
2081
  componentRootPath: ctx.elementPath,
1594
2082
  componentResolvedProps: resolvedProps,
1595
2083
  });
1596
-
1597
2084
  } catch (error) {
1598
2085
  return renderNodeFallback(ctx, error, `component ${componentName}`);
1599
2086
  }
@@ -1605,7 +2092,7 @@ async function renderComponent(
1605
2092
  async function renderLinkNode(
1606
2093
  propsWithStyleAndAttrs: Record<string, unknown>,
1607
2094
  children: unknown,
1608
- ctx: SSRContext
2095
+ ctx: SSRContext,
1609
2096
  ): Promise<string> {
1610
2097
  const to = 'to' in propsWithStyleAndAttrs ? propsWithStyleAndAttrs.to : undefined;
1611
2098
  const restProps: Record<string, unknown> = { ...propsWithStyleAndAttrs };
@@ -1614,17 +2101,22 @@ async function renderLinkNode(
1614
2101
  const href = localizeHref(rawHref, ctx);
1615
2102
 
1616
2103
  // Build class attribute from utility classes
1617
- const linkClassAttr = restProps.className
1618
- ? ` class="${escapeHtml(String(restProps.className))}"`
1619
- : '';
2104
+ const linkClassAttr = restProps.className ? ` class="${escapeHtml(String(restProps.className))}"` : '';
1620
2105
 
1621
2106
  const attrs = buildAttributes(restProps, ['style', 'className', 'to']);
1622
2107
  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] });
2108
+ ? (
2109
+ await Promise.all(
2110
+ (children as (ComponentNode | string)[]).map((child, index) => {
2111
+ const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
2112
+ return renderNode(child, { ...ctx, elementPath: childPath });
2113
+ }),
2114
+ )
2115
+ ).join('')
2116
+ : await renderNode(children as ComponentNode, {
2117
+ ...ctx,
2118
+ elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0],
2119
+ });
1628
2120
 
1629
2121
  return `<a href="${escapeHtml(String(href))}"${linkClassAttr}${attrs}${editorAttrs(ctx)}>${childrenHTML}</a>`;
1630
2122
  }
@@ -1636,7 +2128,7 @@ async function renderHtmlElement(
1636
2128
  tag: string,
1637
2129
  propsWithStyleAndAttrs: Record<string, unknown>,
1638
2130
  children: unknown,
1639
- ctx: SSRContext
2131
+ ctx: SSRContext,
1640
2132
  ): Promise<string> {
1641
2133
  // Build class attribute from utility classes
1642
2134
  let classValue = propsWithStyleAndAttrs.className ? String(propsWithStyleAndAttrs.className) : '';
@@ -1663,9 +2155,7 @@ async function renderHtmlElement(
1663
2155
 
1664
2156
  // For img tags, exclude image-specific props (handled specially in renderImageElement)
1665
2157
  const imageProps = ['src', 'alt', 'loading', 'width', 'height', 'sizes', 'srcset', 'fetchpriority'];
1666
- const excludeProps = tag.toLowerCase() === 'img'
1667
- ? ['style', 'className', ...imageProps]
1668
- : ['style', 'className'];
2158
+ const excludeProps = tag.toLowerCase() === 'img' ? ['style', 'className', ...imageProps] : ['style', 'className'];
1669
2159
  const attrs = buildAttributes(propsWithStyleAndAttrs, excludeProps);
1670
2160
  // `<style>` and `<script>` are HTML "raw text" elements: their content is
1671
2161
  // not parsed for entities or child elements. Running text children through
@@ -1682,22 +2172,55 @@ async function renderHtmlElement(
1682
2172
  if (typeof node === 'string') return node;
1683
2173
  if (typeof node === 'number') return String(node);
1684
2174
  if (Array.isArray(node)) return node.map(flatten).join('');
2175
+ // Verbatim-code markers ({ _code, expr }) are silent by design — they
2176
+ // render natively in the Astro build and have nothing to emit in
2177
+ // meno-core SSR (mirrors the renderNode top-level handling).
2178
+ if ((node as { _code?: unknown })._code === true) return '';
2179
+ // Raw-text elements can't contain child elements, so any other object
2180
+ // child is dropped from the output — correct per HTML semantics, but
2181
+ // authored content vanishing should be visible, so surface the drop.
2182
+ logRuntimeError(
2183
+ 'ssrRenderer.renderHtmlElement',
2184
+ new Error(`Element child dropped inside <${tag}> — raw-text elements render only string content`),
2185
+ { tag, droppedNodeType: String((node as { type?: unknown }).type ?? typeof node) },
2186
+ );
1685
2187
  return '';
1686
2188
  };
1687
2189
  childrenHTML = flatten(children as any);
1688
2190
  } else {
1689
2191
  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] });
2192
+ ? (
2193
+ await Promise.all(
2194
+ (children as (ComponentNode | string)[]).map((child, index) => {
2195
+ const childPath = ctx.elementPath ? [...ctx.elementPath, index] : [index];
2196
+ return renderNode(child, { ...ctx, elementPath: childPath });
2197
+ }),
2198
+ )
2199
+ ).join('')
2200
+ : await renderNode(children as ComponentNode, {
2201
+ ...ctx,
2202
+ elementPath: ctx.elementPath ? [...ctx.elementPath, 0] : [0],
2203
+ });
1695
2204
  }
1696
2205
 
1697
2206
  const ea = editorAttrs(ctx);
1698
2207
 
1699
2208
  // Self-closing tags
1700
- const voidElements = ['img', 'input', 'br', 'hr', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'];
2209
+ const voidElements = [
2210
+ 'img',
2211
+ 'input',
2212
+ 'br',
2213
+ 'hr',
2214
+ 'meta',
2215
+ 'link',
2216
+ 'area',
2217
+ 'base',
2218
+ 'col',
2219
+ 'embed',
2220
+ 'source',
2221
+ 'track',
2222
+ 'wbr',
2223
+ ];
1701
2224
  if (voidElements.includes(tag.toLowerCase())) {
1702
2225
  // Special handling for img tags - inject srcset and render as <picture> when AVIF available
1703
2226
  if (tag.toLowerCase() === 'img') {
@@ -1718,7 +2241,7 @@ function renderImageElement(
1718
2241
  classAttr: string,
1719
2242
  styleAttr: string,
1720
2243
  attrs: string,
1721
- ctx: SSRContext
2244
+ ctx: SSRContext,
1722
2245
  ): string {
1723
2246
  const imgProps = propsWithStyleAndAttrs;
1724
2247
  const src = imgProps.src as string | undefined;
@@ -1726,8 +2249,8 @@ function renderImageElement(
1726
2249
  const loading = imgProps.loading as string | undefined;
1727
2250
  const sizes = imgProps.sizes as string | undefined;
1728
2251
  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;
2252
+ const width = imgProps.width as string | number | undefined;
2253
+ const height = imgProps.height as string | number | undefined;
1731
2254
 
1732
2255
  // Get image metadata from manifest
1733
2256
  const metadata = src ? ctx.imageMetadataMap?.get(src) : undefined;
@@ -1766,44 +2289,51 @@ function renderImageElement(
1766
2289
  if (width !== undefined) imgAttrs += ` width="${escapeHtml(String(width))}"`;
1767
2290
  if (height !== undefined) imgAttrs += ` height="${escapeHtml(String(height))}"`;
1768
2291
 
1769
-
1770
2292
  // Render as <picture> element if AVIF is available and imageFormat is 'avif'
1771
2293
  // Layout classes go on <picture> (block container), image-specific classes on <img>
1772
2294
  if (metadata?.avifSrcset && ctx.imageFormat !== 'webp') {
1773
2295
  // Image-specific class prefixes that should stay on <img>
1774
2296
  const imgClassPrefixes = [
1775
- 'objf-', 'objp-', 'flt-', 'tm-', 'bs-',
1776
- 'br-', 'bt-', 'bb-', 'bl-', 'border-r-', 'bc-', 'b-'
2297
+ 'objf-',
2298
+ 'objp-',
2299
+ 'flt-',
2300
+ 'tm-',
2301
+ 'bs-',
2302
+ 'br-',
2303
+ 'bt-',
2304
+ 'bb-',
2305
+ 'bl-',
2306
+ 'border-r-',
2307
+ 'bc-',
2308
+ 'b-',
1777
2309
  ];
1778
2310
  // Opacity classes (o-NUMBER) go on img, but overflow (o-h, o-a, o-s, o-v) stays on picture
1779
2311
  const opacityPattern = /^o-\d/;
1780
2312
 
1781
2313
  const classMatch = classAttr.match(/class="([^"]*)"/);
1782
- const allClasses = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
2314
+ const allClasses = classMatch ? (classMatch[1] ?? '').split(/\s+/).filter(Boolean) : [];
1783
2315
 
1784
2316
  const imgClasses: string[] = [];
1785
2317
  const pictureClasses: string[] = [];
1786
2318
 
1787
2319
  for (const cls of allClasses) {
1788
- if (imgClassPrefixes.some(prefix => cls.startsWith(prefix)) || opacityPattern.test(cls)) {
2320
+ if (imgClassPrefixes.some((prefix) => cls.startsWith(prefix)) || opacityPattern.test(cls)) {
1789
2321
  imgClasses.push(cls);
1790
2322
  } else {
1791
2323
  pictureClasses.push(cls);
1792
2324
  }
1793
2325
  }
1794
2326
 
1795
- const pictureClassAttr = pictureClasses.length > 0
1796
- ? ` class="${escapeHtml(pictureClasses.join(' '))}"`
1797
- : '';
1798
- const imgClassAttr = imgClasses.length > 0
1799
- ? ` class="${escapeHtml(imgClasses.join(' '))}"`
1800
- : '';
2327
+ const pictureClassAttr = pictureClasses.length > 0 ? ` class="${escapeHtml(pictureClasses.join(' '))}"` : '';
2328
+ const imgClassAttr = imgClasses.length > 0 ? ` class="${escapeHtml(imgClasses.join(' '))}"` : '';
1801
2329
 
1802
- return `<picture${pictureClassAttr}${styleAttr}>` +
2330
+ return (
2331
+ `<picture${pictureClassAttr}${styleAttr}>` +
1803
2332
  `<source type="image/avif" srcset="${escapeHtml(metadata.avifSrcset)}" sizes="${escapeHtml(sizesAttr)}" />` +
1804
2333
  `<source type="image/webp" srcset="${escapeHtml(metadata.srcset)}" sizes="${escapeHtml(sizesAttr)}" />` +
1805
2334
  `<img${imgClassAttr}${imgAttrs}${attrs} />` +
1806
- `</picture>`;
2335
+ `</picture>`
2336
+ );
1807
2337
  }
1808
2338
 
1809
2339
  // Fallback: regular img with WebP srcset
@@ -1839,8 +2369,7 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
1839
2369
  }
1840
2370
 
1841
2371
  const localeStyleOpts = {
1842
- fluidActive:
1843
- ctx.responsiveScales?.enabled === true && ctx.responsiveScales?.mode === 'fluid',
2372
+ fluidActive: ctx.responsiveScales?.enabled === true && ctx.responsiveScales?.mode === 'fluid',
1844
2373
  responsiveScales: ctx.responsiveScales,
1845
2374
  };
1846
2375
 
@@ -1866,18 +2395,14 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
1866
2395
  localeListStyleAttr = buildCssVariableStyleAttr(localeListCssVariables);
1867
2396
  }
1868
2397
 
1869
- const containerClassAttr = containerClasses.length > 0
1870
- ? ` class="${escapeHtml(containerClasses.join(' '))}"`
1871
- : '';
2398
+ const containerClassAttr = containerClasses.length > 0 ? ` class="${escapeHtml(containerClasses.join(' '))}"` : '';
1872
2399
 
1873
2400
  // Convert item styles to utility classes
1874
2401
  let itemClasses: string[] = [];
1875
2402
  if (node.itemStyle) {
1876
2403
  itemClasses = responsiveStylesToClasses(node.itemStyle as ResponsiveStyleObject, localeStyleOpts);
1877
2404
  }
1878
- const itemClassAttr = itemClasses.length > 0
1879
- ? ` class="${escapeHtml(itemClasses.join(' '))}"`
1880
- : '';
2405
+ const itemClassAttr = itemClasses.length > 0 ? ` class="${escapeHtml(itemClasses.join(' '))}"` : '';
1881
2406
 
1882
2407
  // Convert active item styles to utility classes
1883
2408
  let activeItemClasses: string[] = [];
@@ -1890,24 +2415,19 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
1890
2415
  if (node.separatorStyle) {
1891
2416
  separatorClasses = responsiveStylesToClasses(node.separatorStyle as ResponsiveStyleObject, localeStyleOpts);
1892
2417
  }
1893
- const separatorClassAttr = separatorClasses.length > 0
1894
- ? ` class="${escapeHtml(separatorClasses.join(' '))}"`
1895
- : '';
2418
+ const separatorClassAttr = separatorClasses.length > 0 ? ` class="${escapeHtml(separatorClasses.join(' '))}"` : '';
1896
2419
 
1897
2420
  // Convert flag styles to utility classes
1898
2421
  let flagClasses: string[] = [];
1899
2422
  if (node.flagStyle) {
1900
2423
  flagClasses = responsiveStylesToClasses(node.flagStyle as ResponsiveStyleObject, localeStyleOpts);
1901
2424
  }
1902
- const flagClassAttr = flagClasses.length > 0
1903
- ? ` class="${escapeHtml(flagClasses.join(' '))}"`
1904
- : '';
2425
+ const flagClassAttr = flagClasses.length > 0 ? ` class="${escapeHtml(flagClasses.join(' '))}"` : '';
1905
2426
 
1906
2427
  // Current item gets both itemStyle + activeItemStyle (additive/override)
1907
2428
  const currentItemClasses = [...itemClasses, ...activeItemClasses];
1908
- const currentItemClassAttr = currentItemClasses.length > 0
1909
- ? ` class="${escapeHtml(currentItemClasses.join(' '))}"`
1910
- : '';
2429
+ const currentItemClassAttr =
2430
+ currentItemClasses.length > 0 ? ` class="${escapeHtml(currentItemClasses.join(' '))}"` : '';
1911
2431
 
1912
2432
  // Build links HTML
1913
2433
  const links: string[] = [];
@@ -1933,20 +2453,19 @@ function renderLocaleList(node: import('../../shared/types').LocaleListNode, ctx
1933
2453
  case 'name':
1934
2454
  displayText = link.name;
1935
2455
  break;
1936
- case 'nativeName':
1937
2456
  default:
1938
2457
  displayText = link.nativeName;
1939
2458
  break;
1940
2459
  }
1941
2460
  linkContent += `<div>${escapeHtml(displayText)}</div>`;
1942
2461
 
1943
- links.push(`<a href="${escapeHtml(link.path)}"${hreflangAttr}${currentAttr} data-locale="${escapeHtml(link.locale)}"${classAttrForLink}>${linkContent}</a>`);
2462
+ links.push(
2463
+ `<a href="${escapeHtml(link.path)}"${hreflangAttr}${currentAttr} data-locale="${escapeHtml(link.locale)}"${classAttrForLink}>${linkContent}</a>`,
2464
+ );
1944
2465
  }
1945
2466
 
1946
2467
  // 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('');
2468
+ const linksHTML = showSeparator ? links.join(`<span${separatorClassAttr}></span>`) : links.join('');
1950
2469
 
1951
2470
  // Extract attributes from node
1952
2471
  const nodeAttributes = resolveI18nAttrs(extractAttributesFromNode(node), locale, i18nConfig);
@@ -1979,8 +2498,20 @@ export async function renderPageSSR(
1979
2498
  cmsContext?: CMSContext,
1980
2499
  cmsService?: CMSService,
1981
2500
  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> }> {
2501
+ injectEditorAttrs?: boolean,
2502
+ ): Promise<{
2503
+ html: string;
2504
+ meta: string;
2505
+ title: string;
2506
+ javascript: string;
2507
+ componentCSS?: string;
2508
+ locale: string;
2509
+ interactiveStylesMap: Map<string, InteractiveStyles>;
2510
+ preloadImages: PreloadImage[];
2511
+ neededCollections: Set<string>;
2512
+ ssrFallbackCollector: Map<string, string>;
2513
+ processedRawHtmlCollector: Map<string, string>;
2514
+ }> {
1984
2515
  // Extract page content
1985
2516
  const rootNode = pageData?.root || undefined;
1986
2517
  if (!rootNode) {
@@ -1988,10 +2519,10 @@ export async function renderPageSSR(
1988
2519
  }
1989
2520
 
1990
2521
  // Load i18n config if not provided
1991
- const config = i18nConfig || await loadI18nConfig();
2522
+ const config = i18nConfig || (await loadI18nConfig());
1992
2523
 
1993
2524
  // Extract locale from path if not explicitly provided
1994
- const { locale: pathLocale, pathWithoutLocale } = extractLocaleFromPath(pagePath, config);
2525
+ const { locale: pathLocale, pathWithoutLocale: _pathWithoutLocale } = extractLocaleFromPath(pagePath, config);
1995
2526
  const effectiveLocale = locale || pathLocale || config.defaultLocale;
1996
2527
 
1997
2528
  // Extract meta information and process CMS templates in meta
@@ -2008,7 +2539,10 @@ export async function renderPageSSR(
2008
2539
  meta = { ...meta, ogTitle: processCMSTemplate(meta.ogTitle, cmsContext.cms, effectiveLocale, config) };
2009
2540
  }
2010
2541
  if (typeof meta.ogDescription === 'string') {
2011
- meta = { ...meta, ogDescription: processCMSTemplate(meta.ogDescription, cmsContext.cms, effectiveLocale, config) };
2542
+ meta = {
2543
+ ...meta,
2544
+ ogDescription: processCMSTemplate(meta.ogDescription, cmsContext.cms, effectiveLocale, config),
2545
+ };
2012
2546
  }
2013
2547
  if (typeof meta.ogImage === 'string') {
2014
2548
  meta = { ...meta, ogImage: processCMSTemplate(meta.ogImage, cmsContext.cms, effectiveLocale, config) };
@@ -2019,9 +2553,27 @@ export async function renderPageSSR(
2019
2553
 
2020
2554
  // Render the component tree to HTML with i18n and CMS support
2021
2555
  // 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>() };
2556
+ // (rootNode is guaranteed non-null the guard at the top of this function throws otherwise)
2557
+ const {
2558
+ html: contentHTML,
2559
+ interactiveStylesMap,
2560
+ preloadImages,
2561
+ neededCollections,
2562
+ ssrFallbackCollector,
2563
+ processedRawHtmlCollector,
2564
+ } = await buildComponentHTML(
2565
+ rootNode,
2566
+ globalComponents,
2567
+ pageComponents,
2568
+ effectiveLocale,
2569
+ config,
2570
+ slugMappings,
2571
+ pagePath,
2572
+ cmsContext,
2573
+ cmsService,
2574
+ isProductionBuild,
2575
+ injectEditorAttrs,
2576
+ );
2025
2577
 
2026
2578
  // Collect JavaScript and CSS from all components
2027
2579
  const javascript = await collectComponentJavaScript(globalComponents, pageComponents);