@wildo-ai/saas-website 1.1.1
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/LICENSE +34 -0
- package/dist/esm/.builder.pid +9 -0
- package/dist/esm/astro/blog-post-bridge.d.ts +324 -0
- package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -0
- package/dist/esm/astro/blog-post-bridge.js +57 -0
- package/dist/esm/astro/blog-post-bridge.js.map +1 -0
- package/dist/esm/astro/blog-post-head.renderer.d.ts +129 -0
- package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -0
- package/dist/esm/astro/blog-post-head.renderer.js +139 -0
- package/dist/esm/astro/blog-post-head.renderer.js.map +1 -0
- package/dist/esm/astro/bridge-runtime.d.ts +224 -0
- package/dist/esm/astro/bridge-runtime.d.ts.map +1 -0
- package/dist/esm/astro/bridge-runtime.js +30 -0
- package/dist/esm/astro/bridge-runtime.js.map +1 -0
- package/dist/esm/astro/collect-expected-label-keys.d.ts +65 -0
- package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -0
- package/dist/esm/astro/collect-expected-label-keys.js +101 -0
- package/dist/esm/astro/collect-expected-label-keys.js.map +1 -0
- package/dist/esm/astro/headers-renderer.d.ts +32 -0
- package/dist/esm/astro/headers-renderer.d.ts.map +1 -0
- package/dist/esm/astro/headers-renderer.js +64 -0
- package/dist/esm/astro/headers-renderer.js.map +1 -0
- package/dist/esm/astro/i18n-routing.helper.d.ts +91 -0
- package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -0
- package/dist/esm/astro/i18n-routing.helper.js +23 -0
- package/dist/esm/astro/i18n-routing.helper.js.map +1 -0
- package/dist/esm/astro/internal/html-escape.d.ts +31 -0
- package/dist/esm/astro/internal/html-escape.d.ts.map +1 -0
- package/dist/esm/astro/internal/html-escape.js +40 -0
- package/dist/esm/astro/internal/html-escape.js.map +1 -0
- package/dist/esm/astro/label-pack-loader.d.ts +134 -0
- package/dist/esm/astro/label-pack-loader.d.ts.map +1 -0
- package/dist/esm/astro/label-pack-loader.js +298 -0
- package/dist/esm/astro/label-pack-loader.js.map +1 -0
- package/dist/esm/astro/llms-txt-server.d.ts +28 -0
- package/dist/esm/astro/llms-txt-server.d.ts.map +1 -0
- package/dist/esm/astro/llms-txt-server.js +33 -0
- package/dist/esm/astro/llms-txt-server.js.map +1 -0
- package/dist/esm/astro/pick-labels-for-page.d.ts +63 -0
- package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -0
- package/dist/esm/astro/pick-labels-for-page.js +73 -0
- package/dist/esm/astro/pick-labels-for-page.js.map +1 -0
- package/dist/esm/astro/robots-renderer.d.ts +85 -0
- package/dist/esm/astro/robots-renderer.d.ts.map +1 -0
- package/dist/esm/astro/robots-renderer.js +157 -0
- package/dist/esm/astro/robots-renderer.js.map +1 -0
- package/dist/esm/astro/sitemap-coverage.d.ts +101 -0
- package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -0
- package/dist/esm/astro/sitemap-coverage.js +67 -0
- package/dist/esm/astro/sitemap-coverage.js.map +1 -0
- package/dist/esm/astro/structured-data.renderer.d.ts +21 -0
- package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -0
- package/dist/esm/astro/structured-data.renderer.js +142 -0
- package/dist/esm/astro/structured-data.renderer.js.map +1 -0
- package/dist/esm/astro/website-page-head.renderer.d.ts +93 -0
- package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -0
- package/dist/esm/astro/website-page-head.renderer.js +184 -0
- package/dist/esm/astro/website-page-head.renderer.js.map +1 -0
- package/dist/esm/astro/website-page-runtime.helper.d.ts +112 -0
- package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -0
- package/dist/esm/astro/website-page-runtime.helper.js +60 -0
- package/dist/esm/astro/website-page-runtime.helper.js.map +1 -0
- package/dist/esm/astro/website-site-context.d.ts +126 -0
- package/dist/esm/astro/website-site-context.d.ts.map +1 -0
- package/dist/esm/astro/website-site-context.js +37 -0
- package/dist/esm/astro/website-site-context.js.map +1 -0
- package/dist/esm/astro-island.d.ts +56 -0
- package/dist/esm/astro-island.d.ts.map +1 -0
- package/dist/esm/astro-island.js +61 -0
- package/dist/esm/astro-island.js.map +1 -0
- package/dist/esm/astro.d.ts +151 -0
- package/dist/esm/astro.d.ts.map +1 -0
- package/dist/esm/astro.js +151 -0
- package/dist/esm/astro.js.map +1 -0
- package/dist/esm/companion-exports.d.ts +38 -0
- package/dist/esm/companion-exports.d.ts.map +1 -0
- package/dist/esm/companion-exports.js +38 -0
- package/dist/esm/companion-exports.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteButton.d.ts +73 -0
- package/dist/esm/components/low-level/WebsiteButton.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteButton.js +68 -0
- package/dist/esm/components/low-level/WebsiteButton.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteHeading.d.ts +78 -0
- package/dist/esm/components/low-level/WebsiteHeading.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteHeading.js +41 -0
- package/dist/esm/components/low-level/WebsiteHeading.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteImage.d.ts +73 -0
- package/dist/esm/components/low-level/WebsiteImage.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteImage.js +21 -0
- package/dist/esm/components/low-level/WebsiteImage.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts +90 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.js +47 -0
- package/dist/esm/components/low-level/WebsiteInternalButton.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts +99 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.js +68 -0
- package/dist/esm/components/low-level/WebsiteInternalLink.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteLink.d.ts +50 -0
- package/dist/esm/components/low-level/WebsiteLink.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteLink.js +22 -0
- package/dist/esm/components/low-level/WebsiteLink.js.map +1 -0
- package/dist/esm/components/low-level/WebsiteText.d.ts +57 -0
- package/dist/esm/components/low-level/WebsiteText.d.ts.map +1 -0
- package/dist/esm/components/low-level/WebsiteText.js +57 -0
- package/dist/esm/components/low-level/WebsiteText.js.map +1 -0
- package/dist/esm/config/define-website-config.d.ts +83 -0
- package/dist/esm/config/define-website-config.d.ts.map +1 -0
- package/dist/esm/config/define-website-config.js +126 -0
- package/dist/esm/config/define-website-config.js.map +1 -0
- package/dist/esm/config/index.d.ts +38 -0
- package/dist/esm/config/index.d.ts.map +1 -0
- package/dist/esm/config/index.js +38 -0
- package/dist/esm/config/index.js.map +1 -0
- package/dist/esm/config/load-website-config.d.ts +136 -0
- package/dist/esm/config/load-website-config.d.ts.map +1 -0
- package/dist/esm/config/load-website-config.js +177 -0
- package/dist/esm/config/load-website-config.js.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts +251 -0
- package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -0
- package/dist/esm/config/wildo-website-config.schemas.js +302 -0
- package/dist/esm/config/wildo-website-config.schemas.js.map +1 -0
- package/dist/esm/config-loader.d.ts +35 -0
- package/dist/esm/config-loader.d.ts.map +1 -0
- package/dist/esm/config-loader.js +35 -0
- package/dist/esm/config-loader.js.map +1 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +38 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.js +77 -0
- package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +41 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +58 -0
- package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +81 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +177 -0
- package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts +40 -0
- package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsitePageContext.js +8 -0
- package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +162 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js +22 -0
- package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts +47 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.js +8 -0
- package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -0
- package/dist/esm/core/contexts/useWebsitePage.d.ts +13 -0
- package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsitePage.js +22 -0
- package/dist/esm/core/contexts/useWebsitePage.js.map +1 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +19 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -0
- package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -0
- package/dist/esm/core/contexts/useWebsiteSection.d.ts +12 -0
- package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -0
- package/dist/esm/core/contexts/useWebsiteSection.js +22 -0
- package/dist/esm/core/contexts/useWebsiteSection.js.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +17 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js +23 -0
- package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts +70 -0
- package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -0
- package/dist/esm/core/factories/define-website-page-manifest.js +51 -0
- package/dist/esm/core/factories/define-website-page-manifest.js.map +1 -0
- package/dist/esm/core/factories/define-website-section.d.ts +101 -0
- package/dist/esm/core/factories/define-website-section.d.ts.map +1 -0
- package/dist/esm/core/factories/define-website-section.js +71 -0
- package/dist/esm/core/factories/define-website-section.js.map +1 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts +23 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.js +25 -0
- package/dist/esm/core/hooks/useWebsiteDesignTokens.js.map +1 -0
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts +68 -0
- package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -0
- package/dist/esm/core/hooks/useWebsiteLabel.js +103 -0
- package/dist/esm/core/hooks/useWebsiteLabel.js.map +1 -0
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts +87 -0
- package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -0
- package/dist/esm/core/layouts/WebsitePageLayout.js +170 -0
- package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -0
- package/dist/esm/core/layouts/WebsiteSection.d.ts +91 -0
- package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -0
- package/dist/esm/core/layouts/WebsiteSection.js +26 -0
- package/dist/esm/core/layouts/WebsiteSection.js.map +1 -0
- package/dist/esm/core/routing/internal-routing.utils.d.ts +65 -0
- package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -0
- package/dist/esm/core/routing/internal-routing.utils.js +14 -0
- package/dist/esm/core/routing/internal-routing.utils.js.map +1 -0
- package/dist/esm/index.d.ts +129 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +129 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/mdx/BlogPost.d.ts +193 -0
- package/dist/esm/mdx/BlogPost.d.ts.map +1 -0
- package/dist/esm/mdx/BlogPost.js +161 -0
- package/dist/esm/mdx/BlogPost.js.map +1 -0
- package/dist/esm/mdx/blog-post-collection.config.d.ts +139 -0
- package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -0
- package/dist/esm/mdx/blog-post-collection.config.js +27 -0
- package/dist/esm/mdx/blog-post-collection.config.js.map +1 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts +12 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.js +13 -0
- package/dist/esm/mdx/blog-post-frontmatter-source.js.map +1 -0
- package/dist/esm/mdx/index.d.ts +22 -0
- package/dist/esm/mdx/index.d.ts.map +1 -0
- package/dist/esm/mdx/index.js +22 -0
- package/dist/esm/mdx/index.js.map +1 -0
- package/dist/esm/mdx/website-mdx-provider.d.ts +90 -0
- package/dist/esm/mdx/website-mdx-provider.d.ts.map +1 -0
- package/dist/esm/mdx/website-mdx-provider.js +8 -0
- package/dist/esm/mdx/website-mdx-provider.js.map +1 -0
- package/dist/esm/mdx.d.ts +38 -0
- package/dist/esm/mdx.d.ts.map +1 -0
- package/dist/esm/mdx.js +38 -0
- package/dist/esm/mdx.js.map +1 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts +75 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js +85 -0
- package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +120 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +152 -0
- package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts +73 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.js +76 -0
- package/dist/esm/schemas/label-keys/website-label-key.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts +209 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js +218 -0
- package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +431 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +438 -0
- package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts +40 -0
- package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/refs/page-ref.schemas.js +43 -0
- package/dist/esm/schemas/refs/page-ref.schemas.js.map +1 -0
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts +41 -0
- package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/refs/section-ref.schemas.js +44 -0
- package/dist/esm/schemas/refs/section-ref.schemas.js.map +1 -0
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts +63 -0
- package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -0
- package/dist/esm/schemas/sections/website-section-category.shared.js +64 -0
- package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +114 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +69 -0
- package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +100 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +355 -0
- package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +58 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js +2 -0
- package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -0
- package/dist/esm/schemas/validators/label-pack.validator.d.ts +144 -0
- package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -0
- package/dist/esm/schemas/validators/label-pack.validator.js +100 -0
- package/dist/esm/schemas/validators/label-pack.validator.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +143 -0
- package/src/__tests__/bundle-isolation.test.ts +607 -0
- package/src/astro/__tests__/blog-post-bridge.test.tsx +288 -0
- package/src/astro/__tests__/blog-post-head.renderer.test.ts +249 -0
- package/src/astro/__tests__/bridge-runtime.test.tsx +198 -0
- package/src/astro/__tests__/collect-expected-label-keys.test.ts +69 -0
- package/src/astro/__tests__/headers-renderer.test.ts +143 -0
- package/src/astro/__tests__/i18n-routing.helper.test.ts +116 -0
- package/src/astro/__tests__/label-pack-loader.test.ts +305 -0
- package/src/astro/__tests__/llms-txt-server.test.ts +127 -0
- package/src/astro/__tests__/pick-labels-for-page.test.ts +60 -0
- package/src/astro/__tests__/robots-renderer.test.ts +228 -0
- package/src/astro/__tests__/sitemap-coverage.test.ts +230 -0
- package/src/astro/__tests__/structured-data.renderer.test.ts +172 -0
- package/src/astro/__tests__/website-page-head.renderer.test.ts +424 -0
- package/src/astro/__tests__/website-page-runtime.helper.test.ts +157 -0
- package/src/astro/__tests__/website-site-context.test.ts +121 -0
- package/src/astro/blog-post-bridge.tsx +404 -0
- package/src/astro/blog-post-head.renderer.ts +293 -0
- package/src/astro/bridge-runtime.tsx +270 -0
- package/src/astro/collect-expected-label-keys.ts +113 -0
- package/src/astro/headers-renderer.ts +82 -0
- package/src/astro/i18n-routing.helper.ts +116 -0
- package/src/astro/internal/html-escape.ts +41 -0
- package/src/astro/label-pack-loader.ts +385 -0
- package/src/astro/llms-txt-server.ts +56 -0
- package/src/astro/pick-labels-for-page.ts +85 -0
- package/src/astro/robots-renderer.ts +171 -0
- package/src/astro/sitemap-coverage.ts +186 -0
- package/src/astro/structured-data.renderer.ts +171 -0
- package/src/astro/website-page-head.renderer.ts +279 -0
- package/src/astro/website-page-runtime.helper.ts +188 -0
- package/src/astro/website-site-context.ts +136 -0
- package/src/astro-island.ts +61 -0
- package/src/astro.ts +151 -0
- package/src/companion-exports.ts +38 -0
- package/src/components/low-level/WebsiteButton.tsx +118 -0
- package/src/components/low-level/WebsiteHeading.tsx +96 -0
- package/src/components/low-level/WebsiteImage.tsx +96 -0
- package/src/components/low-level/WebsiteInternalButton.tsx +172 -0
- package/src/components/low-level/WebsiteInternalLink.tsx +193 -0
- package/src/components/low-level/WebsiteLink.tsx +71 -0
- package/src/components/low-level/WebsiteText.tsx +75 -0
- package/src/components/low-level/__tests__/WebsiteInternalButton.test.tsx +169 -0
- package/src/components/low-level/__tests__/WebsiteInternalLink.test.tsx +202 -0
- package/src/components/low-level/__tests__/primitives.test.tsx +141 -0
- package/src/config/__tests__/define-website-config.test.ts +231 -0
- package/src/config/__tests__/load-website-config.test.ts +215 -0
- package/src/config/define-website-config.ts +134 -0
- package/src/config/index.ts +38 -0
- package/src/config/load-website-config.ts +229 -0
- package/src/config/wildo-website-config.schemas.ts +322 -0
- package/src/config-loader.ts +35 -0
- package/src/core/__tests__/WebsitePageLayout.test.tsx +184 -0
- package/src/core/__tests__/WebsiteSection.test.tsx +85 -0
- package/src/core/__tests__/contexts.test.tsx +133 -0
- package/src/core/__tests__/core-boundary.test.ts +85 -0
- package/src/core/__tests__/define-website-page-manifest.test.tsx +111 -0
- package/src/core/__tests__/define-website-section.test.tsx +89 -0
- package/src/core/__tests__/useWebsiteDesignTokens.test.tsx +41 -0
- package/src/core/__tests__/useWebsiteLabel.test.tsx +117 -0
- package/src/core/anonymous-session/InboundContactForm.tsx +157 -0
- package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +44 -0
- package/src/core/anonymous-session/inbound-contact-form.schema.ts +62 -0
- package/src/core/anonymous-session/website-anonymous-session-client.ts +194 -0
- package/src/core/contexts/WebsitePageContext.tsx +52 -0
- package/src/core/contexts/WebsiteRuntimeContext.tsx +175 -0
- package/src/core/contexts/WebsiteSectionContext.tsx +59 -0
- package/src/core/contexts/useWebsitePage.ts +28 -0
- package/src/core/contexts/useWebsiteRuntime.ts +34 -0
- package/src/core/contexts/useWebsiteSection.ts +28 -0
- package/src/core/external-providers/frontend-provider-registry.website.ts +62 -0
- package/src/core/factories/define-website-page-manifest.ts +102 -0
- package/src/core/factories/define-website-section.ts +128 -0
- package/src/core/hooks/useWebsiteDesignTokens.ts +26 -0
- package/src/core/hooks/useWebsiteLabel.ts +121 -0
- package/src/core/layouts/WebsitePageLayout.tsx +311 -0
- package/src/core/layouts/WebsiteSection.tsx +139 -0
- package/src/core/routing/__tests__/internal-routing.utils.test.ts +134 -0
- package/src/core/routing/internal-routing.utils.ts +77 -0
- package/src/index.ts +132 -0
- package/src/mdx/BlogPost.tsx +296 -0
- package/src/mdx/__tests__/BlogPost.test.tsx +218 -0
- package/src/mdx/__tests__/blog-post-collection.config.test.ts +127 -0
- package/src/mdx/__tests__/website-mdx-provider.test.tsx +82 -0
- package/src/mdx/blog-post-collection.config.ts +177 -0
- package/src/mdx/blog-post-frontmatter-source.ts +31 -0
- package/src/mdx/index.ts +39 -0
- package/src/mdx/website-mdx-provider.tsx +95 -0
- package/src/mdx.ts +38 -0
- package/src/schemas/__tests__/blog-frontmatter.test.ts +50 -0
- package/src/schemas/__tests__/companion-exports-react-free.test.ts +51 -0
- package/src/schemas/__tests__/design-tokens.test.ts +163 -0
- package/src/schemas/__tests__/label-key.test.ts +46 -0
- package/src/schemas/__tests__/label-pack-validator.test.ts +146 -0
- package/src/schemas/__tests__/page-manifest.test.ts +66 -0
- package/src/schemas/__tests__/refs.test.ts +37 -0
- package/src/schemas/__tests__/root-config.test.ts +292 -0
- package/src/schemas/__tests__/section-definition.test.ts +47 -0
- package/src/schemas/__tests__/structured-data-reconciliation.test.ts +543 -0
- package/src/schemas/blog/blog-post-frontmatter.shared.schemas.ts +111 -0
- package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +166 -0
- package/src/schemas/label-keys/website-label-key.schemas.ts +88 -0
- package/src/schemas/manifests/website-page-manifest.shared.schemas.ts +266 -0
- package/src/schemas/manifests/website-root-config.shared.schemas.ts +504 -0
- package/src/schemas/refs/page-ref.schemas.ts +49 -0
- package/src/schemas/refs/section-ref.schemas.ts +50 -0
- package/src/schemas/sections/website-section-category.shared.ts +65 -0
- package/src/schemas/sections/website-section-definition.shared.schemas.ts +120 -0
- package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +579 -0
- package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +62 -0
- package/src/schemas/validators/label-pack.validator.ts +201 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Copyright (c) 2026 P2C Consulting, s.à r.l.-S
|
|
2
|
+
RCS Luxembourg B307591
|
|
3
|
+
6C, Porte de France, L-4360 Esch-sur-Alzette, Luxembourg
|
|
4
|
+
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
This software and its accompanying materials (the "Software") are the
|
|
8
|
+
confidential and proprietary property of P2C Consulting, s.à r.l.-S (the
|
|
9
|
+
"Licensor"). The Software is made available for viewing only. No licence,
|
|
10
|
+
right, or permission of any kind is granted by its publication or by its
|
|
11
|
+
availability on any package registry or source repository.
|
|
12
|
+
|
|
13
|
+
Without the Licensor's prior written permission, you may not use, copy,
|
|
14
|
+
reproduce, modify, adapt, translate, merge, publish, distribute, sublicense,
|
|
15
|
+
sell, host, or create derivative works of the Software, in whole or in part,
|
|
16
|
+
nor use it to provide a service to any third party.
|
|
17
|
+
|
|
18
|
+
Publication of the Software in any public registry is for distribution to
|
|
19
|
+
authorised recipients and for technical evaluation of its packaging only. It
|
|
20
|
+
does not place the Software in the public domain, does not constitute an open
|
|
21
|
+
source release, and does not waive any right of the Licensor.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
27
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
28
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
29
|
+
|
|
30
|
+
Third-party components distributed with the Software remain governed by their
|
|
31
|
+
own licences.
|
|
32
|
+
|
|
33
|
+
Contact: P2C Consulting, s.à r.l.-S, 6C, Porte de France, L-4360
|
|
34
|
+
Esch-sur-Alzette, Luxembourg.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"writerPid": 95147,
|
|
4
|
+
"orchestratorPid": 94847,
|
|
5
|
+
"orchestratorSessionId": null,
|
|
6
|
+
"orchestratorStartedAt": "Mon Aug 31 09:18:19 2026",
|
|
7
|
+
"distDirectory": "/Users/paulcayon/Projects/Wildo.ai/dev/wildo-ai/engine/saas-website/dist/esm",
|
|
8
|
+
"acquiredAt": "2026-08-31T07:18:20.760Z"
|
|
9
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import type { ComponentType, FunctionComponent } from 'react';
|
|
2
|
+
import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
|
|
3
|
+
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
4
|
+
import { BLOG_POST_PAGE_REF } from '../mdx/BlogPost';
|
|
5
|
+
import type { WebsiteMdxComponentsMap } from '../mdx/website-mdx-provider';
|
|
6
|
+
import type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
|
|
7
|
+
import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
|
|
8
|
+
import type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';
|
|
9
|
+
import type { LabelPack } from '../schemas/validators/label-pack.validator';
|
|
10
|
+
/**
|
|
11
|
+
* @wildo_source:part:start saas.website.blog.bridge facet:layer:astro facet:family:website
|
|
12
|
+
*
|
|
13
|
+
* `createWebsiteBlogPostBridgeIsland(deps)` — factory that returns the
|
|
14
|
+
* React island a consumer's per-locale `*.bridge.tsx` module exports for
|
|
15
|
+
* mounting blog post pages. **Sibling** of `createWebsitePageBridgeIsland`
|
|
16
|
+
* but adapted to the dynamic-routing constraint of the marketing-blog
|
|
17
|
+
* adapter (Phase 7, D-6 = Option A).
|
|
18
|
+
*
|
|
19
|
+
* # Why blog posts cannot reuse the page bridge factory
|
|
20
|
+
*
|
|
21
|
+
* Three structural differences between marketing pages and blog posts
|
|
22
|
+
* forced this dedicated factory shape:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Per-slug body resolution.** A marketing page (`landing`,
|
|
25
|
+
* `pricing`, etc.) has ONE `PageComponent` known at bridge-module
|
|
26
|
+
* load time — the page bridge closes over it. A blog post's body
|
|
27
|
+
* is the compiled MDX module, which varies per slug. Closing the
|
|
28
|
+
* MDX body inside the factory at module load time would force one
|
|
29
|
+
* bridge file per post (defeats Q16=B "one MDX file per post").
|
|
30
|
+
* Instead, the factory closes over a `slugToMdxBody: Record<string,
|
|
31
|
+
* ComponentType>` map — typically built from
|
|
32
|
+
* `import.meta.glob('./content/blog/<locale>/*.mdx', { eager: true })`
|
|
33
|
+
* on the consumer side — and the island accepts the active `slug`
|
|
34
|
+
* as a primitive (JSON-serializable) prop.
|
|
35
|
+
*
|
|
36
|
+
* 2. **Layout already wrapped by `<BlogPost>`.** The page bridge wraps
|
|
37
|
+
* its `PageComponent` in `<WebsitePageLayout pageRef={…}>`. The
|
|
38
|
+
* `<BlogPost>` renderer ALREADY wraps its tree in
|
|
39
|
+
* `<WebsitePageLayout pageRef={BLOG_POST_PAGE_REF}>` (the framework
|
|
40
|
+
* reserves the page ref for the entire blog surface — see
|
|
41
|
+
* `BLOG_POST_PAGE_REF` JSDoc in `BlogPost.tsx`). Reusing the page
|
|
42
|
+
* bridge would double-wrap the layout. This factory instead
|
|
43
|
+
* provides the runtime context and yields directly to `<BlogPost>`,
|
|
44
|
+
* which owns the layout boundary itself.
|
|
45
|
+
*
|
|
46
|
+
* 3. **Astro's React-children context-propagation gap.** Astro
|
|
47
|
+
* renders React component children INDEPENDENTLY of the parent
|
|
48
|
+
* component when the boundary is `.astro` ↔ `.tsx`, even on the
|
|
49
|
+
* server side. This means
|
|
50
|
+
* `<BlogPost client:load><Content /></BlogPost>` written in an
|
|
51
|
+
* `.astro` file would NOT propagate the `<WebsiteMdxProvider>`
|
|
52
|
+
* context to the React components embedded inside `<Content />`
|
|
53
|
+
* (the embedded `<CTASection />` would render WITHOUT the engine's
|
|
54
|
+
* MDX provider scope; the `mdxComponents` map would be ignored).
|
|
55
|
+
* Confirmed via the Astro docs, `@astrojs/react` integration
|
|
56
|
+
* readme, and the documented "Bridging two JSX runtimes" gotcha.
|
|
57
|
+
*
|
|
58
|
+
* The fix: the entire blog post tree (BlogPost + MDX body +
|
|
59
|
+
* embedded section components) lives inside ONE React island. The
|
|
60
|
+
* MDX module is imported as a React component via Astro's
|
|
61
|
+
* `import.meta.glob`, and the bridge renders it as JSX children
|
|
62
|
+
* of `<BlogPost>` from INSIDE the island — no `.astro` ↔ `.tsx`
|
|
63
|
+
* boundary is crossed during the body render.
|
|
64
|
+
*
|
|
65
|
+
* # Why hydration is required (vs. pure SSR)
|
|
66
|
+
*
|
|
67
|
+
* The `WebsiteRuntimeContextProvider` + `<BlogPost>` tree could in
|
|
68
|
+
* theory render on the server only (no `client:*` directive). But:
|
|
69
|
+
*
|
|
70
|
+
* - Embedded sections (`<CTASection />`) typically have their own
|
|
71
|
+
* interactivity (analytics handlers, A/B variant flippers, etc.)
|
|
72
|
+
* that need React event listeners to attach client-side.
|
|
73
|
+
* - The framework's `useWebsiteLabel(...)` hook chain assumes a live
|
|
74
|
+
* React render tree — a pure-SSR tree would freeze label resolution
|
|
75
|
+
* at build time, but any future client-driven label override (e.g.
|
|
76
|
+
* a debug-mode label inspector) would silently break.
|
|
77
|
+
*
|
|
78
|
+
* The factory therefore returns an island that the consumer's `.astro`
|
|
79
|
+
* mounts with `client:load` (or `client:idle` for posts known to be
|
|
80
|
+
* non-interactive). The hydration cost is bounded by the per-locale MDX
|
|
81
|
+
* glob — see "Bundle scaling cliff" below.
|
|
82
|
+
*
|
|
83
|
+
* # Bundle scaling cliff (deferred trade-off)
|
|
84
|
+
*
|
|
85
|
+
* `import.meta.glob({ eager: true })` materializes EVERY post's
|
|
86
|
+
* compiled JS into the per-locale React-island bundle (~5–20 KB per
|
|
87
|
+
* post depending on length and embedded section count). For the
|
|
88
|
+
* Phase 7 dogfood (≤10 posts) this is fine and matches the documented
|
|
89
|
+
* Astro+MDX pattern. Past ~50 posts this becomes a real budget concern.
|
|
90
|
+
* The factory's API is forward-compatible with `{ eager: false }`
|
|
91
|
+
* (lazy loaders): swap the consumer's glob to lazy mode and the
|
|
92
|
+
* `slugToMdxBody[slug]` lookup returns a `() => Promise<{ default:
|
|
93
|
+
* ComponentType }>`; wrap in `React.lazy()` + `<Suspense>` and the
|
|
94
|
+
* factory call site stays unchanged. The migration is a consumer-side
|
|
95
|
+
* change, not an engine API break.
|
|
96
|
+
*
|
|
97
|
+
* # Authoring pattern
|
|
98
|
+
*
|
|
99
|
+
* **Per-locale bridge file** (consumer side, ONE per locale):
|
|
100
|
+
*
|
|
101
|
+
* ```tsx
|
|
102
|
+
* // examples/<app>/website/src/pages-impl/blog-post-en.bridge.tsx
|
|
103
|
+
* import type { ComponentType } from 'react';
|
|
104
|
+
*
|
|
105
|
+
* import { createWebsiteBlogPostBridgeIsland }
|
|
106
|
+
* from '@wildo-ai/saas-website/mdx';
|
|
107
|
+
* import { BLOG_POST_MDX_COMPONENTS }
|
|
108
|
+
* from './blog-post.mdx-components';
|
|
109
|
+
* import { WEBSITE_ROOT_CONFIG } from '../website-root.config';
|
|
110
|
+
*
|
|
111
|
+
* // Static analyzable glob — Vite resolves at build time. The glob
|
|
112
|
+
* // pattern MUST be a single string literal (no concatenation, no
|
|
113
|
+
* // variable interpolation); Vite parses it AST-side. The `as` cast
|
|
114
|
+
* // narrows the unknown record to the per-MDX-file shape Astro's mdx
|
|
115
|
+
* // integration emits (default export = the compiled React component).
|
|
116
|
+
* const rawModules = import.meta.glob('./content/blog/en/*.mdx', {
|
|
117
|
+
* eager: true,
|
|
118
|
+
* }) as Record<string, { default: ComponentType }>;
|
|
119
|
+
*
|
|
120
|
+
* const slugToMdxBody: Record<string, ComponentType> = {};
|
|
121
|
+
* for (const [path, mod] of Object.entries(rawModules)) {
|
|
122
|
+
* // './content/blog/en/why-context.mdx' -> 'why-context'
|
|
123
|
+
* const slug = path.split('/').pop()!.replace(/\.mdx$/, '');
|
|
124
|
+
* slugToMdxBody[slug] = mod.default;
|
|
125
|
+
* }
|
|
126
|
+
*
|
|
127
|
+
* export const BlogPostEnBridgeIsland =
|
|
128
|
+
* createWebsiteBlogPostBridgeIsland({
|
|
129
|
+
* slugToMdxBody,
|
|
130
|
+
* navigation: WEBSITE_ROOT_CONFIG.navigation,
|
|
131
|
+
* mdxComponents: BLOG_POST_MDX_COMPONENTS,
|
|
132
|
+
* });
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* **Per-locale `[slug].astro` shell** (consumer side):
|
|
136
|
+
*
|
|
137
|
+
* ```astro
|
|
138
|
+
* ---
|
|
139
|
+
* import { getCollection } from 'astro:content';
|
|
140
|
+
* import {
|
|
141
|
+
* loadWebsitePageRuntime,
|
|
142
|
+
* renderBlogPostHeadHtml,
|
|
143
|
+
* renderWebsiteDesignTokensCss,
|
|
144
|
+
* } from '@wildo-ai/saas-website/astro';
|
|
145
|
+
* import { parseBlogPostFrontmatter } from '@wildo-ai/saas-website/mdx';
|
|
146
|
+
* import { BlogPostEnBridgeIsland } from '@/pages-impl/blog-post-en.bridge';
|
|
147
|
+
* import { BLOG_POST_PAGE_MANIFEST } from '@/pages-impl/blog-post.page';
|
|
148
|
+
* import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';
|
|
149
|
+
*
|
|
150
|
+
* export async function getStaticPaths() {
|
|
151
|
+
* const posts = await getCollection('blog');
|
|
152
|
+
* const defaultLocale = WEBSITE_SITE_CONTEXT.websiteConfig.defaultLocale;
|
|
153
|
+
* return posts
|
|
154
|
+
* .filter((p) => p.data.locale === defaultLocale)
|
|
155
|
+
* .map((p) => ({ params: { slug: p.data.slug }, props: { entry: p } }));
|
|
156
|
+
* }
|
|
157
|
+
* const { entry } = Astro.props;
|
|
158
|
+
*
|
|
159
|
+
* const runtime = loadWebsitePageRuntime({
|
|
160
|
+
* manifest: BLOG_POST_PAGE_MANIFEST,
|
|
161
|
+
* siteContext: WEBSITE_SITE_CONTEXT,
|
|
162
|
+
* labelPacksDir: './src/i18n',
|
|
163
|
+
* astroLocale: Astro.currentLocale,
|
|
164
|
+
* });
|
|
165
|
+
* const frontmatter = parseBlogPostFrontmatter(entry.data, { source: entry.id });
|
|
166
|
+
* const headHtml = renderBlogPostHeadHtml({
|
|
167
|
+
* siteContext: WEBSITE_SITE_CONTEXT,
|
|
168
|
+
* locale: runtime.locale,
|
|
169
|
+
* routePath: `/blog/${frontmatter.slug}`,
|
|
170
|
+
* frontmatter,
|
|
171
|
+
* });
|
|
172
|
+
* const designTokensCss = renderWebsiteDesignTokensCss(
|
|
173
|
+
* WEBSITE_SITE_CONTEXT.rootConfig.designTokens,
|
|
174
|
+
* );
|
|
175
|
+
* ---
|
|
176
|
+
* <!doctype html>
|
|
177
|
+
* <html lang={runtime.locale}>
|
|
178
|
+
* <head>
|
|
179
|
+
* <meta charset="UTF-8" />
|
|
180
|
+
* <meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
181
|
+
* <Fragment set:html={headHtml} />
|
|
182
|
+
* <style set:html={designTokensCss}></style>
|
|
183
|
+
* </head>
|
|
184
|
+
* <body>
|
|
185
|
+
* <BlogPostEnBridgeIsland
|
|
186
|
+
* client:load
|
|
187
|
+
* slug={frontmatter.slug}
|
|
188
|
+
* rawFrontmatter={entry.data}
|
|
189
|
+
* source={entry.id}
|
|
190
|
+
* locale={runtime.locale}
|
|
191
|
+
* defaultLocale={runtime.defaultLocale}
|
|
192
|
+
* labelPack={runtime.labelPack}
|
|
193
|
+
* designTokens={WEBSITE_SITE_CONTEXT.rootConfig.designTokens}
|
|
194
|
+
* skipToMainContentLabelKey={
|
|
195
|
+
* WEBSITE_SITE_CONTEXT.rootConfig.skipToMainContentLabelKey
|
|
196
|
+
* }
|
|
197
|
+
* />
|
|
198
|
+
* </body>
|
|
199
|
+
* </html>
|
|
200
|
+
* ```
|
|
201
|
+
*
|
|
202
|
+
* # Boundary
|
|
203
|
+
*
|
|
204
|
+
* Strictly client-bundle-safe (re-exported from `mdx.ts` — NOT from
|
|
205
|
+
* `astro-island.ts`, because this module hard-imports the MDX adapter
|
|
206
|
+
* whose `@mdx-js/react` peer is optional; walked by the
|
|
207
|
+
* bundle-isolation test). MUST stay clear of `node:*` and SSR-tier
|
|
208
|
+
* specifiers.
|
|
209
|
+
*/
|
|
210
|
+
/**
|
|
211
|
+
* Module-load-time dependencies — closed over inside the returned
|
|
212
|
+
* island. NEVER cross the hydration boundary as serialized props.
|
|
213
|
+
*/
|
|
214
|
+
export interface CreateWebsiteBlogPostBridgeIslandDeps {
|
|
215
|
+
/**
|
|
216
|
+
* Per-slug map of compiled MDX bodies (each entry is a React
|
|
217
|
+
* component — the `.default` export of the MDX module). Typically
|
|
218
|
+
* built from `import.meta.glob('./content/blog/<locale>/*.mdx',
|
|
219
|
+
* { eager: true })` with the path-key normalized to the slug. Closed
|
|
220
|
+
* over at module-load time so the entire MDX body graph for the
|
|
221
|
+
* locale lands inside ONE React island bundle (D-6 Option A) — this
|
|
222
|
+
* is the structural fix for Astro's React-children context-propagation
|
|
223
|
+
* gap (see file preamble).
|
|
224
|
+
*
|
|
225
|
+
* The map is read by `slugToMdxBody[props.slug]` at island render
|
|
226
|
+
* time. A missing slug throws a clear framework-owned error (rather than
|
|
227
|
+
* rendering an empty body silently); the consumer's `getStaticPaths`
|
|
228
|
+
* is responsible for not emitting routes for slugs absent from the
|
|
229
|
+
* map.
|
|
230
|
+
*
|
|
231
|
+
* **Why `ComponentType<any>` and not a stricter generic**: matches
|
|
232
|
+
* the upstream `@mdx-js/react` convention (`MDXContent` from
|
|
233
|
+
* `mdx/types` is parameterized over an open `MDXProps` shape). MDX
|
|
234
|
+
* compilation hands the body a compiler-managed props object that
|
|
235
|
+
* the type system cannot narrow at the framework boundary; using
|
|
236
|
+
* `<any>` here keeps the consumer's
|
|
237
|
+
* `import.meta.glob(...) as Record<string, { default: ComponentType }>`
|
|
238
|
+
* cast assignable without an additional wrap/unwrap. Same posture
|
|
239
|
+
* as `WebsiteMdxComponentsMap` (intentional symmetry).
|
|
240
|
+
*/
|
|
241
|
+
slugToMdxBody: Readonly<Record<string, ComponentType<any>>>;
|
|
242
|
+
/**
|
|
243
|
+
* Site-wide navigation slot bindings (header / footer components).
|
|
244
|
+
* Closed over here for the same reason as the page bridge: both
|
|
245
|
+
* fields are `ComponentType` refs that cannot survive Astro's
|
|
246
|
+
* serialization boundary.
|
|
247
|
+
*/
|
|
248
|
+
navigation: WebsiteNavigationConfig;
|
|
249
|
+
/**
|
|
250
|
+
* MDX-tag → React-component map forwarded into `<BlogPost>` and
|
|
251
|
+
* scoped via `<WebsiteMdxProvider>`. Typically a small object
|
|
252
|
+
* literal exporting the consumer-authored business sections that
|
|
253
|
+
* blog posts can embed (`<CTASection />`, etc.). Closed over here
|
|
254
|
+
* (rather than passed as an island prop) because the values are
|
|
255
|
+
* `ComponentType` refs.
|
|
256
|
+
*/
|
|
257
|
+
mdxComponents?: WebsiteMdxComponentsMap;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Hydration-boundary props — what the consumer's `.astro` page passes
|
|
261
|
+
* to `<BlogPostEnBridgeIsland client:load ...{primitiveProps}/>`.
|
|
262
|
+
* Every field is JSON-serializable.
|
|
263
|
+
*/
|
|
264
|
+
export interface WebsiteBlogPostBridgeIslandProps {
|
|
265
|
+
/**
|
|
266
|
+
* Slug of the post being rendered. Used by the island to look up
|
|
267
|
+
* the matching MDX body in `deps.slugToMdxBody`. Carried as a plain
|
|
268
|
+
* string (not the branded `BlogPostSlug`) because Astro's
|
|
269
|
+
* serialization boundary unwraps brands.
|
|
270
|
+
*/
|
|
271
|
+
slug: string;
|
|
272
|
+
/**
|
|
273
|
+
* Raw frontmatter object Astro hands the page from `entry.data`.
|
|
274
|
+
* Forwarded straight to `<BlogPost>` for strict Zod-4 validation.
|
|
275
|
+
* The bridge does NOT re-validate (single validation surface, owned
|
|
276
|
+
* by the renderer).
|
|
277
|
+
*/
|
|
278
|
+
rawFrontmatter: unknown;
|
|
279
|
+
/**
|
|
280
|
+
* Diagnostic source identifier echoed into validation errors —
|
|
281
|
+
* typically `entry.id` from Astro's content collection. Optional
|
|
282
|
+
* but recommended for actionable error messages.
|
|
283
|
+
*/
|
|
284
|
+
source?: string;
|
|
285
|
+
/** The locale Astro resolved (with default-locale fallback). */
|
|
286
|
+
locale: AvailableLanguage;
|
|
287
|
+
/**
|
|
288
|
+
* The site's default locale. Required for `WebsiteInternalLink`
|
|
289
|
+
* locale-prefix logic (mirrors the page bridge's `defaultLocale`
|
|
290
|
+
* island prop).
|
|
291
|
+
*/
|
|
292
|
+
defaultLocale: AvailableLanguage;
|
|
293
|
+
/** Already-loaded + already-shape-validated label pack. */
|
|
294
|
+
labelPack: LabelPack;
|
|
295
|
+
/** Design-token bundle from `WebsiteRootConfig.designTokens`. */
|
|
296
|
+
designTokens: WebsiteDesignTokens;
|
|
297
|
+
/**
|
|
298
|
+
* Fully-composed label key the framework reads to render the
|
|
299
|
+
* "Skip to main content" affordance. Mirrors
|
|
300
|
+
* `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as a
|
|
301
|
+
* primitive island prop for the same reason as the page bridge.
|
|
302
|
+
*/
|
|
303
|
+
skipToMainContentLabelKey: WebsiteLabelKey;
|
|
304
|
+
/** Optional website-facing public providers materialized at build time. */
|
|
305
|
+
frontendProviders?: FrontendProvidersBlock;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function
|
|
309
|
+
* component — Astro hydrates nothing else here — and the union is not callable, so declaring it
|
|
310
|
+
* cost callers the ability to invoke the island directly (which the bridge suites do, to observe
|
|
311
|
+
* the synchronous throw for an unregistered slug without routing it through React's reconciler
|
|
312
|
+
* error path). `FunctionComponent` is assignable to `ComponentType`, so no consumer narrows.
|
|
313
|
+
*/
|
|
314
|
+
export declare function createWebsiteBlogPostBridgeIsland(deps: CreateWebsiteBlogPostBridgeIslandDeps): FunctionComponent<WebsiteBlogPostBridgeIslandProps>;
|
|
315
|
+
/**
|
|
316
|
+
* Re-export of the framework-reserved blog-post page ref. Consumers
|
|
317
|
+
* authoring `BLOG_POST_PAGE_MANIFEST` need this constant to satisfy
|
|
318
|
+
* the manifest's `ref` field; re-exporting it from the bridge module
|
|
319
|
+
* means the consumer's per-locale bridge file can grab both symbols
|
|
320
|
+
* from the one `@wildo-ai/saas-website/mdx` import it already has.
|
|
321
|
+
*/
|
|
322
|
+
export { BLOG_POST_PAGE_REF };
|
|
323
|
+
/** @wildo_source:part:end saas.website.blog.bridge */
|
|
324
|
+
//# sourceMappingURL=blog-post-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-post-bridge.d.ts","sourceRoot":"","sources":["../../../../src/astro/blog-post-bridge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAa,MAAM,OAAO,CAAC;AAEzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAO9E,OAAO,EAAY,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAC3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+DAA+D,CAAC;AACzG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yDAAyD,CAAC;AACvG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuMG;AAEH;;;GAGG;AACH,MAAM,WAAW,qCAAqC;IACpD;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5D;;;;;OAKG;IACH,UAAU,EAAE,uBAAuB,CAAC;IACpC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,uBAAuB,CAAC;CACzC;AAED;;;;GAIG;AACH,MAAM,WAAW,gCAAgC;IAC/C;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;OAIG;IACH,aAAa,EAAE,iBAAiB,CAAC;IACjC,2DAA2D;IAC3D,SAAS,EAAE,SAAS,CAAC;IACrB,iEAAiE;IACjE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;OAKG;IACH,yBAAyB,EAAE,eAAe,CAAC;IAC3C,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;CAC5C;AAID;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,qCAAqC,GAC1C,iBAAiB,CAAC,gCAAgC,CAAC,CAiErD;AAED;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAC9B,sDAAsD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { WebsiteRuntimeContextProvider, } from '../core/contexts/WebsiteRuntimeContext.js';
|
|
3
|
+
import { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website.js';
|
|
4
|
+
import { BlogPost, BLOG_POST_PAGE_REF } from '../mdx/BlogPost.js';
|
|
5
|
+
const EMPTY_MDX_COMPONENTS = Object.freeze({});
|
|
6
|
+
/**
|
|
7
|
+
* Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function
|
|
8
|
+
* component — Astro hydrates nothing else here — and the union is not callable, so declaring it
|
|
9
|
+
* cost callers the ability to invoke the island directly (which the bridge suites do, to observe
|
|
10
|
+
* the synchronous throw for an unregistered slug without routing it through React's reconciler
|
|
11
|
+
* error path). `FunctionComponent` is assignable to `ComponentType`, so no consumer narrows.
|
|
12
|
+
*/
|
|
13
|
+
export function createWebsiteBlogPostBridgeIsland(deps) {
|
|
14
|
+
const { slugToMdxBody, navigation, mdxComponents = EMPTY_MDX_COMPONENTS, } = deps;
|
|
15
|
+
function WebsiteBlogPostBridgeIsland({ slug, rawFrontmatter, source, locale, defaultLocale, labelPack, designTokens, skipToMainContentLabelKey, frontendProviders, }) {
|
|
16
|
+
const MdxBody = slugToMdxBody[slug];
|
|
17
|
+
if (MdxBody === undefined) {
|
|
18
|
+
/**
|
|
19
|
+
* Loud failure — the consumer's `getStaticPaths` is supposed to
|
|
20
|
+
* only emit routes for slugs present in the MDX glob. A missing
|
|
21
|
+
* lookup means the glob and the collection are out of sync; the
|
|
22
|
+
* operator needs an actionable error string with the offending
|
|
23
|
+
* slug + the keys that ARE present (truncated to keep the build
|
|
24
|
+
* log readable).
|
|
25
|
+
*/
|
|
26
|
+
const knownKeys = Object.keys(slugToMdxBody);
|
|
27
|
+
const known = knownKeys.length <= 5
|
|
28
|
+
? knownKeys.join(', ')
|
|
29
|
+
: `${knownKeys.slice(0, 5).join(', ')}, … (${knownKeys.length} total)`;
|
|
30
|
+
throw new Error(`[blog-post-bridge] No MDX body registered for slug="${slug}". ` +
|
|
31
|
+
`The bridge factory's slugToMdxBody map only knows: ${known}. ` +
|
|
32
|
+
`Verify the consumer's import.meta.glob('...content/blog/<locale>/*.mdx') ` +
|
|
33
|
+
`pattern matches the source file naming.`);
|
|
34
|
+
}
|
|
35
|
+
const runtimeValue = {
|
|
36
|
+
locale,
|
|
37
|
+
defaultLocale,
|
|
38
|
+
labelPack,
|
|
39
|
+
designTokens,
|
|
40
|
+
navigation,
|
|
41
|
+
skipToMainContentLabelKey,
|
|
42
|
+
frontendProviderRegistry: createFrontendWebsiteProviderRegistry(frontendProviders),
|
|
43
|
+
};
|
|
44
|
+
return (_jsx(WebsiteRuntimeContextProvider, { value: runtimeValue, children: _jsx(BlogPost, { rawFrontmatter: rawFrontmatter, source: source, mdxComponents: mdxComponents, children: _jsx(MdxBody, {}) }) }));
|
|
45
|
+
}
|
|
46
|
+
WebsiteBlogPostBridgeIsland.displayName = 'WebsiteBlogPostBridgeIsland';
|
|
47
|
+
return WebsiteBlogPostBridgeIsland;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Re-export of the framework-reserved blog-post page ref. Consumers
|
|
51
|
+
* authoring `BLOG_POST_PAGE_MANIFEST` need this constant to satisfy
|
|
52
|
+
* the manifest's `ref` field; re-exporting it from the bridge module
|
|
53
|
+
* means the consumer's per-locale bridge file can grab both symbols
|
|
54
|
+
* from the one `@wildo-ai/saas-website/mdx` import it already has.
|
|
55
|
+
*/
|
|
56
|
+
export { BLOG_POST_PAGE_REF };
|
|
57
|
+
//# sourceMappingURL=blog-post-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-post-bridge.js","sourceRoot":"","sources":["../../../../src/astro/blog-post-bridge.tsx"],"names":[],"mappings":";AAKA,OAAO,EACL,6BAA6B,GAE9B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,+DAA+D,CAAC;AACtH,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAmT/D,MAAM,oBAAoB,GAA4B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,UAAU,iCAAiC,CAC/C,IAA2C;IAE3C,MAAM,EACJ,aAAa,EACb,UAAU,EACV,aAAa,GAAG,oBAAoB,GACrC,GAAG,IAAI,CAAC;IAET,SAAS,2BAA2B,CAAC,EACnC,IAAI,EACJ,cAAc,EACd,MAAM,EACN,MAAM,EACN,aAAa,EACb,SAAS,EACT,YAAY,EACZ,yBAAyB,EACzB,iBAAiB,GACgB;QACjC,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B;;;;;;;eAOG;YACH,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YAC7C,MAAM,KAAK,GACT,SAAS,CAAC,MAAM,IAAI,CAAC;gBACnB,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;gBACtB,CAAC,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,SAAS,CAAC,MAAM,SAAS,CAAC;YAC3E,MAAM,IAAI,KAAK,CACb,uDAAuD,IAAI,KAAK;gBAC9D,sDAAsD,KAAK,IAAI;gBAC/D,2EAA2E;gBAC3E,yCAAyC,CAC5C,CAAC;QACJ,CAAC;QAED,MAAM,YAAY,GAA+B;YAC/C,MAAM;YACN,aAAa;YACb,SAAS;YACT,YAAY;YACZ,UAAU;YACV,yBAAyB;YACzB,wBAAwB,EAAE,qCAAqC,CAAC,iBAAiB,CAAC;SACnF,CAAC;QACF,OAAO,CACL,KAAC,6BAA6B,IAAC,KAAK,EAAE,YAAY,YAChD,KAAC,QAAQ,IACP,cAAc,EAAE,cAAc,EAC9B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,aAAa,YAE5B,KAAC,OAAO,KAAG,GACF,GACmB,CACjC,CAAC;IACJ,CAAC;IACD,2BAA2B,CAAC,WAAW,GAAG,6BAA6B,CAAC;IAExE,OAAO,2BAA2B,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,CAAC","sourcesContent":["import type { ComponentType, FunctionComponent, ReactNode } from 'react';\n\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport {\n WebsiteRuntimeContextProvider,\n type WebsiteRuntimeContextValue,\n} from '../core/contexts/WebsiteRuntimeContext';\nimport { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';\nimport { BlogPost, BLOG_POST_PAGE_REF } from '../mdx/BlogPost';\nimport type { WebsiteMdxComponentsMap } from '../mdx/website-mdx-provider';\nimport type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\n/**\n * @wildo_source:part:start saas.website.blog.bridge facet:layer:astro facet:family:website\n *\n * `createWebsiteBlogPostBridgeIsland(deps)` — factory that returns the\n * React island a consumer's per-locale `*.bridge.tsx` module exports for\n * mounting blog post pages. **Sibling** of `createWebsitePageBridgeIsland`\n * but adapted to the dynamic-routing constraint of the marketing-blog\n * adapter (Phase 7, D-6 = Option A).\n *\n * # Why blog posts cannot reuse the page bridge factory\n *\n * Three structural differences between marketing pages and blog posts\n * forced this dedicated factory shape:\n *\n * 1. **Per-slug body resolution.** A marketing page (`landing`,\n * `pricing`, etc.) has ONE `PageComponent` known at bridge-module\n * load time — the page bridge closes over it. A blog post's body\n * is the compiled MDX module, which varies per slug. Closing the\n * MDX body inside the factory at module load time would force one\n * bridge file per post (defeats Q16=B \"one MDX file per post\").\n * Instead, the factory closes over a `slugToMdxBody: Record<string,\n * ComponentType>` map — typically built from\n * `import.meta.glob('./content/blog/<locale>/*.mdx', { eager: true })`\n * on the consumer side — and the island accepts the active `slug`\n * as a primitive (JSON-serializable) prop.\n *\n * 2. **Layout already wrapped by `<BlogPost>`.** The page bridge wraps\n * its `PageComponent` in `<WebsitePageLayout pageRef={…}>`. The\n * `<BlogPost>` renderer ALREADY wraps its tree in\n * `<WebsitePageLayout pageRef={BLOG_POST_PAGE_REF}>` (the framework\n * reserves the page ref for the entire blog surface — see\n * `BLOG_POST_PAGE_REF` JSDoc in `BlogPost.tsx`). Reusing the page\n * bridge would double-wrap the layout. This factory instead\n * provides the runtime context and yields directly to `<BlogPost>`,\n * which owns the layout boundary itself.\n *\n * 3. **Astro's React-children context-propagation gap.** Astro\n * renders React component children INDEPENDENTLY of the parent\n * component when the boundary is `.astro` ↔ `.tsx`, even on the\n * server side. This means\n * `<BlogPost client:load><Content /></BlogPost>` written in an\n * `.astro` file would NOT propagate the `<WebsiteMdxProvider>`\n * context to the React components embedded inside `<Content />`\n * (the embedded `<CTASection />` would render WITHOUT the engine's\n * MDX provider scope; the `mdxComponents` map would be ignored).\n * Confirmed via the Astro docs, `@astrojs/react` integration\n * readme, and the documented \"Bridging two JSX runtimes\" gotcha.\n *\n * The fix: the entire blog post tree (BlogPost + MDX body +\n * embedded section components) lives inside ONE React island. The\n * MDX module is imported as a React component via Astro's\n * `import.meta.glob`, and the bridge renders it as JSX children\n * of `<BlogPost>` from INSIDE the island — no `.astro` ↔ `.tsx`\n * boundary is crossed during the body render.\n *\n * # Why hydration is required (vs. pure SSR)\n *\n * The `WebsiteRuntimeContextProvider` + `<BlogPost>` tree could in\n * theory render on the server only (no `client:*` directive). But:\n *\n * - Embedded sections (`<CTASection />`) typically have their own\n * interactivity (analytics handlers, A/B variant flippers, etc.)\n * that need React event listeners to attach client-side.\n * - The framework's `useWebsiteLabel(...)` hook chain assumes a live\n * React render tree — a pure-SSR tree would freeze label resolution\n * at build time, but any future client-driven label override (e.g.\n * a debug-mode label inspector) would silently break.\n *\n * The factory therefore returns an island that the consumer's `.astro`\n * mounts with `client:load` (or `client:idle` for posts known to be\n * non-interactive). The hydration cost is bounded by the per-locale MDX\n * glob — see \"Bundle scaling cliff\" below.\n *\n * # Bundle scaling cliff (deferred trade-off)\n *\n * `import.meta.glob({ eager: true })` materializes EVERY post's\n * compiled JS into the per-locale React-island bundle (~5–20 KB per\n * post depending on length and embedded section count). For the\n * Phase 7 dogfood (≤10 posts) this is fine and matches the documented\n * Astro+MDX pattern. Past ~50 posts this becomes a real budget concern.\n * The factory's API is forward-compatible with `{ eager: false }`\n * (lazy loaders): swap the consumer's glob to lazy mode and the\n * `slugToMdxBody[slug]` lookup returns a `() => Promise<{ default:\n * ComponentType }>`; wrap in `React.lazy()` + `<Suspense>` and the\n * factory call site stays unchanged. The migration is a consumer-side\n * change, not an engine API break.\n *\n * # Authoring pattern\n *\n * **Per-locale bridge file** (consumer side, ONE per locale):\n *\n * ```tsx\n * // examples/<app>/website/src/pages-impl/blog-post-en.bridge.tsx\n * import type { ComponentType } from 'react';\n *\n * import { createWebsiteBlogPostBridgeIsland }\n * from '@wildo-ai/saas-website/mdx';\n * import { BLOG_POST_MDX_COMPONENTS }\n * from './blog-post.mdx-components';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * // Static analyzable glob — Vite resolves at build time. The glob\n * // pattern MUST be a single string literal (no concatenation, no\n * // variable interpolation); Vite parses it AST-side. The `as` cast\n * // narrows the unknown record to the per-MDX-file shape Astro's mdx\n * // integration emits (default export = the compiled React component).\n * const rawModules = import.meta.glob('./content/blog/en/*.mdx', {\n * eager: true,\n * }) as Record<string, { default: ComponentType }>;\n *\n * const slugToMdxBody: Record<string, ComponentType> = {};\n * for (const [path, mod] of Object.entries(rawModules)) {\n * // './content/blog/en/why-context.mdx' -> 'why-context'\n * const slug = path.split('/').pop()!.replace(/\\.mdx$/, '');\n * slugToMdxBody[slug] = mod.default;\n * }\n *\n * export const BlogPostEnBridgeIsland =\n * createWebsiteBlogPostBridgeIsland({\n * slugToMdxBody,\n * navigation: WEBSITE_ROOT_CONFIG.navigation,\n * mdxComponents: BLOG_POST_MDX_COMPONENTS,\n * });\n * ```\n *\n * **Per-locale `[slug].astro` shell** (consumer side):\n *\n * ```astro\n * ---\n * import { getCollection } from 'astro:content';\n * import {\n * loadWebsitePageRuntime,\n * renderBlogPostHeadHtml,\n * renderWebsiteDesignTokensCss,\n * } from '@wildo-ai/saas-website/astro';\n * import { parseBlogPostFrontmatter } from '@wildo-ai/saas-website/mdx';\n * import { BlogPostEnBridgeIsland } from '@/pages-impl/blog-post-en.bridge';\n * import { BLOG_POST_PAGE_MANIFEST } from '@/pages-impl/blog-post.page';\n * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';\n *\n * export async function getStaticPaths() {\n * const posts = await getCollection('blog');\n * const defaultLocale = WEBSITE_SITE_CONTEXT.websiteConfig.defaultLocale;\n * return posts\n * .filter((p) => p.data.locale === defaultLocale)\n * .map((p) => ({ params: { slug: p.data.slug }, props: { entry: p } }));\n * }\n * const { entry } = Astro.props;\n *\n * const runtime = loadWebsitePageRuntime({\n * manifest: BLOG_POST_PAGE_MANIFEST,\n * siteContext: WEBSITE_SITE_CONTEXT,\n * labelPacksDir: './src/i18n',\n * astroLocale: Astro.currentLocale,\n * });\n * const frontmatter = parseBlogPostFrontmatter(entry.data, { source: entry.id });\n * const headHtml = renderBlogPostHeadHtml({\n * siteContext: WEBSITE_SITE_CONTEXT,\n * locale: runtime.locale,\n * routePath: `/blog/${frontmatter.slug}`,\n * frontmatter,\n * });\n * const designTokensCss = renderWebsiteDesignTokensCss(\n * WEBSITE_SITE_CONTEXT.rootConfig.designTokens,\n * );\n * ---\n * <!doctype html>\n * <html lang={runtime.locale}>\n * <head>\n * <meta charset=\"UTF-8\" />\n * <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n * <Fragment set:html={headHtml} />\n * <style set:html={designTokensCss}></style>\n * </head>\n * <body>\n * <BlogPostEnBridgeIsland\n * client:load\n * slug={frontmatter.slug}\n * rawFrontmatter={entry.data}\n * source={entry.id}\n * locale={runtime.locale}\n * defaultLocale={runtime.defaultLocale}\n * labelPack={runtime.labelPack}\n * designTokens={WEBSITE_SITE_CONTEXT.rootConfig.designTokens}\n * skipToMainContentLabelKey={\n * WEBSITE_SITE_CONTEXT.rootConfig.skipToMainContentLabelKey\n * }\n * />\n * </body>\n * </html>\n * ```\n *\n * # Boundary\n *\n * Strictly client-bundle-safe (re-exported from `mdx.ts` — NOT from\n * `astro-island.ts`, because this module hard-imports the MDX adapter\n * whose `@mdx-js/react` peer is optional; walked by the\n * bundle-isolation test). MUST stay clear of `node:*` and SSR-tier\n * specifiers.\n */\n\n/**\n * Module-load-time dependencies — closed over inside the returned\n * island. NEVER cross the hydration boundary as serialized props.\n */\nexport interface CreateWebsiteBlogPostBridgeIslandDeps {\n /**\n * Per-slug map of compiled MDX bodies (each entry is a React\n * component — the `.default` export of the MDX module). Typically\n * built from `import.meta.glob('./content/blog/<locale>/*.mdx',\n * { eager: true })` with the path-key normalized to the slug. Closed\n * over at module-load time so the entire MDX body graph for the\n * locale lands inside ONE React island bundle (D-6 Option A) — this\n * is the structural fix for Astro's React-children context-propagation\n * gap (see file preamble).\n *\n * The map is read by `slugToMdxBody[props.slug]` at island render\n * time. A missing slug throws a clear framework-owned error (rather than\n * rendering an empty body silently); the consumer's `getStaticPaths`\n * is responsible for not emitting routes for slugs absent from the\n * map.\n *\n * **Why `ComponentType<any>` and not a stricter generic**: matches\n * the upstream `@mdx-js/react` convention (`MDXContent` from\n * `mdx/types` is parameterized over an open `MDXProps` shape). MDX\n * compilation hands the body a compiler-managed props object that\n * the type system cannot narrow at the framework boundary; using\n * `<any>` here keeps the consumer's\n * `import.meta.glob(...) as Record<string, { default: ComponentType }>`\n * cast assignable without an additional wrap/unwrap. Same posture\n * as `WebsiteMdxComponentsMap` (intentional symmetry).\n */\n slugToMdxBody: Readonly<Record<string, ComponentType<any>>>;\n /**\n * Site-wide navigation slot bindings (header / footer components).\n * Closed over here for the same reason as the page bridge: both\n * fields are `ComponentType` refs that cannot survive Astro's\n * serialization boundary.\n */\n navigation: WebsiteNavigationConfig;\n /**\n * MDX-tag → React-component map forwarded into `<BlogPost>` and\n * scoped via `<WebsiteMdxProvider>`. Typically a small object\n * literal exporting the consumer-authored business sections that\n * blog posts can embed (`<CTASection />`, etc.). Closed over here\n * (rather than passed as an island prop) because the values are\n * `ComponentType` refs.\n */\n mdxComponents?: WebsiteMdxComponentsMap;\n}\n\n/**\n * Hydration-boundary props — what the consumer's `.astro` page passes\n * to `<BlogPostEnBridgeIsland client:load ...{primitiveProps}/>`.\n * Every field is JSON-serializable.\n */\nexport interface WebsiteBlogPostBridgeIslandProps {\n /**\n * Slug of the post being rendered. Used by the island to look up\n * the matching MDX body in `deps.slugToMdxBody`. Carried as a plain\n * string (not the branded `BlogPostSlug`) because Astro's\n * serialization boundary unwraps brands.\n */\n slug: string;\n /**\n * Raw frontmatter object Astro hands the page from `entry.data`.\n * Forwarded straight to `<BlogPost>` for strict Zod-4 validation.\n * The bridge does NOT re-validate (single validation surface, owned\n * by the renderer).\n */\n rawFrontmatter: unknown;\n /**\n * Diagnostic source identifier echoed into validation errors —\n * typically `entry.id` from Astro's content collection. Optional\n * but recommended for actionable error messages.\n */\n source?: string;\n /** The locale Astro resolved (with default-locale fallback). */\n locale: AvailableLanguage;\n /**\n * The site's default locale. Required for `WebsiteInternalLink`\n * locale-prefix logic (mirrors the page bridge's `defaultLocale`\n * island prop).\n */\n defaultLocale: AvailableLanguage;\n /** Already-loaded + already-shape-validated label pack. */\n labelPack: LabelPack;\n /** Design-token bundle from `WebsiteRootConfig.designTokens`. */\n designTokens: WebsiteDesignTokens;\n /**\n * Fully-composed label key the framework reads to render the\n * \"Skip to main content\" affordance. Mirrors\n * `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as a\n * primitive island prop for the same reason as the page bridge.\n */\n skipToMainContentLabelKey: WebsiteLabelKey;\n /** Optional website-facing public providers materialized at build time. */\n frontendProviders?: FrontendProvidersBlock;\n}\n\nconst EMPTY_MDX_COMPONENTS: WebsiteMdxComponentsMap = Object.freeze({});\n\n/**\n * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function\n * component — Astro hydrates nothing else here — and the union is not callable, so declaring it\n * cost callers the ability to invoke the island directly (which the bridge suites do, to observe\n * the synchronous throw for an unregistered slug without routing it through React's reconciler\n * error path). `FunctionComponent` is assignable to `ComponentType`, so no consumer narrows.\n */\nexport function createWebsiteBlogPostBridgeIsland(\n deps: CreateWebsiteBlogPostBridgeIslandDeps,\n): FunctionComponent<WebsiteBlogPostBridgeIslandProps> {\n const {\n slugToMdxBody,\n navigation,\n mdxComponents = EMPTY_MDX_COMPONENTS,\n } = deps;\n\n function WebsiteBlogPostBridgeIsland({\n slug,\n rawFrontmatter,\n source,\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n skipToMainContentLabelKey,\n frontendProviders,\n }: WebsiteBlogPostBridgeIslandProps): ReactNode {\n const MdxBody = slugToMdxBody[slug];\n if (MdxBody === undefined) {\n /**\n * Loud failure — the consumer's `getStaticPaths` is supposed to\n * only emit routes for slugs present in the MDX glob. A missing\n * lookup means the glob and the collection are out of sync; the\n * operator needs an actionable error string with the offending\n * slug + the keys that ARE present (truncated to keep the build\n * log readable).\n */\n const knownKeys = Object.keys(slugToMdxBody);\n const known =\n knownKeys.length <= 5\n ? knownKeys.join(', ')\n : `${knownKeys.slice(0, 5).join(', ')}, … (${knownKeys.length} total)`;\n throw new Error(\n `[blog-post-bridge] No MDX body registered for slug=\"${slug}\". ` +\n `The bridge factory's slugToMdxBody map only knows: ${known}. ` +\n `Verify the consumer's import.meta.glob('...content/blog/<locale>/*.mdx') ` +\n `pattern matches the source file naming.`,\n );\n }\n\n const runtimeValue: WebsiteRuntimeContextValue = {\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n navigation,\n skipToMainContentLabelKey,\n frontendProviderRegistry: createFrontendWebsiteProviderRegistry(frontendProviders),\n };\n return (\n <WebsiteRuntimeContextProvider value={runtimeValue}>\n <BlogPost\n rawFrontmatter={rawFrontmatter}\n source={source}\n mdxComponents={mdxComponents}\n >\n <MdxBody />\n </BlogPost>\n </WebsiteRuntimeContextProvider>\n );\n }\n WebsiteBlogPostBridgeIsland.displayName = 'WebsiteBlogPostBridgeIsland';\n\n return WebsiteBlogPostBridgeIsland;\n}\n\n/**\n * Re-export of the framework-reserved blog-post page ref. Consumers\n * authoring `BLOG_POST_PAGE_MANIFEST` need this constant to satisfy\n * the manifest's `ref` field; re-exporting it from the bridge module\n * means the consumer's per-locale bridge file can grab both symbols\n * from the one `@wildo-ai/saas-website/mdx` import it already has.\n */\nexport { BLOG_POST_PAGE_REF };\n/** @wildo_source:part:end saas.website.blog.bridge */\n"]}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
import type { BlogPostFrontmatter } from '../schemas/blog/blog-post-frontmatter.shared.schemas';
|
|
3
|
+
import type { WebsiteStructuredDataPayload } from '../schemas/structured-data/website-structured-data.shared.schemas';
|
|
4
|
+
import type { WebsiteSiteContext } from './website-site-context';
|
|
5
|
+
/**
|
|
6
|
+
* @wildo_source:part:start saas.website.blog.head-renderer facet:layer:astro facet:family:website
|
|
7
|
+
*
|
|
8
|
+
* `renderBlogPostHeadHtml(opts)` — sibling of `renderWebsitePageHeadHtml`
|
|
9
|
+
* that emits the SEO `<head>` block for an individual blog post,
|
|
10
|
+
* sourcing per-render copy from a validated `BlogPostFrontmatter`
|
|
11
|
+
* instead of the page manifest's `metaLabelKeys`.
|
|
12
|
+
*
|
|
13
|
+
* # Why a separate renderer (not an overload of the page-head renderer)
|
|
14
|
+
*
|
|
15
|
+
* The framework-mindset rule "every type / bucket / field must carry
|
|
16
|
+
* its semantic weight" pushes against overloading
|
|
17
|
+
* `renderWebsitePageHeadHtml` with a "blog-post mode" flag:
|
|
18
|
+
*
|
|
19
|
+
* - The two renderers have STRUCTURALLY different inputs.
|
|
20
|
+
* `renderWebsitePageHeadHtml` reads `manifest.metaLabelKeys.{titleKey,
|
|
21
|
+
* descriptionKey, ogImageUrl}` + the matching `labelPack` lookup;
|
|
22
|
+
* this renderer reads `frontmatter.{title, summary, coverImageUrl,
|
|
23
|
+
* publishedAt, authorRef, tags}` directly — no label-pack
|
|
24
|
+
* dependency at all (blog body copy is per-post, not
|
|
25
|
+
* per-locale-pack, by design).
|
|
26
|
+
* - The two renderers emit DIFFERENT meta surfaces.
|
|
27
|
+
* `og:type=website` vs. `og:type=article`; the article variant
|
|
28
|
+
* additionally emits `article:published_time`,
|
|
29
|
+
* `article:author`, and `article:tag` (one per `frontmatter.tags`
|
|
30
|
+
* entry). Folding both into one helper would require an
|
|
31
|
+
* `articleMeta?: { ... }` discriminated branch that carries no
|
|
32
|
+
* value when called from a non-blog page — exactly the kind of
|
|
33
|
+
* "convenience union" the framework rules forbid.
|
|
34
|
+
* - The blog-post route itself uses a parameterized `routePath`
|
|
35
|
+
* (`/blog/[slug]`) that needs the SLUG substituted at render time
|
|
36
|
+
* to produce the absolute canonical/hreflang URLs. Page-level
|
|
37
|
+
* routes are always literal — keeping the substitution in this
|
|
38
|
+
* renderer (rather than at the manifest layer) means the page
|
|
39
|
+
* manifest stays free of per-render concerns.
|
|
40
|
+
*
|
|
41
|
+
* Both renderers share the exact same hreflang/canonical/OG locale
|
|
42
|
+
* composition logic — duplicating ~30 lines of stable URL composition
|
|
43
|
+
* is the lesser evil compared to threading a generic options bag
|
|
44
|
+
* through both. If a third renderer ever lands (a "campaign-page" or
|
|
45
|
+
* "tag-page" head emitter, say), the right move is to extract a
|
|
46
|
+
* shared `composeHreflangAndCanonical(...)` helper at that point;
|
|
47
|
+
* doing it now would be premature against the YAGNI line.
|
|
48
|
+
*
|
|
49
|
+
* # The `routePath` argument
|
|
50
|
+
*
|
|
51
|
+
* The blog-post page manifest's `routePath` is `/blog/[slug]` (Astro
|
|
52
|
+
* route-parameter syntax). At render time the consumer's `[slug].astro`
|
|
53
|
+
* has already resolved the slug, so we accept the FULLY-SUBSTITUTED
|
|
54
|
+
* locale-neutral path (e.g. `/blog/why-context-beats-content`) here.
|
|
55
|
+
* `buildLocalizedRoutePath` then handles the locale-prefix decision,
|
|
56
|
+
* mirroring how `WebsiteInternalLink` and `renderWebsitePageHeadHtml`
|
|
57
|
+
* handle their own routes — the locale-prefix rule never lives in two
|
|
58
|
+
* places.
|
|
59
|
+
*
|
|
60
|
+
* # Failure modes
|
|
61
|
+
*
|
|
62
|
+
* Throws nothing — all required frontmatter fields are guaranteed by
|
|
63
|
+
* `parseBlogPostFrontmatter` upstream (same Zod 4 schema). The
|
|
64
|
+
* optional `coverImageUrl` is the only branch: when absent, the
|
|
65
|
+
* Twitter card kind degrades to `summary` (text-only) and `og:image`
|
|
66
|
+
* is omitted entirely.
|
|
67
|
+
*/
|
|
68
|
+
export interface RenderBlogPostHeadHtmlOptions {
|
|
69
|
+
/**
|
|
70
|
+
* Composed `WebsiteSiteContext` — the renderer reads
|
|
71
|
+
* `websiteConfig.{defaultLocale, locales, publicOrigin}` for the
|
|
72
|
+
* URL/locale surface and `rootConfig.seo.{siteName}` for the
|
|
73
|
+
* brand fallback. `seo.defaultOgImageUrl` is intentionally NOT
|
|
74
|
+
* consulted: blog posts that ship without a `coverImageUrl` get a
|
|
75
|
+
* smaller-card degradation rather than a generic site-wide image
|
|
76
|
+
* (the default OG image is meaningful for marketing pages but
|
|
77
|
+
* misleading for blog posts).
|
|
78
|
+
*/
|
|
79
|
+
siteContext: WebsiteSiteContext;
|
|
80
|
+
/**
|
|
81
|
+
* The locale Astro resolved (with default-locale fallback) for the
|
|
82
|
+
* current request. Drives the `<html lang>` value upstream + the
|
|
83
|
+
* `og:locale` line and the canonical URL's locale-prefix decision.
|
|
84
|
+
*/
|
|
85
|
+
locale: AvailableLanguage;
|
|
86
|
+
/**
|
|
87
|
+
* The fully-substituted locale-neutral route path for the post —
|
|
88
|
+
* typically `/blog/<slug>` with `<slug>` already resolved by the
|
|
89
|
+
* consumer's `[slug].astro` `getStaticPaths()`. Passing the
|
|
90
|
+
* substituted path (rather than the parameterized one) keeps the
|
|
91
|
+
* renderer agnostic of the consumer's route-parameter naming.
|
|
92
|
+
*/
|
|
93
|
+
routePath: string;
|
|
94
|
+
/**
|
|
95
|
+
* Validated frontmatter — the result of `parseBlogPostFrontmatter`.
|
|
96
|
+
* Carrying the parsed (branded) shape here means an unvalidated
|
|
97
|
+
* `entry.data` cannot reach this renderer accidentally (the
|
|
98
|
+
* branded-type signature would reject a raw `unknown`).
|
|
99
|
+
*/
|
|
100
|
+
frontmatter: BlogPostFrontmatter;
|
|
101
|
+
/**
|
|
102
|
+
* AI-training opt-out (D-AG3) — when `true`, the renderer emits
|
|
103
|
+
* `<meta name="robots" content="noai, noimageai">`. The sibling
|
|
104
|
+
* `_headers` entry is produced by `renderBlogPostNoaiHeaders()`
|
|
105
|
+
* (one knob, two channels — see the page-head renderer's
|
|
106
|
+
* companion).
|
|
107
|
+
*
|
|
108
|
+
* **Why a renderer-level option, not a frontmatter field**: the
|
|
109
|
+
* blog-post frontmatter schema (`BlogPostFrontmatterSchema`)
|
|
110
|
+
* doesn't yet expose a per-post AI-opt-out knob — when it does,
|
|
111
|
+
* the consumer's `[slug].astro` will read it from the parsed
|
|
112
|
+
* frontmatter and pass it through here. Keeping the option at
|
|
113
|
+
* the renderer surface NOW means a third-party blog-post catalog
|
|
114
|
+
* (campaigns, customer stories) can opt out without authoring a
|
|
115
|
+
* new frontmatter schema first.
|
|
116
|
+
*/
|
|
117
|
+
noaiOptOut?: boolean;
|
|
118
|
+
}
|
|
119
|
+
export interface BuildBlogPostStructuredDataPayloadOptions {
|
|
120
|
+
frontmatter: BlogPostFrontmatter;
|
|
121
|
+
canonicalUrl: string;
|
|
122
|
+
siteOrigin: string;
|
|
123
|
+
siteName: string;
|
|
124
|
+
locale: AvailableLanguage;
|
|
125
|
+
}
|
|
126
|
+
export declare function buildBlogPostStructuredDataPayload(opts: BuildBlogPostStructuredDataPayloadOptions): WebsiteStructuredDataPayload;
|
|
127
|
+
export declare function renderBlogPostHeadHtml(opts: RenderBlogPostHeadHtmlOptions): string;
|
|
128
|
+
/** @wildo_source:part:end saas.website.blog.head-renderer */
|
|
129
|
+
//# sourceMappingURL=blog-post-head.renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blog-post-head.renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/blog-post-head.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAG9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAChG,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,mEAAmE,CAAC;AAItH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAEH,MAAM,WAAW,6BAA6B;IAC5C;;;;;;;;;OASG;IACH,WAAW,EAAE,kBAAkB,CAAC;IAChC;;;;OAIG;IACH,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,WAAW,EAAE,mBAAmB,CAAC;IACjC;;;;;;;;;;;;;;;OAeG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,yCAAyC;IACxD,WAAW,EAAE,mBAAmB,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,yCAAyC,GAC9C,4BAA4B,CAuB9B;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,6BAA6B,GAAG,MAAM,CAkIlF;AACD,6DAA6D"}
|