@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
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
4
|
+
|
|
5
|
+
import { buildLocalizedRoutePath } from '../internal-routing.utils';
|
|
6
|
+
|
|
7
|
+
describe('buildLocalizedRoutePath', () => {
|
|
8
|
+
describe('default locale (un-prefixed routing)', () => {
|
|
9
|
+
it('returns the route verbatim for the default locale', () => {
|
|
10
|
+
expect(
|
|
11
|
+
buildLocalizedRoutePath({
|
|
12
|
+
locale: AvailableLanguage.EN,
|
|
13
|
+
defaultLocale: AvailableLanguage.EN,
|
|
14
|
+
routePath: '/pricing',
|
|
15
|
+
}),
|
|
16
|
+
).toBe('/pricing');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns "/" verbatim for the default-locale root', () => {
|
|
20
|
+
expect(
|
|
21
|
+
buildLocalizedRoutePath({
|
|
22
|
+
locale: AvailableLanguage.EN,
|
|
23
|
+
defaultLocale: AvailableLanguage.EN,
|
|
24
|
+
routePath: '/',
|
|
25
|
+
}),
|
|
26
|
+
).toBe('/');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('non-default locale (prefixed routing)', () => {
|
|
31
|
+
it('prefixes a typical route with /<locale>', () => {
|
|
32
|
+
expect(
|
|
33
|
+
buildLocalizedRoutePath({
|
|
34
|
+
locale: AvailableLanguage.FR,
|
|
35
|
+
defaultLocale: AvailableLanguage.EN,
|
|
36
|
+
routePath: '/pricing',
|
|
37
|
+
}),
|
|
38
|
+
).toBe('/fr/pricing');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('collapses the root path to /<locale> (NOT /<locale>/)', () => {
|
|
42
|
+
expect(
|
|
43
|
+
buildLocalizedRoutePath({
|
|
44
|
+
locale: AvailableLanguage.FR,
|
|
45
|
+
defaultLocale: AvailableLanguage.EN,
|
|
46
|
+
routePath: '/',
|
|
47
|
+
}),
|
|
48
|
+
).toBe('/fr');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('handles deeper routes', () => {
|
|
52
|
+
expect(
|
|
53
|
+
buildLocalizedRoutePath({
|
|
54
|
+
locale: AvailableLanguage.FR,
|
|
55
|
+
defaultLocale: AvailableLanguage.EN,
|
|
56
|
+
routePath: '/blog/launch',
|
|
57
|
+
}),
|
|
58
|
+
).toBe('/fr/blog/launch');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Phase 6 fourth audit fix M-Κ — coverage gap follow-ups for the
|
|
64
|
+
* three "tricky path shape" cases the original test grid skipped.
|
|
65
|
+
* `buildLocalizedRoutePath` is a low-level prefixer that does NOT
|
|
66
|
+
* try to interpret query strings, hash fragments, or already-
|
|
67
|
+
* prefixed paths — those are the responsibility of the calling
|
|
68
|
+
* primitive (`WebsiteInternalLink` / `WebsiteInternalButton`,
|
|
69
|
+
* which both throw on already-prefixed paths). These tests pin
|
|
70
|
+
* the helper's literal behavior so a future "be smart" refactor
|
|
71
|
+
* cannot silently change the contract.
|
|
72
|
+
*/
|
|
73
|
+
describe('path shape edge cases (M-Κ — pin literal behavior)', () => {
|
|
74
|
+
it('preserves a query string by suffix-pasting (no parsing)', () => {
|
|
75
|
+
expect(
|
|
76
|
+
buildLocalizedRoutePath({
|
|
77
|
+
locale: AvailableLanguage.FR,
|
|
78
|
+
defaultLocale: AvailableLanguage.EN,
|
|
79
|
+
routePath: '/pricing?plan=pro',
|
|
80
|
+
}),
|
|
81
|
+
).toBe('/fr/pricing?plan=pro');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('preserves a hash fragment by suffix-pasting (no parsing)', () => {
|
|
85
|
+
expect(
|
|
86
|
+
buildLocalizedRoutePath({
|
|
87
|
+
locale: AvailableLanguage.FR,
|
|
88
|
+
defaultLocale: AvailableLanguage.EN,
|
|
89
|
+
routePath: '/pricing#tiers',
|
|
90
|
+
}),
|
|
91
|
+
).toBe('/fr/pricing#tiers');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('preserves both query and fragment in order', () => {
|
|
95
|
+
expect(
|
|
96
|
+
buildLocalizedRoutePath({
|
|
97
|
+
locale: AvailableLanguage.FR,
|
|
98
|
+
defaultLocale: AvailableLanguage.EN,
|
|
99
|
+
routePath: '/pricing?plan=pro#tiers',
|
|
100
|
+
}),
|
|
101
|
+
).toBe('/fr/pricing?plan=pro#tiers');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('DOES NOT detect already-prefixed paths — produces a double-prefix on FR pages', () => {
|
|
105
|
+
// Documents the helper's deliberate dumb-prefix contract: the
|
|
106
|
+
// caller is responsible for guarding against accidental locale
|
|
107
|
+
// prefixes. `WebsiteInternalLink` / `WebsiteInternalButton`
|
|
108
|
+
// both throw before reaching this helper when the routePath
|
|
109
|
+
// begins with a known locale segment (see their
|
|
110
|
+
// `detectAccidentalLocalePrefix` guard).
|
|
111
|
+
expect(
|
|
112
|
+
buildLocalizedRoutePath({
|
|
113
|
+
locale: AvailableLanguage.FR,
|
|
114
|
+
defaultLocale: AvailableLanguage.EN,
|
|
115
|
+
routePath: '/fr/pricing',
|
|
116
|
+
}),
|
|
117
|
+
).toBe('/fr/fr/pricing');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('DOES NOT detect already-prefixed paths — silently passes on default-locale pages', () => {
|
|
121
|
+
// The other half of the silent-bug surface: on a default-locale
|
|
122
|
+
// page (no prefix added), an already-prefixed path renders
|
|
123
|
+
// unchanged, so the EN visitor lands on the FR page with no
|
|
124
|
+
// visible warning. Same caller-responsibility contract.
|
|
125
|
+
expect(
|
|
126
|
+
buildLocalizedRoutePath({
|
|
127
|
+
locale: AvailableLanguage.EN,
|
|
128
|
+
defaultLocale: AvailableLanguage.EN,
|
|
129
|
+
routePath: '/fr/pricing',
|
|
130
|
+
}),
|
|
131
|
+
).toBe('/fr/pricing');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @wildo_source:part:start saas.website.routing.localized-path facet:layer:core facet:family:website
|
|
5
|
+
*
|
|
6
|
+
* `buildLocalizedRoutePath(opts)` — pure, isomorphic helper that
|
|
7
|
+
* returns the locale-prefixed pathname for a given route, honoring the
|
|
8
|
+
* marketing-site routing convention enforced by `buildAstroI18nConfig`
|
|
9
|
+
* (`prefixDefaultLocale: false`).
|
|
10
|
+
*
|
|
11
|
+
* **The architectural problem this solves**: the marketing site mounts the
|
|
12
|
+
* default locale at the
|
|
13
|
+
* un-prefixed path (`/`, `/pricing`, `/contact`) and every other locale
|
|
14
|
+
* under `/<locale>/…` (`/fr/`, `/fr/pricing`, `/fr/contact`). Internal
|
|
15
|
+
* links authored as raw `href="/pricing"` therefore work on
|
|
16
|
+
* default-locale pages and SILENTLY BREAK on non-default-locale pages
|
|
17
|
+
* (a French visitor on `/fr/index` clicking "Pricing" lands on the
|
|
18
|
+
* English `/pricing`, falling out of their locale).
|
|
19
|
+
*
|
|
20
|
+
* **Why a helper rather than asking authors to construct the path**:
|
|
21
|
+
* the rule is a framework-level routing convention — every consumer
|
|
22
|
+
* authors links the same way, every consumer must apply the same
|
|
23
|
+
* prefix logic, and getting it wrong produces a silent locale-leak
|
|
24
|
+
* that test suites do not catch (they navigate inside a single
|
|
25
|
+
* locale). A single helper makes the rule the framework's
|
|
26
|
+
* responsibility, not the section author's.
|
|
27
|
+
*
|
|
28
|
+
* **Routing rule** (matches `buildAstroI18nConfig` and the head
|
|
29
|
+
* renderer's `buildAbsoluteUrl` helper, which delegates here as the
|
|
30
|
+
* single source of truth):
|
|
31
|
+
* - When `locale === defaultLocale`: return `routePath` verbatim
|
|
32
|
+
* (the un-prefixed path).
|
|
33
|
+
* - Otherwise: return `/<locale>${routePath}`, with the special
|
|
34
|
+
* case that `routePath === '/'` collapses to `/<locale>` (no
|
|
35
|
+
* trailing slash) so the locale-prefixed root does not become
|
|
36
|
+
* the awkward `/<locale>/`.
|
|
37
|
+
*
|
|
38
|
+
* **Inputs are validated by the calling layer**, not here:
|
|
39
|
+
* - `locale` is `AvailableLanguage` (branded), so the type system
|
|
40
|
+
* prevents an unknown locale from reaching this function.
|
|
41
|
+
* - `routePath` is the same `routePath` field validated by the page
|
|
42
|
+
* manifest schema (`WebsitePageRoutePathSchema` enforces it
|
|
43
|
+
* starts with `/`). We do not re-validate here — duplicate
|
|
44
|
+
* validation costs CPU on every link render with no incremental
|
|
45
|
+
* safety, and the page-manifest layer fails loud at config-load
|
|
46
|
+
* time if a malformed path leaks in.
|
|
47
|
+
*
|
|
48
|
+
* **Boundary**: pure function, zero React / DOM / fs dependencies.
|
|
49
|
+
* Safe to import from both the React-island bundle (consumed by
|
|
50
|
+
* `WebsiteInternalLink`) and the SSR head renderer (consumed by
|
|
51
|
+
* `buildAbsoluteUrl`). The package's routing tests pin this behavior.
|
|
52
|
+
*/
|
|
53
|
+
export interface BuildLocalizedRoutePathOptions {
|
|
54
|
+
/** The currently-active locale (typically from `useWebsiteRuntime().locale`). */
|
|
55
|
+
locale: AvailableLanguage;
|
|
56
|
+
/** The site's default locale (typically from `useWebsiteRuntime().defaultLocale`). */
|
|
57
|
+
defaultLocale: AvailableLanguage;
|
|
58
|
+
/**
|
|
59
|
+
* The page's route path, exactly as declared in
|
|
60
|
+
* `WebsitePageManifest.routePath` (always starts with `/`).
|
|
61
|
+
*/
|
|
62
|
+
routePath: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildLocalizedRoutePath(opts: BuildLocalizedRoutePathOptions): string {
|
|
66
|
+
const { locale, defaultLocale, routePath } = opts;
|
|
67
|
+
const localeStr = String(locale);
|
|
68
|
+
const defaultLocaleStr = String(defaultLocale);
|
|
69
|
+
if (localeStr === defaultLocaleStr) {
|
|
70
|
+
return routePath;
|
|
71
|
+
}
|
|
72
|
+
if (routePath === '/') {
|
|
73
|
+
return `/${localeStr}`;
|
|
74
|
+
}
|
|
75
|
+
return `/${localeStr}${routePath}`;
|
|
76
|
+
}
|
|
77
|
+
/** @wildo_source:part:end saas.website.routing.localized-path */
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wildo-package @wildo-ai/saas-website
|
|
3
|
+
*
|
|
4
|
+
* SaaS marketing/landing website engine — schemas + helpers + layout
|
|
5
|
+
* placeholders + Astro/MDX adapters + SEO/AEO/GEO builder library.
|
|
6
|
+
*
|
|
7
|
+
* Per `saas-website-slice.md` D6/D15: this engine ships **NO concrete
|
|
8
|
+
* business sections** (no Hero, no CTA, no Pricing). It ships the
|
|
9
|
+
* structural and helper machinery; consumers author every business
|
|
10
|
+
* section themselves via the `defineWebsiteSection` factory and the
|
|
11
|
+
* accompanying SKILLs in `framework-knowledge`.
|
|
12
|
+
*
|
|
13
|
+
* @wildo-boundary
|
|
14
|
+
* - This barrel MAY re-export from any subdirectory of `src/`.
|
|
15
|
+
* - Subdirectories MUST observe their own boundary contracts (see
|
|
16
|
+
* each subdirectory's own `index.ts` `@wildo-boundary` block):
|
|
17
|
+
* - `schemas/` : NO React, NO Astro, NO `~wildo-frontend-lib/**`
|
|
18
|
+
* - `seo/` : NO React, NO Astro
|
|
19
|
+
* - `core/` : MAY use React; NO `~wildo-frontend-lib/**` (Q17=B)
|
|
20
|
+
* - `astro/` : MAY use Astro; NO React component runtime imports
|
|
21
|
+
* - `mdx/` : MAY use `@mdx-js/*`
|
|
22
|
+
* - `integration/` : MAY use React; MAY import the
|
|
23
|
+
* `@wildo-ai/saas-models/public-runtime` SUBPATH,
|
|
24
|
+
* the `@wildo-ai/presets-components-models/public-runtime`
|
|
25
|
+
* SUBPATH, `@wildo-ai/zod-decorators`,
|
|
26
|
+
* plus the SSR / Astro-middleware tier
|
|
27
|
+
* (`axios`, `jsonwebtoken`, `socket.io-client`,
|
|
28
|
+
* `reflect-metadata`) — D12 revised
|
|
29
|
+
* 2026-04-19 (second same-day loosening);
|
|
30
|
+
* the ROOT barrels of `@wildo-ai/saas-models`
|
|
31
|
+
* and `@wildo-ai/presets-components-models`
|
|
32
|
+
* are FORBIDDEN (they boot the decorator
|
|
33
|
+
* framework / pull the heavy registry);
|
|
34
|
+
* the saas-app DI / backend stack
|
|
35
|
+
* (`inversify`, `mongoose`, `express`,
|
|
36
|
+
* `pino`, `helmet`, `argon2`, `bcrypt`,
|
|
37
|
+
* the `@wildo-ai/saas-frontend-lib` /
|
|
38
|
+
* `@wildo-ai/saas-backend-lib` /
|
|
39
|
+
* `@wildo-ai/external-connectors-models`
|
|
40
|
+
* root + subpath surfaces) is FORBIDDEN;
|
|
41
|
+
* MUST NOT import `~wildo-frontend-lib/**`
|
|
42
|
+
* (Q17=B / D16)
|
|
43
|
+
*
|
|
44
|
+
* D12 (client-bundle import discipline, all `core/`/`schemas/`/`integration/`
|
|
45
|
+
* files): the entire saas-website source tree ships into a browser bundle
|
|
46
|
+
* (Astro SSG + React islands). Every shared model atom MUST come from a
|
|
47
|
+
* side-effect-free `/public-runtime` subpath:
|
|
48
|
+
* - `@wildo-ai/saas-models/public-runtime` is the primary door
|
|
49
|
+
* (saas-models-public-runtime.md K-6 — consumers see one path).
|
|
50
|
+
* - `@wildo-ai/presets-components-models/public-runtime` is the
|
|
51
|
+
* internal seam for symbols not (yet) re-exported by saas-models.
|
|
52
|
+
* - `@wildo-ai/zod-decorators` is allowed to import (first revision
|
|
53
|
+
* 2026-04-19). The package itself is side-effect-free; only an
|
|
54
|
+
* explicit `initZodDecorators()` call mutates `z.ZodType.prototype`,
|
|
55
|
+
* which neither saas-website nor the saas-models /public-runtime
|
|
56
|
+
* barrel does.
|
|
57
|
+
* - `axios`, `jsonwebtoken`, `socket.io-client`, `reflect-metadata` are
|
|
58
|
+
* allowed for SSR / Astro-middleware code paths (second same-day
|
|
59
|
+
* revision 2026-04-19). The website engine needs them to provide
|
|
60
|
+
* middleware utilities (HTTP proxying to the SaaS backend, JWT
|
|
61
|
+
* verification of cross-frontend handoff codes, real-time push,
|
|
62
|
+
* decorator-metadata reads). They are SSR-tier — discipline
|
|
63
|
+
* (and a future strict/loose entry split for React islands vs
|
|
64
|
+
* middleware) keeps them out of the React-island bundle.
|
|
65
|
+
*
|
|
66
|
+
* The ROOT barrel of `@wildo-ai/saas-models` is forbidden everywhere in
|
|
67
|
+
* this package — it calls `initZodDecorators()` at module load and pulls
|
|
68
|
+
* `@wildo-ai/external-connectors-models` + the full schema graph into
|
|
69
|
+
* the bundle. Same forbid applies to the root barrel of
|
|
70
|
+
* `@wildo-ai/presets-components-models`, the saas-app DI runtime
|
|
71
|
+
* (`inversify`, `@wildo-ai/saas-frontend-lib`, `@wildo-ai/saas-backend-lib`),
|
|
72
|
+
* and pure-backend libs (`mongoose`, `express`, `pino`, `helmet`,
|
|
73
|
+
* `argon2`, `bcrypt`). Enforced by `src/__tests__/bundle-isolation.test.ts`.
|
|
74
|
+
*
|
|
75
|
+
* These boundaries are enforced by review + the bundle-isolation test
|
|
76
|
+
* above until Phase 8 promotes them to a programmatic ESLint check
|
|
77
|
+
* (cf. `saas-website-slice.md`
|
|
78
|
+
* §"DEFERRED to Phase 8 — boundary lint mechanism design").
|
|
79
|
+
*
|
|
80
|
+
* Phase 1+2 status: schemas + runtime contexts/hooks + layout
|
|
81
|
+
* primitives + L1 placeholders + factories are all exported. Phase 5
|
|
82
|
+
* adds the Astro adapter; Phase 5.7 the `integration/` layer.
|
|
83
|
+
*
|
|
84
|
+
* D18 (L1 boundary, Phase 2): the L1 primitives (`WebsiteHeading`,
|
|
85
|
+
* `WebsiteText`, `WebsiteButton`, `WebsiteLink`, `WebsiteImage`) are
|
|
86
|
+
* self-contained placeholders — they do NOT consume
|
|
87
|
+
* `ComponentRegistryService` from `saas-frontend-lib` and do NOT
|
|
88
|
+
* re-export `saas-frontend-lib` primitives. Consumers override at the
|
|
89
|
+
* L1 boundary by barrel-re-exporting their preferred styled variant
|
|
90
|
+
* from their app's `app/website/primitives/<Primitive>.tsx`. Phase 6
|
|
91
|
+
* dogfood will revisit and decide whether to promote a registry-based
|
|
92
|
+
* override mechanism.
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
export * from './schemas/refs/page-ref.schemas';
|
|
96
|
+
export * from './schemas/refs/section-ref.schemas';
|
|
97
|
+
export * from './schemas/label-keys/website-label-key.schemas';
|
|
98
|
+
export * from './schemas/sections/website-section-category.shared';
|
|
99
|
+
export * from './schemas/sections/website-section-definition.shared.schemas';
|
|
100
|
+
export * from './schemas/structured-data/website-structured-data.shared.schemas';
|
|
101
|
+
export * from './schemas/structured-data/structured-data-reconciliation.shared';
|
|
102
|
+
export * from './schemas/design-tokens/website-design-tokens.shared.schemas';
|
|
103
|
+
export * from './schemas/manifests/website-page-manifest.shared.schemas';
|
|
104
|
+
export * from './schemas/manifests/website-root-config.shared.schemas';
|
|
105
|
+
export * from './schemas/blog/blog-post-frontmatter.shared.schemas';
|
|
106
|
+
export * from './schemas/validators/label-pack.validator';
|
|
107
|
+
|
|
108
|
+
export * from './core/contexts/WebsiteRuntimeContext';
|
|
109
|
+
export * from './core/contexts/useWebsiteRuntime';
|
|
110
|
+
export * from './core/contexts/WebsitePageContext';
|
|
111
|
+
export * from './core/contexts/useWebsitePage';
|
|
112
|
+
export * from './core/contexts/WebsiteSectionContext';
|
|
113
|
+
export * from './core/contexts/useWebsiteSection';
|
|
114
|
+
export * from './core/hooks/useWebsiteLabel';
|
|
115
|
+
export * from './core/hooks/useWebsiteDesignTokens';
|
|
116
|
+
export * from './core/factories/define-website-section';
|
|
117
|
+
export * from './core/factories/define-website-page-manifest';
|
|
118
|
+
export * from './core/layouts/WebsitePageLayout';
|
|
119
|
+
export * from './core/layouts/WebsiteSection';
|
|
120
|
+
|
|
121
|
+
export * from './core/anonymous-session/website-anonymous-session-client';
|
|
122
|
+
export * from './core/anonymous-session/inbound-contact-form.schema';
|
|
123
|
+
export * from './core/anonymous-session/InboundContactForm';
|
|
124
|
+
|
|
125
|
+
export * from './components/low-level/WebsiteHeading';
|
|
126
|
+
export * from './components/low-level/WebsiteText';
|
|
127
|
+
export * from './components/low-level/WebsiteLink';
|
|
128
|
+
export * from './components/low-level/WebsiteInternalLink';
|
|
129
|
+
export * from './core/routing/internal-routing.utils';
|
|
130
|
+
export * from './components/low-level/WebsiteButton';
|
|
131
|
+
export * from './components/low-level/WebsiteInternalButton';
|
|
132
|
+
export * from './components/low-level/WebsiteImage';
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { WebsitePageLayout } from '../core/layouts/WebsitePageLayout';
|
|
4
|
+
import type { WebsitePageRef } from '../schemas/refs/page-ref.schemas';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
parseBlogPostFrontmatter,
|
|
8
|
+
type ParseBlogPostFrontmatterContext,
|
|
9
|
+
} from './blog-post-collection.config';
|
|
10
|
+
import {
|
|
11
|
+
WebsiteMdxProvider,
|
|
12
|
+
type WebsiteMdxComponentsMap,
|
|
13
|
+
} from './website-mdx-provider';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @wildo_source:part:start saas.website.blog.post-renderer facet:layer:mdx facet:family:website
|
|
17
|
+
*
|
|
18
|
+
* `<BlogPost>` — engine-owned per-post page renderer for the marketing
|
|
19
|
+
* blog adapter.
|
|
20
|
+
*
|
|
21
|
+
* # What it does, end-to-end
|
|
22
|
+
*
|
|
23
|
+
* 1. **Strict frontmatter parse.** Reads the raw `entry.data` Astro
|
|
24
|
+
* hands the page (passthrough at the content-collection layer per
|
|
25
|
+
* Path Z, see `blog-post-collection.config.ts`) and runs it
|
|
26
|
+
* through `BlogPostFrontmatterSchema` (Zod 4) — branded `slug` /
|
|
27
|
+
* `authorRef` / `tags`, ISO-8601 `publishedAt`, optional
|
|
28
|
+
* `coverImageUrl`. A failure throws synchronously with a
|
|
29
|
+
* `[blog-post] (source="…")` message; Astro halts the build and
|
|
30
|
+
* points the operator to the offending MDX file.
|
|
31
|
+
* 2. **Renders `<WebsitePageLayout>` with a stable blog-post page
|
|
32
|
+
* ref.** The framework reserves `'blog-post'` as the
|
|
33
|
+
* `WebsitePageRef` for every blog post (see `BLOG_POST_PAGE_REF`
|
|
34
|
+
* below for the rationale). All blog posts share one page ref
|
|
35
|
+
* because the SECTION ref under that page is what's
|
|
36
|
+
* blog-author-controlled (see point 3) — the page-level chrome,
|
|
37
|
+
* label scope, and analytics signal are the same across the entire
|
|
38
|
+
* blog surface.
|
|
39
|
+
* 3. **Wraps the MDX body in `<WebsiteMdxProvider>`.** The consumer
|
|
40
|
+
* passes a `mdxComponents` map (typically containing one or two
|
|
41
|
+
* business-section refs they want available inside post bodies —
|
|
42
|
+
* `<CTASection ref="post-cta" />`, etc.). The provider scopes the
|
|
43
|
+
* map for the duration of the render and ONLY the duration of the
|
|
44
|
+
* render, so two blog posts on the same site can ship different
|
|
45
|
+
* provider configs without leaking.
|
|
46
|
+
* 4. **Emits a hero block (title / summary / publish date / author
|
|
47
|
+
* handle / cover image)** from the parsed frontmatter, then yields
|
|
48
|
+
* to the MDX body. Hero markup is intentionally minimal — the
|
|
49
|
+
* consumer styles it via `WebsiteDesignTokens` custom properties.
|
|
50
|
+
* No CSS file ships from this component; everything is inline
|
|
51
|
+
* style + design-token references.
|
|
52
|
+
*
|
|
53
|
+
* # Why a blog-specific page renderer (vs. the generic page bridge)
|
|
54
|
+
*
|
|
55
|
+
* The `createWebsitePageBridgeIsland` factory takes a `PageComponent`
|
|
56
|
+
* — a React component the page author writes. A blog post has NO React
|
|
57
|
+
* component; the MDX file IS the component (compiled by `@astrojs/mdx`
|
|
58
|
+
* to a default-export React tree). Reusing the page-bridge factory
|
|
59
|
+
* would force the consumer to write a ten-line wrapper per post that
|
|
60
|
+
* imports the MDX module, calls `<MdxBody />`, and re-passes the
|
|
61
|
+
* frontmatter — a cold violation of the "every blog post is one MDX
|
|
62
|
+
* file, period" principle. `<BlogPost>` is the framework's
|
|
63
|
+
* single ten-line wrapper, written once.
|
|
64
|
+
*
|
|
65
|
+
* # Why no per-post `pageRef` / per-section `sectionRef` knobs
|
|
66
|
+
*
|
|
67
|
+
* Two reasons:
|
|
68
|
+
*
|
|
69
|
+
* - **Label-key scope semantics.** A blog post's body has no
|
|
70
|
+
* framework-routed labels (the prose IS in the MDX). The section
|
|
71
|
+
* refs that DO need labels are those an MDX file embeds via the
|
|
72
|
+
* `mdxComponents` map — and those sections were already registered
|
|
73
|
+
* in the consumer's `WEBSITE_SECTIONS` registry with their
|
|
74
|
+
* own `expectedLabelKeys`. Letting blog authors pass arbitrary
|
|
75
|
+
* `sectionRef`s would create unbounded label-key surface area that
|
|
76
|
+
* `assertLabelPacksValid` cannot validate at build start.
|
|
77
|
+
* - **Analytics + chrome consistency.** Marketing teams treat "the
|
|
78
|
+
* blog" as one funnel. A single `pageRef='blog-post'` keeps the
|
|
79
|
+
* `data-website-page-ref` attribute on the layout root constant
|
|
80
|
+
* across every post, so the analytics ingest sees one blog-post
|
|
81
|
+
* page event regardless of slug. Per-post overrides would fragment
|
|
82
|
+
* this without buying any operator value.
|
|
83
|
+
*
|
|
84
|
+
* # Hydration + bundle boundary
|
|
85
|
+
*
|
|
86
|
+
* This component is NOT itself a Bridge factory — it's plain React
|
|
87
|
+
* meant to be rendered inside an Astro `.astro` page. The consumer's
|
|
88
|
+
* per-post Astro template (typically `src/pages/[locale]/blog/[slug].astro`)
|
|
89
|
+
* imports `<BlogPost>` directly and renders it; the surrounding
|
|
90
|
+
* `<WebsitePageBridgeIsland>` provides the runtime context. The MDX
|
|
91
|
+
* runtime (`@mdx-js/react`) is React-only and pulls into the React
|
|
92
|
+
* island bundle — bundle-isolation tests walk `mdx.ts` as their own
|
|
93
|
+
* entry to keep the browser-facing closure auditable.
|
|
94
|
+
*
|
|
95
|
+
* Authoring example:
|
|
96
|
+
*
|
|
97
|
+
* ```astro
|
|
98
|
+
* ---
|
|
99
|
+
* // src/pages/blog/[slug].astro
|
|
100
|
+
* import { getCollection, getEntry } from 'astro:content';
|
|
101
|
+
* import { BlogPost } from '@wildo-ai/saas-website/mdx';
|
|
102
|
+
* import { BLOG_POST_MDX_COMPONENTS } from '@/blog/mdx-components';
|
|
103
|
+
*
|
|
104
|
+
* export async function getStaticPaths() {
|
|
105
|
+
* const posts = await getCollection('blog');
|
|
106
|
+
* return posts
|
|
107
|
+
* .filter((p) => p.data.locale === 'en')
|
|
108
|
+
* .map((p) => ({ params: { slug: p.data.slug }, props: { entry: p } }));
|
|
109
|
+
* }
|
|
110
|
+
*
|
|
111
|
+
* const { entry } = Astro.props;
|
|
112
|
+
* const { Content } = await entry.render();
|
|
113
|
+
* ---
|
|
114
|
+
* <BlogPost
|
|
115
|
+
* rawFrontmatter={entry.data}
|
|
116
|
+
* source={entry.id}
|
|
117
|
+
* mdxComponents={BLOG_POST_MDX_COMPONENTS}
|
|
118
|
+
* client:load
|
|
119
|
+
* >
|
|
120
|
+
* <Content />
|
|
121
|
+
* </BlogPost>
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Reserved `WebsitePageRef` for every blog post page. Exported so
|
|
127
|
+
* consumers can register a matching `BLOG_POST_PAGE_MANIFEST` in
|
|
128
|
+
* their site root config (the page manifest carries the navigation
|
|
129
|
+
* + label-pack scope; the renderer carries the structural layout).
|
|
130
|
+
*
|
|
131
|
+
* Branded as a plain string literal (cast through `unknown`) because:
|
|
132
|
+
* - The `WebsitePageRef` brand is enforced via Zod's `.brand<>()`,
|
|
133
|
+
* and parsing a string literal at module load time would require
|
|
134
|
+
* dragging the Zod runtime into module init — gratuitous when the
|
|
135
|
+
* ref is a compile-time constant.
|
|
136
|
+
* - `WebsitePageLayout` re-parses any incoming `pageRef` string
|
|
137
|
+
* defensively (see `WebsitePageLayoutProps.pageRef` JSDoc), so the
|
|
138
|
+
* brand is re-asserted at render time anyway.
|
|
139
|
+
*/
|
|
140
|
+
export const BLOG_POST_PAGE_REF: WebsitePageRef = 'blog-post' as unknown as WebsitePageRef;
|
|
141
|
+
|
|
142
|
+
export interface BlogPostProps {
|
|
143
|
+
/**
|
|
144
|
+
* Raw frontmatter object Astro hands the page from `entry.data` (or
|
|
145
|
+
* equivalent). The renderer validates this through
|
|
146
|
+
* `BlogPostFrontmatterSchema` synchronously — invalid shapes throw
|
|
147
|
+
* with a `[blog-post]`-prefixed message. Pass `entry.data` directly;
|
|
148
|
+
* do NOT pre-parse on the consumer side (the framework owns the
|
|
149
|
+
* validation surface so a future schema change lands in ONE place).
|
|
150
|
+
*/
|
|
151
|
+
rawFrontmatter: unknown;
|
|
152
|
+
/**
|
|
153
|
+
* Diagnostic source identifier echoed into validation errors —
|
|
154
|
+
* typically `entry.id` from Astro's content collection (`'en/why-context.mdx'`
|
|
155
|
+
* or similar). Optional but strongly recommended; without it,
|
|
156
|
+
* `parseBlogPostFrontmatter` throws with a generic
|
|
157
|
+
* `[blog-post] Frontmatter failed …` message that does not name the
|
|
158
|
+
* offending file.
|
|
159
|
+
*/
|
|
160
|
+
source?: string;
|
|
161
|
+
/**
|
|
162
|
+
* MDX-tag → React-component map made available inside the post
|
|
163
|
+
* body via `<WebsiteMdxProvider>`. Typically a small object literal
|
|
164
|
+
* exporting one or two business sections the consumer wants
|
|
165
|
+
* available everywhere in the blog surface
|
|
166
|
+
* (`<CTASection ref="post-cta" />`, `<NewsletterSignup />`, etc.).
|
|
167
|
+
*
|
|
168
|
+
* Empty maps are valid — a post body using only standard markdown
|
|
169
|
+
* (no embedded React) needs no overrides.
|
|
170
|
+
*/
|
|
171
|
+
mdxComponents?: WebsiteMdxComponentsMap;
|
|
172
|
+
/**
|
|
173
|
+
* Per-page chrome overrides forwarded straight into
|
|
174
|
+
* `<WebsitePageLayout>`. `null` suppresses the slot entirely;
|
|
175
|
+
* `undefined` (the default) inherits the runtime navigation
|
|
176
|
+
* config's header/footer components. Most blog posts leave both
|
|
177
|
+
* unset to inherit the site-wide chrome.
|
|
178
|
+
*/
|
|
179
|
+
header?: ReactNode | null;
|
|
180
|
+
footer?: ReactNode | null;
|
|
181
|
+
/**
|
|
182
|
+
* The compiled MDX body — typically `<Content />` from Astro's
|
|
183
|
+
* `await entry.render()` call. Rendered inside the
|
|
184
|
+
* `<WebsiteMdxProvider>` boundary so its inline `<TagName />`
|
|
185
|
+
* references resolve against `mdxComponents`.
|
|
186
|
+
*/
|
|
187
|
+
children: ReactNode;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Empty default for `mdxComponents` — defined at module scope (NOT
|
|
192
|
+
* inline in the destructure default) so the object identity is
|
|
193
|
+
* stable across re-renders. Stable identity matters here because
|
|
194
|
+
* `<MDXProvider>`'s internal context value is `useMemo`-derived from
|
|
195
|
+
* the `components` prop; inline `{}` would invalidate every render.
|
|
196
|
+
*/
|
|
197
|
+
const EMPTY_MDX_COMPONENTS: WebsiteMdxComponentsMap = Object.freeze({});
|
|
198
|
+
|
|
199
|
+
export function BlogPost(props: BlogPostProps): ReactNode {
|
|
200
|
+
const {
|
|
201
|
+
rawFrontmatter,
|
|
202
|
+
source,
|
|
203
|
+
mdxComponents = EMPTY_MDX_COMPONENTS,
|
|
204
|
+
header,
|
|
205
|
+
footer,
|
|
206
|
+
children,
|
|
207
|
+
} = props;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build the diagnostic context once per render. We do not memoize
|
|
211
|
+
* (one object literal per render is cheaper than the `useMemo`
|
|
212
|
+
* accounting), but we also do not pass `source: undefined`
|
|
213
|
+
* unconditionally because `parseBlogPostFrontmatter` switches on
|
|
214
|
+
* `ctx.source !== undefined` for the error-suffix; passing `{}`
|
|
215
|
+
* when undefined keeps the suffix absent in that case.
|
|
216
|
+
*/
|
|
217
|
+
const parseCtx: ParseBlogPostFrontmatterContext =
|
|
218
|
+
source !== undefined ? { source } : {};
|
|
219
|
+
const frontmatter = parseBlogPostFrontmatter(rawFrontmatter, parseCtx);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The hero markup intentionally uses semantic HTML5 elements
|
|
223
|
+
* (`<header>`, `<h1>`, `<time>`, `<address>`) rather than raw
|
|
224
|
+
* `<div>`s so screen readers and crawlers see the post structure
|
|
225
|
+
* without consumer styling intervention. The styling story is:
|
|
226
|
+
* design tokens via the surrounding `WebsiteRuntimeContext` +
|
|
227
|
+
* the consumer's stylesheet — NO CSS ships from this file.
|
|
228
|
+
*/
|
|
229
|
+
return (
|
|
230
|
+
<WebsitePageLayout
|
|
231
|
+
pageRef={BLOG_POST_PAGE_REF}
|
|
232
|
+
header={header}
|
|
233
|
+
footer={footer}
|
|
234
|
+
>
|
|
235
|
+
<article data-website-blog-post-slug={frontmatter.slug as unknown as string}>
|
|
236
|
+
<header>
|
|
237
|
+
{frontmatter.coverImageUrl !== undefined ? (
|
|
238
|
+
<img
|
|
239
|
+
src={frontmatter.coverImageUrl}
|
|
240
|
+
alt=""
|
|
241
|
+
loading="eager"
|
|
242
|
+
data-website-blog-post-cover
|
|
243
|
+
/>
|
|
244
|
+
) : null}
|
|
245
|
+
<h1>{frontmatter.title}</h1>
|
|
246
|
+
<p data-website-blog-post-summary>{frontmatter.summary}</p>
|
|
247
|
+
{/*
|
|
248
|
+
* Meta line is a `<div>`, not a `<p>` — `<address>` is a flow-
|
|
249
|
+
* content element and cannot be a descendant of `<p>` per HTML5
|
|
250
|
+
* (browsers auto-close the `<p>` on `<address>`, which produces
|
|
251
|
+
* a hydration mismatch in React 19). The `<div>` keeps the
|
|
252
|
+
* semantic `<time>` + `<address>` landmarks intact for screen
|
|
253
|
+
* readers + crawlers without violating the parsing rules.
|
|
254
|
+
*/}
|
|
255
|
+
<div data-website-blog-post-meta>
|
|
256
|
+
<time dateTime={frontmatter.publishedAt}>
|
|
257
|
+
{frontmatter.publishedAt}
|
|
258
|
+
</time>
|
|
259
|
+
{' · '}
|
|
260
|
+
<address data-website-blog-post-author>
|
|
261
|
+
{frontmatter.authorRef as unknown as string}
|
|
262
|
+
</address>
|
|
263
|
+
</div>
|
|
264
|
+
{frontmatter.tags.length > 0 ? (
|
|
265
|
+
<ul data-website-blog-post-tags>
|
|
266
|
+
{frontmatter.tags.map((tag) => (
|
|
267
|
+
<li key={tag as unknown as string}>
|
|
268
|
+
{tag as unknown as string}
|
|
269
|
+
</li>
|
|
270
|
+
))}
|
|
271
|
+
</ul>
|
|
272
|
+
) : null}
|
|
273
|
+
</header>
|
|
274
|
+
<WebsiteMdxProvider components={mdxComponents}>
|
|
275
|
+
{children}
|
|
276
|
+
</WebsiteMdxProvider>
|
|
277
|
+
</article>
|
|
278
|
+
</WebsitePageLayout>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
BlogPost.displayName = 'BlogPost';
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Type alias re-export so consumers writing `BLOG_POST_MDX_COMPONENTS:
|
|
285
|
+
* BlogPostMdxComponents = { … }` have a stable name (the underlying
|
|
286
|
+
* type is the same `WebsiteMdxComponentsMap` from the provider).
|
|
287
|
+
*/
|
|
288
|
+
export type BlogPostMdxComponents = WebsiteMdxComponentsMap;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Convenience type — the Astro `ComponentType` consumers write inline
|
|
292
|
+
* inside a `mdxComponents` map. Re-exported so consumer authoring
|
|
293
|
+
* doesn't need to import from `react` directly.
|
|
294
|
+
*/
|
|
295
|
+
export type BlogPostMdxComponent = ComponentType<any>;
|
|
296
|
+
/** @wildo_source:part:end saas.website.blog.post-renderer */
|