@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,218 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
|
|
6
|
+
import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
WebsiteRuntimeContextProvider,
|
|
10
|
+
type WebsiteRuntimeContextValue,
|
|
11
|
+
} from '../../core/contexts/WebsiteRuntimeContext';
|
|
12
|
+
import { WebsiteLabelKeySchema } from '../../schemas/label-keys/website-label-key.schemas';
|
|
13
|
+
import { BlogPost, BLOG_POST_PAGE_REF } from '../BlogPost';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `<BlogPost>` integration coverage.
|
|
17
|
+
*
|
|
18
|
+
* The renderer composes three things:
|
|
19
|
+
* 1. Strict frontmatter parse via `parseBlogPostFrontmatter` (Zod 4).
|
|
20
|
+
* 2. `<WebsitePageLayout pageRef="blog-post">` for site-wide chrome
|
|
21
|
+
* + `WebsitePageContext` provisioning + the
|
|
22
|
+
* `data-website-page-ref` analytics attribute.
|
|
23
|
+
* 3. `<WebsiteMdxProvider>` around `children` so MDX-compiled
|
|
24
|
+
* `<TagName />` references resolve against `mdxComponents`.
|
|
25
|
+
*
|
|
26
|
+
* Each test focuses on one of those concerns. Render-time chrome
|
|
27
|
+
* (header/footer slot) is already exercised in `WebsitePageLayout.test.tsx`;
|
|
28
|
+
* we only assert the slots wire through correctly here.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const SkipKey = WebsiteLabelKeySchema.parse(
|
|
32
|
+
'website.chrome.layout.skip-to-main-content',
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const RuntimeHeader = (): React.ReactElement => (
|
|
36
|
+
<span data-testid="runtime-header">RH</span>
|
|
37
|
+
);
|
|
38
|
+
const RuntimeFooter = (): React.ReactElement => (
|
|
39
|
+
<span data-testid="runtime-footer">RF</span>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const RUNTIME: WebsiteRuntimeContextValue = {
|
|
43
|
+
locale: AvailableLanguage.EN,
|
|
44
|
+
defaultLocale: AvailableLanguage.EN,
|
|
45
|
+
labelPack: { 'website.chrome.layout.skip-to-main-content': 'Skip to main content' },
|
|
46
|
+
designTokens: {
|
|
47
|
+
colors: {
|
|
48
|
+
background: '#fff',
|
|
49
|
+
foreground: '#000',
|
|
50
|
+
primary: '#000',
|
|
51
|
+
primaryForeground: '#fff',
|
|
52
|
+
accent: '#000',
|
|
53
|
+
accentForeground: '#fff',
|
|
54
|
+
surface: '#fff',
|
|
55
|
+
surfaceForeground: '#000',
|
|
56
|
+
border: '#ccc',
|
|
57
|
+
muted: '#eee',
|
|
58
|
+
mutedForeground: '#666',
|
|
59
|
+
},
|
|
60
|
+
radii: { none: '0', sm: '2px', md: '4px', lg: '8px', xl: '12px', full: '9999px' },
|
|
61
|
+
},
|
|
62
|
+
navigation: {
|
|
63
|
+
headerComponent: RuntimeHeader,
|
|
64
|
+
footerComponent: RuntimeFooter,
|
|
65
|
+
},
|
|
66
|
+
skipToMainContentLabelKey: SkipKey,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const wrap = (children: React.ReactNode): React.ReactElement => (
|
|
70
|
+
<WebsiteRuntimeContextProvider value={RUNTIME}>
|
|
71
|
+
{children}
|
|
72
|
+
</WebsiteRuntimeContextProvider>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const VALID = {
|
|
76
|
+
title: 'Why Context Beats Content',
|
|
77
|
+
summary: 'Compress the loop your team actually runs every week.',
|
|
78
|
+
slug: 'why-context-beats-content',
|
|
79
|
+
locale: 'en' as const,
|
|
80
|
+
publishedAt: '2026-04-17T09:00:00.000Z',
|
|
81
|
+
authorRef: 'paul-cayon',
|
|
82
|
+
tags: ['context', 'product-strategy'],
|
|
83
|
+
coverImageUrl: 'https://cdn.wildo.ai/blog/why-context-cover.png',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe('BlogPost', () => {
|
|
87
|
+
it('exposes the reserved blog-post page ref via the WebsitePageLayout data attribute', () => {
|
|
88
|
+
const { container } = render(
|
|
89
|
+
wrap(
|
|
90
|
+
<BlogPost rawFrontmatter={VALID} source="en/why-context.mdx">
|
|
91
|
+
<p>Body</p>
|
|
92
|
+
</BlogPost>,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
expect(
|
|
96
|
+
container.querySelector(`[data-website-page-ref="${BLOG_POST_PAGE_REF as unknown as string}"]`),
|
|
97
|
+
).not.toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('renders the hero block from validated frontmatter', () => {
|
|
101
|
+
const { getByText, getByRole, container } = render(
|
|
102
|
+
wrap(
|
|
103
|
+
<BlogPost rawFrontmatter={VALID} source="en/why-context.mdx">
|
|
104
|
+
<p>Body content</p>
|
|
105
|
+
</BlogPost>,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
expect(getByRole('heading', { level: 1, name: VALID.title })).toBeInTheDocument();
|
|
109
|
+
expect(getByText(VALID.summary)).toBeInTheDocument();
|
|
110
|
+
expect(container.querySelector('time[datetime]')).toHaveAttribute(
|
|
111
|
+
'datetime',
|
|
112
|
+
VALID.publishedAt,
|
|
113
|
+
);
|
|
114
|
+
expect(container.querySelector('img[data-website-blog-post-cover]')).toHaveAttribute(
|
|
115
|
+
'src',
|
|
116
|
+
VALID.coverImageUrl,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('renders all tags as <li> children of <ul data-website-blog-post-tags>', () => {
|
|
121
|
+
const { container } = render(
|
|
122
|
+
wrap(
|
|
123
|
+
<BlogPost rawFrontmatter={VALID} source="en/why-context.mdx">
|
|
124
|
+
<p>Body</p>
|
|
125
|
+
</BlogPost>,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
const tagList = container.querySelector('ul[data-website-blog-post-tags]');
|
|
129
|
+
expect(tagList).not.toBeNull();
|
|
130
|
+
const tagItems = tagList!.querySelectorAll('li');
|
|
131
|
+
expect(tagItems.length).toBe(VALID.tags.length);
|
|
132
|
+
expect(Array.from(tagItems).map((li) => li.textContent)).toEqual(VALID.tags);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('omits the cover image element when coverImageUrl is absent', () => {
|
|
136
|
+
const { coverImageUrl: _u, ...withoutCover } = VALID;
|
|
137
|
+
void _u;
|
|
138
|
+
const { container } = render(
|
|
139
|
+
wrap(
|
|
140
|
+
<BlogPost rawFrontmatter={withoutCover} source="en/no-cover.mdx">
|
|
141
|
+
<p>Body</p>
|
|
142
|
+
</BlogPost>,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
expect(container.querySelector('img[data-website-blog-post-cover]')).toBeNull();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('omits the tag list when tags array is empty', () => {
|
|
149
|
+
const { container } = render(
|
|
150
|
+
wrap(
|
|
151
|
+
<BlogPost rawFrontmatter={{ ...VALID, tags: [] }} source="en/no-tags.mdx">
|
|
152
|
+
<p>Body</p>
|
|
153
|
+
</BlogPost>,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
expect(container.querySelector('ul[data-website-blog-post-tags]')).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('throws a [blog-post]-prefixed error when frontmatter is invalid', () => {
|
|
160
|
+
const broken = { ...VALID, slug: 'NotKebab' };
|
|
161
|
+
expect(() =>
|
|
162
|
+
render(
|
|
163
|
+
wrap(
|
|
164
|
+
<BlogPost rawFrontmatter={broken} source="en/broken.mdx">
|
|
165
|
+
<p>Body</p>
|
|
166
|
+
</BlogPost>,
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
).toThrowError(/\[blog-post\] Frontmatter failed BlogPostFrontmatterSchema validation \(source="en\/broken\.mdx"\)/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('renders MDX-style children inside the WebsiteMdxProvider boundary', () => {
|
|
173
|
+
/**
|
|
174
|
+
* Assert the children land in the document. This proves the
|
|
175
|
+
* provider does not eat them; the provider's own forwarding
|
|
176
|
+
* behaviour is exercised in `website-mdx-provider.test.tsx`.
|
|
177
|
+
*/
|
|
178
|
+
const { getByTestId } = render(
|
|
179
|
+
wrap(
|
|
180
|
+
<BlogPost rawFrontmatter={VALID} source="en/why-context.mdx">
|
|
181
|
+
<p data-testid="mdx-body">Body content</p>
|
|
182
|
+
</BlogPost>,
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
expect(getByTestId('mdx-body')).toHaveTextContent('Body content');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('honors per-page header/footer overrides', () => {
|
|
189
|
+
const { getByTestId, queryByTestId } = render(
|
|
190
|
+
wrap(
|
|
191
|
+
<BlogPost
|
|
192
|
+
rawFrontmatter={VALID}
|
|
193
|
+
source="en/why-context.mdx"
|
|
194
|
+
header={<div data-testid="custom-header">CH</div>}
|
|
195
|
+
footer={<div data-testid="custom-footer">CF</div>}
|
|
196
|
+
>
|
|
197
|
+
<p>Body</p>
|
|
198
|
+
</BlogPost>,
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
expect(getByTestId('custom-header')).toBeInTheDocument();
|
|
202
|
+
expect(getByTestId('custom-footer')).toBeInTheDocument();
|
|
203
|
+
expect(queryByTestId('runtime-header')).toBeNull();
|
|
204
|
+
expect(queryByTestId('runtime-footer')).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('falls back to the runtime navigation chrome when no override is supplied', () => {
|
|
208
|
+
const { getByTestId } = render(
|
|
209
|
+
wrap(
|
|
210
|
+
<BlogPost rawFrontmatter={VALID} source="en/why-context.mdx">
|
|
211
|
+
<p>Body</p>
|
|
212
|
+
</BlogPost>,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
expect(getByTestId('runtime-header')).toBeInTheDocument();
|
|
216
|
+
expect(getByTestId('runtime-footer')).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildBlogPostCollectionConfig,
|
|
5
|
+
parseBlogPostFrontmatter,
|
|
6
|
+
type AstroZodFactoryShape,
|
|
7
|
+
} from '../blog-post-collection.config';
|
|
8
|
+
import { parseBlogPostFrontmatterFromMdxSource } from '../blog-post-frontmatter-source';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Coverage for the Astro content-collection bridge (Path Z).
|
|
12
|
+
*
|
|
13
|
+
* Two surfaces:
|
|
14
|
+
* - `buildBlogPostCollectionConfig({ astroZ })` — assembles the
|
|
15
|
+
* passthrough schema slot the consumer spreads into Astro's
|
|
16
|
+
* `defineCollection({ ... })`. We feed it a tiny stub matching
|
|
17
|
+
* `AstroZodFactoryShape` (the engine intentionally cannot import
|
|
18
|
+
* `astro:content` directly; the consumer-supplied factory IS the
|
|
19
|
+
* bridge) and assert the right calls land.
|
|
20
|
+
* - `parseBlogPostFrontmatter(raw, ctx)` — the strict Zod-4 validator
|
|
21
|
+
* called at the top of `<BlogPost>` rendering. We exercise both
|
|
22
|
+
* happy-path branding + failure-case error formatting (the
|
|
23
|
+
* `[blog-post]` prefix + per-issue path/message lines + optional
|
|
24
|
+
* `(source="…")` suffix that names the offending file).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const VALID_FRONTMATTER = {
|
|
28
|
+
title: 'Why Context Beats Content',
|
|
29
|
+
summary:
|
|
30
|
+
'Content tools optimize the wrong loop. Context tools compress the loop your team actually runs every week.',
|
|
31
|
+
slug: 'why-context-beats-content',
|
|
32
|
+
locale: 'en' as const,
|
|
33
|
+
publishedAt: '2026-04-17T09:00:00.000Z',
|
|
34
|
+
authorRef: 'paul-cayon',
|
|
35
|
+
tags: ['context', 'product-strategy'],
|
|
36
|
+
coverImageUrl: 'https://cdn.wildo.ai/blog/why-context-cover.png',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe('buildBlogPostCollectionConfig', () => {
|
|
40
|
+
it('calls astroZ.object({}) followed by .passthrough() and surfaces the result', () => {
|
|
41
|
+
const passthroughSchema = { __mock__: 'passthrough-schema' };
|
|
42
|
+
const passthroughSpy = vi.fn(() => passthroughSchema);
|
|
43
|
+
const objectSpy = vi.fn(() => ({ passthrough: passthroughSpy }));
|
|
44
|
+
const astroZ: AstroZodFactoryShape = {
|
|
45
|
+
object: objectSpy as unknown as AstroZodFactoryShape['object'],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const config = buildBlogPostCollectionConfig({ astroZ });
|
|
49
|
+
|
|
50
|
+
expect(objectSpy).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(objectSpy).toHaveBeenCalledWith({});
|
|
52
|
+
expect(passthroughSpy).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(config.schema).toBe(passthroughSchema);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('parseBlogPostFrontmatter', () => {
|
|
58
|
+
it('returns a branded BlogPostFrontmatter on a valid input', () => {
|
|
59
|
+
const result = parseBlogPostFrontmatter(VALID_FRONTMATTER);
|
|
60
|
+
expect(result.title).toBe(VALID_FRONTMATTER.title);
|
|
61
|
+
expect(result.slug as unknown as string).toBe(VALID_FRONTMATTER.slug);
|
|
62
|
+
expect(result.tags.map((t) => t as unknown as string)).toEqual(
|
|
63
|
+
VALID_FRONTMATTER.tags,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('accepts a frontmatter without the optional coverImageUrl', () => {
|
|
68
|
+
const { coverImageUrl: _unused, ...rest } = VALID_FRONTMATTER;
|
|
69
|
+
void _unused;
|
|
70
|
+
const result = parseBlogPostFrontmatter(rest);
|
|
71
|
+
expect(result.coverImageUrl).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('throws with a [blog-post] prefix and includes the source suffix when ctx.source is set', () => {
|
|
75
|
+
const broken = { ...VALID_FRONTMATTER, slug: 'NotKebab' };
|
|
76
|
+
expect(() => parseBlogPostFrontmatter(broken, { source: 'en/why-context.mdx' }))
|
|
77
|
+
.toThrowError(/\[blog-post\] Frontmatter failed BlogPostFrontmatterSchema validation \(source="en\/why-context\.mdx"\):/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('omits the source suffix when ctx is empty', () => {
|
|
81
|
+
const broken = { ...VALID_FRONTMATTER, slug: '' };
|
|
82
|
+
expect(() => parseBlogPostFrontmatter(broken, {}))
|
|
83
|
+
.toThrowError(/\[blog-post\] Frontmatter failed BlogPostFrontmatterSchema validation:\n - slug:/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('includes the failing field path in the error message', () => {
|
|
87
|
+
const broken = { ...VALID_FRONTMATTER, publishedAt: 'not-a-datetime' };
|
|
88
|
+
expect(() => parseBlogPostFrontmatter(broken)).toThrowError(/publishedAt:/);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reports multiple issues on multiple bad fields', () => {
|
|
92
|
+
const broken = { ...VALID_FRONTMATTER, title: '', summary: '' };
|
|
93
|
+
expect(() => parseBlogPostFrontmatter(broken)).toThrowError(/title:[\s\S]*summary:/);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('parseBlogPostFrontmatterFromMdxSource', () => {
|
|
98
|
+
it('parses YAML frontmatter from raw MDX source through the canonical schema', () => {
|
|
99
|
+
const result = parseBlogPostFrontmatterFromMdxSource(
|
|
100
|
+
`---
|
|
101
|
+
title: Why Context Beats Content
|
|
102
|
+
summary: Content tools optimize the wrong loop. Context tools compress the loop your team actually runs every week.
|
|
103
|
+
slug: why-context-beats-content
|
|
104
|
+
locale: en
|
|
105
|
+
publishedAt: "2026-04-17T09:00:00.000Z"
|
|
106
|
+
authorRef: paul-cayon
|
|
107
|
+
tags:
|
|
108
|
+
- context
|
|
109
|
+
- product-strategy
|
|
110
|
+
coverImageUrl: https://cdn.wildo.ai/blog/why-context-cover.png
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
# Body
|
|
114
|
+
`,
|
|
115
|
+
{ source: 'en/why-context-beats-content.mdx' },
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(result.slug as unknown as string).toBe(VALID_FRONTMATTER.slug);
|
|
119
|
+
expect(result.tags.map((t) => t as unknown as string)).toEqual(VALID_FRONTMATTER.tags);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('fails with source context when raw MDX frontmatter is missing', () => {
|
|
123
|
+
expect(() =>
|
|
124
|
+
parseBlogPostFrontmatterFromMdxSource('# Body only', { source: 'en/broken.mdx' }),
|
|
125
|
+
).toThrowError(/\[blog-post\] MDX file is missing YAML frontmatter \(source="en\/broken\.mdx"\)\./);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
|
|
6
|
+
import { useMDXComponents } from '@mdx-js/react';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
WebsiteMdxProvider,
|
|
10
|
+
type WebsiteMdxComponentsMap,
|
|
11
|
+
} from '../website-mdx-provider';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The provider is intentionally a thin wrapper around `@mdx-js/react`'s
|
|
15
|
+
* `MDXProvider`. We don't re-test the upstream library — we test that
|
|
16
|
+
* the framework boundary correctly forwards the `components` map and
|
|
17
|
+
* exposes its identity to descendants via `useMDXComponents()` (the
|
|
18
|
+
* upstream hook the MDX-compiled JSX call sites use to resolve tag
|
|
19
|
+
* names → React components).
|
|
20
|
+
*
|
|
21
|
+
* If the upstream API ever shifts (e.g. a major bumps `MDXProvider`'s
|
|
22
|
+
* prop name or splits the package), these tests fail at this seam
|
|
23
|
+
* instead of propagating into every consumer's blog body.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
function ProbeComponents(): React.ReactElement {
|
|
27
|
+
const components = useMDXComponents();
|
|
28
|
+
return (
|
|
29
|
+
<ul>
|
|
30
|
+
{Object.keys(components)
|
|
31
|
+
.sort()
|
|
32
|
+
.map((key) => (
|
|
33
|
+
<li key={key} data-testid={`mdx-key-${key}`}>
|
|
34
|
+
{key}
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('WebsiteMdxProvider', () => {
|
|
42
|
+
it('makes provider components available to descendants via useMDXComponents()', () => {
|
|
43
|
+
const components: WebsiteMdxComponentsMap = {
|
|
44
|
+
CTASection: () => <span>cta</span>,
|
|
45
|
+
Newsletter: () => <span>news</span>,
|
|
46
|
+
};
|
|
47
|
+
const { getByTestId } = render(
|
|
48
|
+
<WebsiteMdxProvider components={components}>
|
|
49
|
+
<ProbeComponents />
|
|
50
|
+
</WebsiteMdxProvider>,
|
|
51
|
+
);
|
|
52
|
+
expect(getByTestId('mdx-key-CTASection')).toBeInTheDocument();
|
|
53
|
+
expect(getByTestId('mdx-key-Newsletter')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('renders its children unmodified', () => {
|
|
57
|
+
const { getByTestId } = render(
|
|
58
|
+
<WebsiteMdxProvider components={{}}>
|
|
59
|
+
<span data-testid="child">hello</span>
|
|
60
|
+
</WebsiteMdxProvider>,
|
|
61
|
+
);
|
|
62
|
+
expect(getByTestId('child')).toHaveTextContent('hello');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('inherits parent provider components when nested (default disableParentContext = false)', () => {
|
|
66
|
+
const parent: WebsiteMdxComponentsMap = {
|
|
67
|
+
ParentOnly: () => <span>p</span>,
|
|
68
|
+
};
|
|
69
|
+
const child: WebsiteMdxComponentsMap = {
|
|
70
|
+
ChildOnly: () => <span>c</span>,
|
|
71
|
+
};
|
|
72
|
+
const { getByTestId } = render(
|
|
73
|
+
<WebsiteMdxProvider components={parent}>
|
|
74
|
+
<WebsiteMdxProvider components={child}>
|
|
75
|
+
<ProbeComponents />
|
|
76
|
+
</WebsiteMdxProvider>
|
|
77
|
+
</WebsiteMdxProvider>,
|
|
78
|
+
);
|
|
79
|
+
expect(getByTestId('mdx-key-ParentOnly')).toBeInTheDocument();
|
|
80
|
+
expect(getByTestId('mdx-key-ChildOnly')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { formatZodIssues } from '@wildo-ai/saas-models/public-runtime';
|
|
2
|
+
import {
|
|
3
|
+
BlogPostFrontmatterSchema,
|
|
4
|
+
type BlogPostFrontmatter,
|
|
5
|
+
} from '../schemas/blog/blog-post-frontmatter.shared.schemas';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @wildo_source:part:start saas.website.blog.collection-config facet:layer:mdx facet:family:website
|
|
9
|
+
*
|
|
10
|
+
* Astro content-collection bridge for blog posts (Phase 7 D-3 = Path Z).
|
|
11
|
+
*
|
|
12
|
+
* # The Zod-version bridge problem
|
|
13
|
+
*
|
|
14
|
+
* Astro's `astro:content` ships its own bundled Zod (currently 3.x).
|
|
15
|
+
* Our `BlogPostFrontmatterSchema` is Zod 4 and uses Zod-4-only APIs
|
|
16
|
+
* (`z.iso.datetime`, `z.url(...)`, `.brand<>()`). The two cannot share
|
|
17
|
+
* a schema instance directly across the version boundary, and shimming
|
|
18
|
+
* a Zod 4 schema down to a Zod 3 schema would create either:
|
|
19
|
+
*
|
|
20
|
+
* - a hand-mirrored copy (Path X) — maintained in `src/content/config.ts`
|
|
21
|
+
* using Astro's `z`, with silent-drift risk every time a field is
|
|
22
|
+
* added/removed/refined; or
|
|
23
|
+
* - a runtime bridge package (Path C) — a non-trivial maintenance
|
|
24
|
+
* surface for a marginal gain.
|
|
25
|
+
*
|
|
26
|
+
* Path Z (this file) sidesteps the bridge entirely: the consumer's
|
|
27
|
+
* `src/content/config.ts` declares a `passthrough` collection schema
|
|
28
|
+
* that lets Astro accept any frontmatter shape; the engine then
|
|
29
|
+
* validates with the canonical Zod 4 schema at render time inside
|
|
30
|
+
* `<BlogPost>`. Astro's static site generator runs the page render
|
|
31
|
+
* during the build, so a malformed frontmatter still surfaces as a
|
|
32
|
+
* BUILD-TIME error (the `.parse()` call throws synchronously and Astro
|
|
33
|
+
* reports it with the page URL) — the only practical difference vs.
|
|
34
|
+
* Path X is the error-message stack location.
|
|
35
|
+
*
|
|
36
|
+
* # Why the engine ships an opaque shape (rather than the consumer
|
|
37
|
+
* importing the Astro `z` directly)
|
|
38
|
+
*
|
|
39
|
+
* Astro's content-collection API has churned across Astro 4 → 5
|
|
40
|
+
* (loaders moved from string-keyed `type` to a callable `loader`
|
|
41
|
+
* factory; the `glob()` loader's signature changed). Every consumer
|
|
42
|
+
* touching `defineCollection` directly inherits that churn; centralizing
|
|
43
|
+
* the wiring in the engine means the framework absorbs the upgrade
|
|
44
|
+
* cost in ONE place when Astro bumps a major.
|
|
45
|
+
*
|
|
46
|
+
* The shape returned here is `{ schema: <passthrough record> }`. The
|
|
47
|
+
* consumer spreads it into their `defineCollection` call:
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* // examples/<app>/website/src/content/config.ts
|
|
51
|
+
* import { defineCollection, z } from 'astro:content';
|
|
52
|
+
* import { buildBlogPostCollectionConfig } from '@wildo-ai/saas-website/mdx';
|
|
53
|
+
*
|
|
54
|
+
* export const collections = {
|
|
55
|
+
* blog: defineCollection({
|
|
56
|
+
* type: 'content',
|
|
57
|
+
* ...buildBlogPostCollectionConfig({ astroZ: z }),
|
|
58
|
+
* }),
|
|
59
|
+
* };
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* The consumer passes Astro's own `z` in (because the engine cannot
|
|
63
|
+
* import `astro:content` — that virtual module only exists inside
|
|
64
|
+
* the Astro build context, never at the engine package's compile
|
|
65
|
+
* time, and importing it would also pull `astro` into the engine's
|
|
66
|
+
* walked closure which the bundle-isolation test forbids). The engine
|
|
67
|
+
* builds a passthrough `z.object({}).passthrough()` from the supplied
|
|
68
|
+
* factory and returns the schema slot.
|
|
69
|
+
*
|
|
70
|
+
* # `parseBlogPostFrontmatter`
|
|
71
|
+
*
|
|
72
|
+
* Companion render-time validator. Called at the top of `<BlogPost>`
|
|
73
|
+
* with the raw `entry.data` Astro hands the page; returns the
|
|
74
|
+
* branded, validated `BlogPostFrontmatter` (Zod 4) or throws with
|
|
75
|
+
* the standard Zod-4 issue formatting + a `[blog-post]` prefix that
|
|
76
|
+
* tells the SSG operator which file to fix.
|
|
77
|
+
*
|
|
78
|
+
* # Boundary
|
|
79
|
+
*
|
|
80
|
+
* This file is React-runtime-safe: it only imports the (also
|
|
81
|
+
* React-safe) `BlogPostFrontmatterSchema` from `schemas/blog/`. It
|
|
82
|
+
* ships into the React-island bundle via the `mdx.ts` barrel, which
|
|
83
|
+
* the bundle-isolation test walks as a dedicated entry. Helpers that
|
|
84
|
+
* parse raw MDX source live in `blog-post-frontmatter-source.ts` so the
|
|
85
|
+
* `yaml` package stays on the build-time/Node side of the boundary.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Minimal contract for the Astro-side Zod factory we accept. Typed
|
|
90
|
+
* structurally (rather than imported from `astro:content`) so the
|
|
91
|
+
* engine package can compile/test without an Astro install. Astro's
|
|
92
|
+
* actual `z.object` returns a much richer Zod-3 schema; we narrow to
|
|
93
|
+
* just the `.passthrough()` call we make below.
|
|
94
|
+
*/
|
|
95
|
+
export interface AstroZodFactoryShape {
|
|
96
|
+
object: (shape: Record<string, never>) => {
|
|
97
|
+
passthrough: () => unknown;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface BuildBlogPostCollectionConfigOptions {
|
|
102
|
+
/**
|
|
103
|
+
* Astro's bundled `z` object — pass `import { z } from 'astro:content'`
|
|
104
|
+
* from the consumer-side `src/content/config.ts`. The engine cannot
|
|
105
|
+
* import `astro:content` itself (virtual module, only present inside
|
|
106
|
+
* the Astro build pipeline; would also bloat the bundle-isolation
|
|
107
|
+
* walked closure with the `astro` runtime).
|
|
108
|
+
*/
|
|
109
|
+
astroZ: AstroZodFactoryShape;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface BlogPostCollectionConfigShape {
|
|
113
|
+
/**
|
|
114
|
+
* The schema slot consumed by Astro's `defineCollection({...})`.
|
|
115
|
+
* A `passthrough` record (no field constraints at this layer) so
|
|
116
|
+
* Astro accepts any frontmatter; the strict Zod-4 validation runs
|
|
117
|
+
* at render time inside `<BlogPost>` via
|
|
118
|
+
* `parseBlogPostFrontmatter(...)`.
|
|
119
|
+
*/
|
|
120
|
+
schema: unknown;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function buildBlogPostCollectionConfig(
|
|
124
|
+
opts: BuildBlogPostCollectionConfigOptions,
|
|
125
|
+
): BlogPostCollectionConfigShape {
|
|
126
|
+
const { astroZ } = opts;
|
|
127
|
+
return {
|
|
128
|
+
schema: astroZ.object({}).passthrough(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Strict Zod-4 validator for blog-post frontmatter. Called at the top
|
|
134
|
+
* of `<BlogPost>` rendering against `entry.data` (whatever Astro's
|
|
135
|
+
* passthrough collection schema accepted). Throws on validation
|
|
136
|
+
* failure with a `[blog-post]`-prefixed message that points to the
|
|
137
|
+
* offending file via the surrounding render call site.
|
|
138
|
+
*
|
|
139
|
+
* @param raw — the unvalidated frontmatter object Astro supplies
|
|
140
|
+
* (`entry.data` from `getEntry('blog', slug)` or `getCollection('blog')`).
|
|
141
|
+
* @param ctx — diagnostic context appended to the error message so
|
|
142
|
+
* the consumer can find the failing source file when SSG halts.
|
|
143
|
+
* Optional but recommended.
|
|
144
|
+
*/
|
|
145
|
+
export interface ParseBlogPostFrontmatterContext {
|
|
146
|
+
/**
|
|
147
|
+
* The MDX file's path or slug. Echoed into the error message when
|
|
148
|
+
* validation fails so the operator can grep for it. Astro typically
|
|
149
|
+
* gives consumers `entry.id` (the slug + extension) — passing it
|
|
150
|
+
* through here is enough for an actionable error.
|
|
151
|
+
*/
|
|
152
|
+
readonly source?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function parseBlogPostFrontmatter(
|
|
156
|
+
raw: unknown,
|
|
157
|
+
ctx: ParseBlogPostFrontmatterContext = {},
|
|
158
|
+
): BlogPostFrontmatter {
|
|
159
|
+
const result = BlogPostFrontmatterSchema.safeParse(raw);
|
|
160
|
+
if (result.success) {
|
|
161
|
+
return result.data;
|
|
162
|
+
}
|
|
163
|
+
const sourceSuffix = ctx.source !== undefined ? ` (source="${ctx.source}")` : '';
|
|
164
|
+
/**
|
|
165
|
+
* Zod 4's `result.error.message` is the JSON-stringified `issues[]` array —
|
|
166
|
+
* long-form, it floods the Astro build log, and the path is what an operator
|
|
167
|
+
* needs to find the broken frontmatter field. `formatZodIssues` is the
|
|
168
|
+
* framework-canonical rendering (one `path: message` per entry); we only own
|
|
169
|
+
* the bullet prefix. Unbounded on purpose: every bad field in a post should
|
|
170
|
+
* be fixable from one build run.
|
|
171
|
+
*/
|
|
172
|
+
const issueLines = formatZodIssues(result.error.issues).map((entry) => ` - ${entry}`);
|
|
173
|
+
throw new Error(
|
|
174
|
+
`[blog-post] Frontmatter failed BlogPostFrontmatterSchema validation${sourceSuffix}:\n${issueLines.join('\n')}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
/** @wildo_source:part:end saas.website.blog.collection-config */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parse as parseYaml } from 'yaml';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
parseBlogPostFrontmatter,
|
|
5
|
+
type ParseBlogPostFrontmatterContext,
|
|
6
|
+
} from './blog-post-collection.config';
|
|
7
|
+
import type { BlogPostFrontmatter } from '../schemas/blog/blog-post-frontmatter.shared.schemas';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build-time helper for code that reads raw MDX files from disk and
|
|
11
|
+
* needs the canonical Zod-4 blog frontmatter validation. This module is
|
|
12
|
+
* intentionally kept out of `@wildo-ai/saas-website/mdx`, because its
|
|
13
|
+
* `yaml` dependency belongs to the Node/build-time graph, not the
|
|
14
|
+
* React-island bundle walked by the D12 isolation test.
|
|
15
|
+
*/
|
|
16
|
+
export type ParseBlogPostFrontmatterFromMdxSourceContext = ParseBlogPostFrontmatterContext;
|
|
17
|
+
|
|
18
|
+
export function parseBlogPostFrontmatterFromMdxSource(
|
|
19
|
+
source: string,
|
|
20
|
+
ctx: ParseBlogPostFrontmatterFromMdxSourceContext = {},
|
|
21
|
+
): BlogPostFrontmatter {
|
|
22
|
+
const normalizedSource = source.replace(/\r\n/g, '\n');
|
|
23
|
+
const match = normalizedSource.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
24
|
+
if (!match) {
|
|
25
|
+
const sourceSuffix = ctx.source !== undefined ? ` (source="${ctx.source}")` : '';
|
|
26
|
+
throw new Error(`[blog-post] MDX file is missing YAML frontmatter${sourceSuffix}.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rawFrontmatter = parseYaml(match[1]);
|
|
30
|
+
return parseBlogPostFrontmatter(rawFrontmatter, ctx);
|
|
31
|
+
}
|
package/src/mdx/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-barrel for the marketing-blog MDX adapter (Phase 7). Re-exports
|
|
3
|
+
* the three engine surfaces consumers wire up:
|
|
4
|
+
*
|
|
5
|
+
* - `<BlogPost>` — the per-post page renderer that strict-validates
|
|
6
|
+
* frontmatter + composes `WebsitePageLayout` + `WebsiteMdxProvider`.
|
|
7
|
+
* - `<WebsiteMdxProvider>` — the `@mdx-js/react` boundary the
|
|
8
|
+
* framework owns so the upstream package is swap-safe.
|
|
9
|
+
* - `buildBlogPostCollectionConfig(...)` + `parseBlogPostFrontmatter(...)`
|
|
10
|
+
* — the Astro content-collection bridge (Path Z passthrough at the
|
|
11
|
+
* collection layer, strict Zod-4 validation at render time). Raw
|
|
12
|
+
* MDX-source parsing is exported separately from `./mdx-source` so
|
|
13
|
+
* `yaml` stays out of the React-island graph.
|
|
14
|
+
*
|
|
15
|
+
* Re-exported through the top-level `mdx.ts` barrel so the public
|
|
16
|
+
* import surface is `'@wildo-ai/saas-website/mdx'` (matching the
|
|
17
|
+
* `astro.ts` / `astro-island.ts` naming pattern).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export {
|
|
21
|
+
BlogPost,
|
|
22
|
+
BLOG_POST_PAGE_REF,
|
|
23
|
+
type BlogPostProps,
|
|
24
|
+
type BlogPostMdxComponents,
|
|
25
|
+
type BlogPostMdxComponent,
|
|
26
|
+
} from './BlogPost';
|
|
27
|
+
export {
|
|
28
|
+
buildBlogPostCollectionConfig,
|
|
29
|
+
parseBlogPostFrontmatter,
|
|
30
|
+
type AstroZodFactoryShape,
|
|
31
|
+
type BuildBlogPostCollectionConfigOptions,
|
|
32
|
+
type BlogPostCollectionConfigShape,
|
|
33
|
+
type ParseBlogPostFrontmatterContext,
|
|
34
|
+
} from './blog-post-collection.config';
|
|
35
|
+
export {
|
|
36
|
+
WebsiteMdxProvider,
|
|
37
|
+
type WebsiteMdxComponentsMap,
|
|
38
|
+
type WebsiteMdxProviderProps,
|
|
39
|
+
} from './website-mdx-provider';
|