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