@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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InboundContactForm — the marketing site's real lead-capture form (React island).
|
|
3
|
+
*
|
|
4
|
+
* Self-contained + bundle-safe: uses only `react`, the website-local input schema, and
|
|
5
|
+
* a `WebsiteAnonymousSessionClient` instance (constructed by the section/bridge wiring
|
|
6
|
+
* from the site's API-origin config). On submit it validates client-side, then POSTs
|
|
7
|
+
* `/inbound-contacts` through the anonymous-session client — the FIRST submission mints
|
|
8
|
+
* the anonymous session (init header) and the backend stamps the lead's `anonymousUserId`
|
|
9
|
+
* so it can later transpose to the visitor's account on signup/login.
|
|
10
|
+
*
|
|
11
|
+
* Labels are injected (optional props with English fallbacks) rather than read from the
|
|
12
|
+
* website runtime context, so the component stays framework-light and unit-testable; the
|
|
13
|
+
* section wrapper passes i18n-resolved strings.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
17
|
+
import { InboundContactFormSchema, buildInboundContactCreateBody, type InboundContactFormValues } from "./inbound-contact-form.schema";
|
|
18
|
+
import type { WebsiteAnonymousSessionClient } from "./website-anonymous-session-client";
|
|
19
|
+
|
|
20
|
+
export interface InboundContactFormLabels {
|
|
21
|
+
emailLabel: string;
|
|
22
|
+
firstNameLabel: string;
|
|
23
|
+
lastNameLabel: string;
|
|
24
|
+
phoneLabel: string;
|
|
25
|
+
/** OPTIONAL marketing opt-in checkbox. */
|
|
26
|
+
marketingConsentLabel: string;
|
|
27
|
+
submitLabel: string;
|
|
28
|
+
submittingLabel: string;
|
|
29
|
+
successMessage: string;
|
|
30
|
+
errorMessage: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_LABELS: InboundContactFormLabels = {
|
|
34
|
+
emailLabel: "Email",
|
|
35
|
+
firstNameLabel: "First name",
|
|
36
|
+
lastNameLabel: "Last name",
|
|
37
|
+
phoneLabel: "Phone",
|
|
38
|
+
marketingConsentLabel: "Keep me updated about news and offers",
|
|
39
|
+
submitLabel: "Get in touch",
|
|
40
|
+
submittingLabel: "Sending…",
|
|
41
|
+
successMessage: "Thanks — we'll be in touch shortly.",
|
|
42
|
+
errorMessage: "Something went wrong. Please try again.",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type SubmitState = { status: "idle" } | { status: "submitting" } | { status: "success" } | { status: "error"; message: string };
|
|
46
|
+
|
|
47
|
+
export interface InboundContactFormProps {
|
|
48
|
+
/** The anonymous-session client (constructed by the section wiring from site config). */
|
|
49
|
+
client: WebsiteAnonymousSessionClient;
|
|
50
|
+
/** Optional i18n-resolved labels; English fallbacks are used per-field when omitted. */
|
|
51
|
+
labels?: Partial<InboundContactFormLabels>;
|
|
52
|
+
/** Optional callback fired after a successful submission (e.g. analytics). */
|
|
53
|
+
onSubmitted?: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const EMPTY_VALUES: InboundContactFormValues = {
|
|
57
|
+
email: "",
|
|
58
|
+
firstName: "",
|
|
59
|
+
lastName: "",
|
|
60
|
+
phone: "",
|
|
61
|
+
marketingConsent: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function InboundContactForm({ client, labels, onSubmitted }: InboundContactFormProps): React.ReactElement {
|
|
65
|
+
const resolvedLabels = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
|
|
66
|
+
const [values, setValues] = useState<InboundContactFormValues>(EMPTY_VALUES);
|
|
67
|
+
const [fieldErrors, setFieldErrors] = useState<Partial<Record<keyof InboundContactFormValues, string>>>({});
|
|
68
|
+
const [submit, setSubmit] = useState<SubmitState>({ status: "idle" });
|
|
69
|
+
|
|
70
|
+
const setField = useCallback(<K extends keyof InboundContactFormValues>(key: K, value: InboundContactFormValues[K]) => {
|
|
71
|
+
setValues((prev) => ({ ...prev, [key]: value }));
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const handleSubmit = useCallback(
|
|
75
|
+
async (event: React.FormEvent<HTMLFormElement>) => {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
setFieldErrors({});
|
|
78
|
+
|
|
79
|
+
const parsed = InboundContactFormSchema.safeParse(values);
|
|
80
|
+
if (!parsed.success) {
|
|
81
|
+
const nextErrors: Partial<Record<keyof InboundContactFormValues, string>> = {};
|
|
82
|
+
for (const issue of parsed.error.issues) {
|
|
83
|
+
const key = issue.path[0] as keyof InboundContactFormValues | undefined;
|
|
84
|
+
if (key && !nextErrors[key]) nextErrors[key] = issue.message;
|
|
85
|
+
}
|
|
86
|
+
setFieldErrors(nextErrors);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setSubmit({ status: "submitting" });
|
|
91
|
+
try {
|
|
92
|
+
await client.postAnonymous("/inbound-contacts", buildInboundContactCreateBody(parsed.data));
|
|
93
|
+
setSubmit({ status: "success" });
|
|
94
|
+
setValues(EMPTY_VALUES);
|
|
95
|
+
onSubmitted?.();
|
|
96
|
+
} catch {
|
|
97
|
+
// The backend-only `emailUserMatch` triage signal is never returned here, so a
|
|
98
|
+
// failure exposes no account-existence information — just a generic retry.
|
|
99
|
+
setSubmit({ status: "error", message: resolvedLabels.errorMessage });
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[client, values, onSubmitted, resolvedLabels.errorMessage],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (submit.status === "success") {
|
|
106
|
+
return (
|
|
107
|
+
<div role="status" aria-live="polite">
|
|
108
|
+
{resolvedLabels.successMessage}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isSubmitting = submit.status === "submitting";
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<form onSubmit={handleSubmit} noValidate>
|
|
117
|
+
<label>
|
|
118
|
+
{resolvedLabels.emailLabel}
|
|
119
|
+
<input
|
|
120
|
+
type="email"
|
|
121
|
+
name="email"
|
|
122
|
+
required
|
|
123
|
+
value={values.email}
|
|
124
|
+
onChange={(e) => setField("email", e.target.value)}
|
|
125
|
+
aria-invalid={fieldErrors.email ? true : undefined}
|
|
126
|
+
/>
|
|
127
|
+
{fieldErrors.email ? <span role="alert">{fieldErrors.email}</span> : null}
|
|
128
|
+
</label>
|
|
129
|
+
|
|
130
|
+
<label>
|
|
131
|
+
{resolvedLabels.firstNameLabel}
|
|
132
|
+
<input type="text" name="firstName" value={values.firstName ?? ""} onChange={(e) => setField("firstName", e.target.value)} />
|
|
133
|
+
</label>
|
|
134
|
+
|
|
135
|
+
<label>
|
|
136
|
+
{resolvedLabels.lastNameLabel}
|
|
137
|
+
<input type="text" name="lastName" value={values.lastName ?? ""} onChange={(e) => setField("lastName", e.target.value)} />
|
|
138
|
+
</label>
|
|
139
|
+
|
|
140
|
+
<label>
|
|
141
|
+
{resolvedLabels.phoneLabel}
|
|
142
|
+
<input type="tel" name="phone" value={values.phone ?? ""} onChange={(e) => setField("phone", e.target.value)} />
|
|
143
|
+
</label>
|
|
144
|
+
|
|
145
|
+
<label>
|
|
146
|
+
<input type="checkbox" name="marketingConsent" checked={values.marketingConsent} onChange={(e) => setField("marketingConsent", e.target.checked)} />
|
|
147
|
+
{resolvedLabels.marketingConsentLabel}
|
|
148
|
+
</label>
|
|
149
|
+
|
|
150
|
+
{submit.status === "error" ? <div role="alert">{submit.message}</div> : null}
|
|
151
|
+
|
|
152
|
+
<button type="submit" disabled={isSubmitting}>
|
|
153
|
+
{isSubmitting ? resolvedLabels.submittingLabel : resolvedLabels.submitLabel}
|
|
154
|
+
</button>
|
|
155
|
+
</form>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { InboundContactFormSchema, buildInboundContactCreateBody, type InboundContactFormValues } from "../inbound-contact-form.schema";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The inbound-contact form is the anonymous lead-capture surface. Its
|
|
7
|
+
* The form may collect contact fields and an optional marketing preference. It must
|
|
8
|
+
* not manufacture notice, contract or consent evidence without an exact governed
|
|
9
|
+
* document release and the runtime receipt boundary.
|
|
10
|
+
*/
|
|
11
|
+
describe("InboundContactFormSchema — contact and marketing choices", () => {
|
|
12
|
+
const minimum = { email: "Lead@Example.com" };
|
|
13
|
+
|
|
14
|
+
it("accepts email as the minimum and defaults the optional marketing choice", () => {
|
|
15
|
+
expect(InboundContactFormSchema.parse(minimum)).toEqual({ email: "Lead@Example.com", marketingConsent: false });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("rejects a malformed email", () => {
|
|
19
|
+
expect(InboundContactFormSchema.safeParse({ ...minimum, email: "not-an-email" }).success).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("buildInboundContactCreateBody — no synthetic legal receipt", () => {
|
|
24
|
+
const values: InboundContactFormValues = {
|
|
25
|
+
email: "Lead@Example.com",
|
|
26
|
+
marketingConsent: false,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it("emits no embedded legal-document approval", () => {
|
|
30
|
+
const serialized = JSON.stringify(buildInboundContactCreateBody(values));
|
|
31
|
+
expect(serialized).not.toContain("Approval");
|
|
32
|
+
expect(serialized).not.toContain("acceptedAt");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("lower-cases + trims the email", () => {
|
|
36
|
+
expect(buildInboundContactCreateBody(values).email).toBe("lead@example.com");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("maps the OPTIONAL marketing opt-in to the dataProcessingConsent tier", () => {
|
|
40
|
+
const tier = (v: InboundContactFormValues): string => (buildInboundContactCreateBody(v).privacy as { dataProcessingConsent: string }).dataProcessingConsent;
|
|
41
|
+
expect(tier({ ...values, marketingConsent: true })).toBe("marketing");
|
|
42
|
+
expect(tier({ ...values, marketingConsent: false })).toBe("basic");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound-contact form input schema (website-local, bundle-safe).
|
|
3
|
+
*
|
|
4
|
+
* Deliberately a SEPARATE plain-Zod schema, NOT `InboundContactSchema` from
|
|
5
|
+
* `@wildo-ai/saas-models`: that schema is decorator-bound (`.isPrimaryKey()`,
|
|
6
|
+
* `.excludeFromCreate()`, `.isBackendOnly()`, …), so importing it would boot the
|
|
7
|
+
* decorator framework into the browser bundle — forbidden by bundle discipline. The
|
|
8
|
+
* website only needs to validate the handful of client-authored CREATE fields; the
|
|
9
|
+
* backend is the authority on the full contract (it re-validates + injects the
|
|
10
|
+
* server-set fields: `_id`, `anonymousUserId`, `userId`, `emailUserMatch`, timestamps).
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the non-excluded, non-backend-only CREATE inputs of `InboundContact`
|
|
13
|
+
* (`email` required; `firstName` / `lastName` / `phone` optional), plus TWO distinct
|
|
14
|
+
* privacy signal: an OPTIONAL marketing opt-in (mapped to the
|
|
15
|
+
* `privacy.dataProcessingConsent` tier). A governed notice, contract or consent
|
|
16
|
+
* interaction must use the separate exact-release receipt contract; this DTO cannot
|
|
17
|
+
* manufacture legal evidence from a checkbox and client timestamp.
|
|
18
|
+
* `relatedUserId` is intentionally absent — it is `.excludeFromCreate()` on the
|
|
19
|
+
* resource (sales/admin-only, never client-authored).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The wire payload POSTed to `/api/v1/inbound-contacts`. `privacy` matches the
|
|
26
|
+
* resource's `InboundContactPrivacySchema` shape (a nested consent object) so the
|
|
27
|
+
* backend accepts it verbatim.
|
|
28
|
+
*/
|
|
29
|
+
export const InboundContactFormSchema = z.object({
|
|
30
|
+
// Zod 4: top-level `z.email()` (not the deprecated `z.string().email()`), matching
|
|
31
|
+
// the resource schema. An empty string fails it, so it also covers "required".
|
|
32
|
+
email: z.email("Enter a valid email address"),
|
|
33
|
+
firstName: z.string().trim().max(200).optional(),
|
|
34
|
+
lastName: z.string().trim().max(200).optional(),
|
|
35
|
+
phone: z.string().trim().max(64).optional(),
|
|
36
|
+
/**
|
|
37
|
+
* OPTIONAL marketing opt-in — maps into the resource's `privacy.dataProcessingConsent`
|
|
38
|
+
* tier ('marketing' vs the BASIC default). Independent of the required agreement above.
|
|
39
|
+
*/
|
|
40
|
+
marketingConsent: z.boolean().default(false),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export type InboundContactFormValues = z.infer<typeof InboundContactFormSchema>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the exact JSON body the backend CREATE expects from validated form values.
|
|
47
|
+
* Keeps the `privacy` nesting + the consent enum mapping in ONE place so the form
|
|
48
|
+
* component stays declarative.
|
|
49
|
+
*/
|
|
50
|
+
export function buildInboundContactCreateBody(values: InboundContactFormValues): Record<string, unknown> {
|
|
51
|
+
return {
|
|
52
|
+
email: values.email.trim().toLowerCase(),
|
|
53
|
+
...(values.firstName ? { firstName: values.firstName } : {}),
|
|
54
|
+
...(values.lastName ? { lastName: values.lastName } : {}),
|
|
55
|
+
...(values.phone ? { phone: values.phone } : {}),
|
|
56
|
+
privacy: {
|
|
57
|
+
// OPTIONAL marketing tier: 'marketing' when the visitor opted in, else the
|
|
58
|
+
// resource's BASIC default.
|
|
59
|
+
dataProcessingConsent: values.marketingConsent ? "marketing" : "basic",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebsiteAnonymousSessionClient
|
|
3
|
+
*
|
|
4
|
+
* A self-contained, browser-only anonymous-session HTTP client for the marketing
|
|
5
|
+
* website. It MIRRORS the SaaS app's `AnonymousSessionContext` protocol WITHOUT
|
|
6
|
+
* importing `@wildo-ai/saas-frontend-lib` — the website ships into a browser bundle
|
|
7
|
+
* that must not pull the decorator framework / heavy frontend graph (bundle
|
|
8
|
+
* discipline). It uses ONLY browser globals (`fetch` / `crypto` / `localStorage`) and
|
|
9
|
+
* the bundle-safe `@wildo-ai/saas-models/public-runtime` atoms. No axios (axios is the
|
|
10
|
+
* SSR tier), no React — a React island context can wrap this class.
|
|
11
|
+
*
|
|
12
|
+
* Protocol (W7.3a):
|
|
13
|
+
* 1. The first anonymous request sends `x-anonymous-session-init: <clientUuid>` — the
|
|
14
|
+
* backend mints an `AnonymousUser` (uuidRef = clientUuid) and returns a signed JWT
|
|
15
|
+
* in the `x-anonymous-session-token` RESPONSE header.
|
|
16
|
+
* 2. Subsequent requests send `x-anonymous-session: <jwt>`.
|
|
17
|
+
* Every request also sends `X-Frontend-Service-Name` (required by the anon backend
|
|
18
|
+
* to accept the request and resolve the frontend identity).
|
|
19
|
+
*
|
|
20
|
+
* CORS (critical): the website is a DIFFERENT origin from the backend API, so the
|
|
21
|
+
* `x-anonymous-session-token` response header is readable by `fetch` ONLY if the
|
|
22
|
+
* backend lists it in `Access-Control-Expose-Headers` (non-safelisted response headers
|
|
23
|
+
* are hidden from cross-origin `fetch`). Without that, the captured token is `null`,
|
|
24
|
+
* the session never advances past init, and every request re-mints a session.
|
|
25
|
+
*
|
|
26
|
+
* Cross-origin continuity: the anonymous session created here (uuidRef) is what the
|
|
27
|
+
* SaaS app transposes on signup/login. It is carried across the apex↔app boundary by
|
|
28
|
+
* the backend-set parent-domain `__wildo_anon_session` cookie — hence `credentials:
|
|
29
|
+
* 'include'`, so that cookie rides same-site requests to the API.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { WildoHeaderKeys, MAIN_API_BASE_PATH } from '@wildo-ai/saas-models/public-runtime';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* localStorage keys. The website is self-contained (mirror-don't-import), so it pins
|
|
36
|
+
* the stable literal values rather than importing the decorator-adjacent shared
|
|
37
|
+
* contract. Cross-origin continuity flows through the backend cookie, NOT this
|
|
38
|
+
* localStorage, so these keys need not match the app's byte-for-byte.
|
|
39
|
+
*/
|
|
40
|
+
const STORAGE_KEYS = {
|
|
41
|
+
anonymousUserId: 'anonymousUserId',
|
|
42
|
+
anonymousSessionToken: 'anonymousSessionToken',
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
45
|
+
export interface WebsiteAnonymousSessionClientOptions {
|
|
46
|
+
/** Backend API origin WITHOUT a trailing slash, e.g. `https://api.example.com`. */
|
|
47
|
+
apiOrigin: string;
|
|
48
|
+
/** `X-Frontend-Service-Name` value identifying this marketing site to the backend. */
|
|
49
|
+
frontendServiceName: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Thrown when an anonymous request returns a non-2xx status. */
|
|
53
|
+
export class WebsiteAnonymousRequestError extends Error {
|
|
54
|
+
constructor(
|
|
55
|
+
public readonly status: number,
|
|
56
|
+
public readonly bodyText: string,
|
|
57
|
+
) {
|
|
58
|
+
super(`Anonymous request failed with status ${status}`);
|
|
59
|
+
this.name = 'WebsiteAnonymousRequestError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class WebsiteAnonymousSessionClient {
|
|
64
|
+
private anonymousUserId: string | null = null;
|
|
65
|
+
private sessionToken: string | null = null;
|
|
66
|
+
private readonly apiBaseUrl: string;
|
|
67
|
+
private readonly frontendServiceName: string;
|
|
68
|
+
|
|
69
|
+
constructor(options: WebsiteAnonymousSessionClientOptions) {
|
|
70
|
+
this.apiBaseUrl = `${options.apiOrigin.replace(/\/+$/, '')}${MAIN_API_BASE_PATH}`;
|
|
71
|
+
this.frontendServiceName = options.frontendServiceName;
|
|
72
|
+
this.restoreFromStorage();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The anonymous session's relational identity (client uuidRef), or `null` if not started. */
|
|
76
|
+
getAnonymousUserId(): string | null {
|
|
77
|
+
return this.anonymousUserId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return the anonymous session's relational identity (client uuidRef),
|
|
82
|
+
* STARTING the session first if it has not begun yet. Unlike
|
|
83
|
+
* `getAnonymousUserId()` (which may return `null`), this always yields
|
|
84
|
+
* a stable uuidRef.
|
|
85
|
+
*
|
|
86
|
+
* Used by NESTED anonymous routes that must embed the id in the URL
|
|
87
|
+
* BEFORE the first request — e.g. draft-notes POSTs to
|
|
88
|
+
* `/user-anonymous-users/<uuidRef>/draft-notes`. The id generated here
|
|
89
|
+
* is the SAME one the next `postAnonymous(...)` sends as
|
|
90
|
+
* `x-anonymous-session-init`, so the URL scope and the minted session
|
|
91
|
+
* refer to one anonymous user (no split identity).
|
|
92
|
+
*/
|
|
93
|
+
getOrStartAnonymousUserId(): string {
|
|
94
|
+
this.ensureSessionStarted();
|
|
95
|
+
// ensureSessionStarted() guarantees anonymousUserId is non-null.
|
|
96
|
+
return this.anonymousUserId as string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `true` once an init request has succeeded and a session token is held. */
|
|
100
|
+
isSessionActive(): boolean {
|
|
101
|
+
return this.anonymousUserId !== null && this.sessionToken !== null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* POST `body` to an anonymous endpoint. `path` is relative to `/api/v1`
|
|
106
|
+
* (e.g. `/inbound-contacts`, or a nested `/anonymous-users/<uuidRef>/draft-notes`).
|
|
107
|
+
* Lazily starts the session on the first call, attaches the anon-session +
|
|
108
|
+
* service-name headers, captures any returned session token, and returns the parsed
|
|
109
|
+
* JSON response. Throws `WebsiteAnonymousRequestError` on a non-2xx status.
|
|
110
|
+
*/
|
|
111
|
+
async postAnonymous<TResponse = unknown>(path: string, body: unknown): Promise<TResponse> {
|
|
112
|
+
this.ensureSessionStarted();
|
|
113
|
+
|
|
114
|
+
const response = await fetch(`${this.apiBaseUrl}${path}`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: this.buildHeaders(),
|
|
117
|
+
body: JSON.stringify(body),
|
|
118
|
+
// Carry the parent-domain `__wildo_anon_session` cookie to the API so the
|
|
119
|
+
// session is continuous across the apex/app/api origins.
|
|
120
|
+
credentials: 'include',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.captureSessionToken(response);
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new WebsiteAnonymousRequestError(response.status, await safeReadText(response));
|
|
127
|
+
}
|
|
128
|
+
return (await response.json()) as TResponse;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private ensureSessionStarted(): void {
|
|
132
|
+
if (!this.anonymousUserId) {
|
|
133
|
+
this.anonymousUserId = crypto.randomUUID();
|
|
134
|
+
this.sessionToken = null;
|
|
135
|
+
this.persist();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private buildHeaders(): Record<string, string> {
|
|
140
|
+
const headers: Record<string, string> = {
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
[WildoHeaderKeys.FRONTEND_SERVICE_NAME]: this.frontendServiceName,
|
|
143
|
+
};
|
|
144
|
+
// Once we hold the JWT, restore the session; before that, the raw UUID inits it.
|
|
145
|
+
if (this.sessionToken) {
|
|
146
|
+
headers[WildoHeaderKeys.ANONYMOUS_SESSION] = this.sessionToken;
|
|
147
|
+
} else if (this.anonymousUserId) {
|
|
148
|
+
headers[WildoHeaderKeys.ANONYMOUS_SESSION_INIT] = this.anonymousUserId;
|
|
149
|
+
}
|
|
150
|
+
return headers;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private captureSessionToken(response: Response): void {
|
|
154
|
+
// Requires the backend to CORS-expose this header (see the class-level CORS note).
|
|
155
|
+
const token = response.headers.get(WildoHeaderKeys.ANONYMOUS_SESSION_TOKEN);
|
|
156
|
+
if (token && token !== this.sessionToken) {
|
|
157
|
+
this.sessionToken = token;
|
|
158
|
+
this.persist();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private restoreFromStorage(): void {
|
|
163
|
+
try {
|
|
164
|
+
const userId = localStorage.getItem(STORAGE_KEYS.anonymousUserId);
|
|
165
|
+
const token = localStorage.getItem(STORAGE_KEYS.anonymousSessionToken);
|
|
166
|
+
if (userId) this.anonymousUserId = userId;
|
|
167
|
+
if (token) this.sessionToken = token;
|
|
168
|
+
} catch {
|
|
169
|
+
// localStorage unavailable (SSR build / privacy mode) — the session stays
|
|
170
|
+
// in-memory for this page load only.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private persist(): void {
|
|
175
|
+
try {
|
|
176
|
+
if (this.anonymousUserId) {
|
|
177
|
+
localStorage.setItem(STORAGE_KEYS.anonymousUserId, this.anonymousUserId);
|
|
178
|
+
}
|
|
179
|
+
if (this.sessionToken) {
|
|
180
|
+
localStorage.setItem(STORAGE_KEYS.anonymousSessionToken, this.sessionToken);
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Best-effort persistence; a failure only means the session won't survive reload.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function safeReadText(response: Response): Promise<string> {
|
|
189
|
+
try {
|
|
190
|
+
return await response.text();
|
|
191
|
+
} catch {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React, { createContext, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { WebsitePageRef } from '../../schemas/refs/page-ref.schemas';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @wildo_source:part:start saas.website.page.context facet:layer:core facet:family:website
|
|
7
|
+
*
|
|
8
|
+
* `WebsitePageContext` — identity of the page currently rendering.
|
|
9
|
+
* Provided by `<WebsitePageLayout pageRef="…">` exactly once per
|
|
10
|
+
* page render and consumed by `useWebsiteLabel(leaf)` to compose the
|
|
11
|
+
* full label key (`website.{pageRef}.{sectionRef}.{leaf}`).
|
|
12
|
+
*
|
|
13
|
+
* **Why an isolated context (not merged into runtime)**: the runtime
|
|
14
|
+
* context is set at the host-adapter boundary and is constant per
|
|
15
|
+
* build; the page context is set per page route. Splitting them
|
|
16
|
+
* lets a single host adapter render multiple pages (preview tooling,
|
|
17
|
+
* MDX-driven blog index aggregating post excerpts, etc.) without
|
|
18
|
+
* lying about which page each subtree belongs to.
|
|
19
|
+
*
|
|
20
|
+
* **Why a separate file from the hook**: same Fast-Refresh rationale
|
|
21
|
+
* as `WebsiteRuntimeContext` — split provider/context/hook to keep
|
|
22
|
+
* HMR boundaries small.
|
|
23
|
+
*/
|
|
24
|
+
export interface WebsitePageContextValue {
|
|
25
|
+
/**
|
|
26
|
+
* The validated `WebsitePageRef` of the page currently rendering.
|
|
27
|
+
* MUST match a `WebsitePageManifest.pageRef` registered in the
|
|
28
|
+
* site's `WebsiteRootConfig`.
|
|
29
|
+
*/
|
|
30
|
+
pageRef: WebsitePageRef;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const WebsitePageContext: React.Context<WebsitePageContextValue | null> =
|
|
34
|
+
createContext<WebsitePageContextValue | null>(null);
|
|
35
|
+
|
|
36
|
+
export interface WebsitePageContextProviderProps {
|
|
37
|
+
value: WebsitePageContextValue;
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function WebsitePageContextProvider({
|
|
42
|
+
value,
|
|
43
|
+
children,
|
|
44
|
+
}: WebsitePageContextProviderProps): ReactNode {
|
|
45
|
+
return (
|
|
46
|
+
<WebsitePageContext.Provider value={value}>
|
|
47
|
+
{children}
|
|
48
|
+
</WebsitePageContext.Provider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
WebsitePageContextProvider.displayName = 'WebsitePageContextProvider';
|
|
52
|
+
/** @wildo_source:part:end saas.website.page.context */
|