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