@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,120 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import { WebsiteLabelKeySchema } from '../label-keys/website-label-key.schemas';
|
|
5
|
+
import { WebsiteSectionRefSchema } from '../refs/section-ref.schemas';
|
|
6
|
+
import type { WebsiteStructuredDataReturn } from '../structured-data/website-structured-data.shared.schemas';
|
|
7
|
+
import { WebsiteSectionCategorySchema } from './website-section-category.shared';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @wildo_source:part:start saas.website.sections.definition-schema facet:layer:shared facet:family:website
|
|
11
|
+
*
|
|
12
|
+
* `WebsiteSectionDefinition` — the runtime contract returned by the
|
|
13
|
+
* `defineWebsiteSection({ … })` factory (authored as a separate
|
|
14
|
+
* `core/define-website-section.tsx` module in Phase 2 because it carries
|
|
15
|
+
* React types in its inputs). The shape is declared in `schemas/`
|
|
16
|
+
* because it is the **observable contract** the page-manifest
|
|
17
|
+
* registration, label-pack validator, and SEO dispatcher depend on.
|
|
18
|
+
*
|
|
19
|
+
* **Shape rationale** (each field carries semantic weight — see
|
|
20
|
+
* `design-philosophy.mdc`):
|
|
21
|
+
*
|
|
22
|
+
* - `sectionRef` — the section's identity inside its hosting page.
|
|
23
|
+
* Uniqueness is enforced **per page** at manifest registration time
|
|
24
|
+
* (a section can be reused with the same `sectionRef` across pages
|
|
25
|
+
* when its label-key root is intentionally shared).
|
|
26
|
+
* - `category` — see `WebsiteSectionCategory` JSDoc; tags the section
|
|
27
|
+
* for SEO-dispatch, skill-mapping, and analytics labelling.
|
|
28
|
+
* - `expectedLabelKeys` — the **fully-composed** label keys the section
|
|
29
|
+
* commits to consuming. The plan originally proposed leaf-relative
|
|
30
|
+
* keys (`'headline'`) resolved at runtime against the
|
|
31
|
+
* page+section context, but storing the fully-composed key on the
|
|
32
|
+
* definition has three concrete benefits:
|
|
33
|
+
* 1. The label-pack validator (`validateLabelPackAgainstManifest`)
|
|
34
|
+
* runs as a pure function with no need to walk the JSX tree at
|
|
35
|
+
* build time to discover the leaf-to-key resolution.
|
|
36
|
+
* 2. The contract is auditable from outside the React render
|
|
37
|
+
* graph — `wildo website audit` can list a page's required keys
|
|
38
|
+
* without booting React.
|
|
39
|
+
* 3. The leaf-relative ergonomics (`useWebsiteLabel('headline')`)
|
|
40
|
+
* survive in Phase 2 — the hook composes the leaf with the
|
|
41
|
+
* page+section context and validates the result against
|
|
42
|
+
* `expectedLabelKeys`.
|
|
43
|
+
* - `Component` — the React component that renders the section. Typed
|
|
44
|
+
* as `ComponentType<unknown>` here because schema-layer cannot lock
|
|
45
|
+
* the component's prop shape (props vary per section). The factory
|
|
46
|
+
* function (`defineWebsiteSection<TProps>(…)`) preserves the precise
|
|
47
|
+
* prop type at the consumer call site via generics; this schema only
|
|
48
|
+
* guards the runtime shape of "is callable as a React component".
|
|
49
|
+
* - `getStructuredData?` — optional pure function the SEO/AEO/GEO
|
|
50
|
+
* builder library invokes at static-build time to materialize JSON-LD
|
|
51
|
+
* for the section. The return type is intentionally `unknown` here;
|
|
52
|
+
* the SEO library narrows by `category` (e.g. `PRICING` →
|
|
53
|
+
* `Product | AggregateOffer`).
|
|
54
|
+
*
|
|
55
|
+
* **Why not `z.custom<ComponentType>(…)`**: a `z.custom` predicate
|
|
56
|
+
* cannot meaningfully validate "is a React component" beyond
|
|
57
|
+
* `typeof === 'function'`, and the false-positive cost (any function
|
|
58
|
+
* passes) is too high for the value (none — the consumer either
|
|
59
|
+
* imports a real component or fails at JSX render time with a clearer
|
|
60
|
+
* error). Using `z.unknown()` + a TS-side type cast keeps the runtime
|
|
61
|
+
* cost zero and the type-safety contract entirely on the TS side
|
|
62
|
+
* (where it actually has teeth via the factory's generic).
|
|
63
|
+
*/
|
|
64
|
+
const _WebsiteSectionDefinitionSchema = z.object({
|
|
65
|
+
sectionRef: WebsiteSectionRefSchema,
|
|
66
|
+
category: WebsiteSectionCategorySchema,
|
|
67
|
+
expectedLabelKeys: z.array(WebsiteLabelKeySchema).readonly(),
|
|
68
|
+
Component: z.unknown(),
|
|
69
|
+
getStructuredData: z.unknown().optional(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const WebsiteSectionDefinitionSchema: typeof _WebsiteSectionDefinitionSchema =
|
|
73
|
+
_WebsiteSectionDefinitionSchema;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Runtime shape of `WebsiteSectionDefinition`. Note: `Component` and
|
|
77
|
+
* `getStructuredData` are typed precisely on the factory return type
|
|
78
|
+
* (`defineWebsiteSection<TProps>` in Phase 2); the schema-inferred
|
|
79
|
+
* type is the lowest common denominator used at the persistence /
|
|
80
|
+
* validation boundary only.
|
|
81
|
+
*
|
|
82
|
+
* **Why `ComponentType<any>` and NOT `ComponentType<unknown>`**:
|
|
83
|
+
* React component props are CONTRAVARIANT in `ComponentType<T>` —
|
|
84
|
+
* a narrower `ComponentType<{ foo: string }>` is NOT assignable to
|
|
85
|
+
* `ComponentType<unknown>` (because a caller could pass an `unknown`
|
|
86
|
+
* the narrower component cannot accept). This made every consumer
|
|
87
|
+
* who built sections via `defineWebsiteSection<TProps>(…)` unable
|
|
88
|
+
* to feed the resulting `TypedWebsiteSectionDefinition<TProps>` to
|
|
89
|
+
* helpers typed against the bare `WebsiteSectionDefinition` —
|
|
90
|
+
* collectors, validators, and registries. Using `any` here is the
|
|
91
|
+
* exact escape hatch the React typings themselves use for
|
|
92
|
+
* heterogeneous component containers (`React.forwardRef`,
|
|
93
|
+
* `React.lazy`'s internal component slot, etc.) and matches the
|
|
94
|
+
* runtime contract: the framework NEVER instantiates these
|
|
95
|
+
* components directly with arbitrary props — the consumer's JSX
|
|
96
|
+
* supplies the right props at the call site.
|
|
97
|
+
*/
|
|
98
|
+
export interface WebsiteSectionDefinition extends z.infer<typeof _WebsiteSectionDefinitionSchema> {
|
|
99
|
+
Component: ComponentType<any>;
|
|
100
|
+
/**
|
|
101
|
+
* Optional pure function the SEO/AEO/GEO renderer invokes at
|
|
102
|
+
* static-build time to materialize JSON-LD for the section.
|
|
103
|
+
*
|
|
104
|
+
* **Why typed as `WebsiteStructuredDataReturn` (not `unknown`)** —
|
|
105
|
+
* a typed return shape gives section authors autocomplete on
|
|
106
|
+
* `@type` / `@id` / `@context` (the framework-owned fields) while
|
|
107
|
+
* keeping all other fields free-form (Schema.org has hundreds of
|
|
108
|
+
* optional fields per entity; locking them in TS would be both
|
|
109
|
+
* impractical and brittle as Schema.org evolves). The companion
|
|
110
|
+
* catalog reconciliation surface catches missing-required and
|
|
111
|
+
* deprecated-field-used at the SEMANTIC layer (warning-only per
|
|
112
|
+
* D-AG4), so the runtime type stays loose.
|
|
113
|
+
*
|
|
114
|
+
* Returns `null`, `undefined`, or `[]` when the section has no
|
|
115
|
+
* structured-data contribution for the current state — the
|
|
116
|
+
* renderer short-circuits all three.
|
|
117
|
+
*/
|
|
118
|
+
getStructuredData?: () => WebsiteStructuredDataReturn;
|
|
119
|
+
}
|
|
120
|
+
/** @wildo_source:part:end saas.website.sections.definition-schema */
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
export interface StructuredDataDeprecatedField {
|
|
2
|
+
field: string;
|
|
3
|
+
replacedBy: string | null;
|
|
4
|
+
deprecatedSince: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface StructuredDataCatalogEntry {
|
|
8
|
+
'@type': string;
|
|
9
|
+
requiredFields: readonly string[];
|
|
10
|
+
optionalFields?: readonly string[];
|
|
11
|
+
deprecatedFields?: readonly StructuredDataDeprecatedField[];
|
|
12
|
+
recommendOptionals?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StructuredDataPayload {
|
|
16
|
+
'@type': string;
|
|
17
|
+
'@id'?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StructuredDataSectionDefinitionLike {
|
|
22
|
+
sectionRef: string;
|
|
23
|
+
getStructuredData?: () => unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StructuredDataPageManifestLike {
|
|
27
|
+
ref: string;
|
|
28
|
+
routePath: string;
|
|
29
|
+
sectionRefs: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StructuredDataPagePayloadLike {
|
|
33
|
+
pageRef: string;
|
|
34
|
+
routePath?: string;
|
|
35
|
+
sourceRef?: string;
|
|
36
|
+
payloads: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type StructuredDataSpecViolationCode =
|
|
40
|
+
| 'invalid-payload-shape'
|
|
41
|
+
| 'missing-entity-type'
|
|
42
|
+
| 'missing-required-field'
|
|
43
|
+
| 'missing-section-definition'
|
|
44
|
+
| 'orphan-page-payload';
|
|
45
|
+
|
|
46
|
+
export interface StructuredDataCatalogOverrideFinding {
|
|
47
|
+
kind: 'catalog-override';
|
|
48
|
+
entityType: string;
|
|
49
|
+
count: number;
|
|
50
|
+
message: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface StructuredDataCatalogMissFinding {
|
|
54
|
+
kind: 'catalog-miss';
|
|
55
|
+
pageRef: string;
|
|
56
|
+
routePath: string;
|
|
57
|
+
sectionRef: string;
|
|
58
|
+
entityType: string;
|
|
59
|
+
entityId?: string;
|
|
60
|
+
message: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StructuredDataSpecViolationFinding {
|
|
64
|
+
kind: 'spec-violation';
|
|
65
|
+
code: StructuredDataSpecViolationCode;
|
|
66
|
+
pageRef: string;
|
|
67
|
+
routePath: string;
|
|
68
|
+
sectionRef: string;
|
|
69
|
+
entityType?: string;
|
|
70
|
+
entityId?: string;
|
|
71
|
+
field?: string;
|
|
72
|
+
message: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface StructuredDataDeprecatedFieldFinding {
|
|
76
|
+
kind: 'deprecated-field';
|
|
77
|
+
pageRef: string;
|
|
78
|
+
routePath: string;
|
|
79
|
+
sectionRef: string;
|
|
80
|
+
entityType: string;
|
|
81
|
+
entityId?: string;
|
|
82
|
+
field: string;
|
|
83
|
+
replacedBy: string | null;
|
|
84
|
+
deprecatedSince: string;
|
|
85
|
+
message: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface StructuredDataCrossEntityReferenceDriftFinding {
|
|
89
|
+
kind: 'cross-entity-reference-drift';
|
|
90
|
+
pageRef: string;
|
|
91
|
+
routePath: string;
|
|
92
|
+
sectionRef: string;
|
|
93
|
+
entityType: string;
|
|
94
|
+
entityId?: string;
|
|
95
|
+
missingReferenceId: string;
|
|
96
|
+
message: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type StructuredDataReconciliationFinding =
|
|
100
|
+
| StructuredDataCatalogOverrideFinding
|
|
101
|
+
| StructuredDataCatalogMissFinding
|
|
102
|
+
| StructuredDataSpecViolationFinding
|
|
103
|
+
| StructuredDataDeprecatedFieldFinding
|
|
104
|
+
| StructuredDataCrossEntityReferenceDriftFinding;
|
|
105
|
+
|
|
106
|
+
export interface StructuredDataReconciliationPageResult {
|
|
107
|
+
pageRef: string;
|
|
108
|
+
routePath: string;
|
|
109
|
+
payloadCount: number;
|
|
110
|
+
findingCount: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface StructuredDataReconciliationResult {
|
|
114
|
+
findings: StructuredDataReconciliationFinding[];
|
|
115
|
+
pages: StructuredDataReconciliationPageResult[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ReconcileStructuredDataOptions {
|
|
119
|
+
catalog: readonly StructuredDataCatalogEntry[];
|
|
120
|
+
pages: readonly StructuredDataPageManifestLike[];
|
|
121
|
+
sections: readonly StructuredDataSectionDefinitionLike[];
|
|
122
|
+
pagePayloads?: readonly StructuredDataPagePayloadLike[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface ReconciliationPayloadContext {
|
|
126
|
+
pageRef: string;
|
|
127
|
+
routePath: string;
|
|
128
|
+
sectionRef: string;
|
|
129
|
+
payload: StructuredDataPayload;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function reconcileStructuredDataAgainstCatalog(
|
|
133
|
+
options: ReconcileStructuredDataOptions,
|
|
134
|
+
): StructuredDataReconciliationResult {
|
|
135
|
+
const findings: StructuredDataReconciliationFinding[] = [];
|
|
136
|
+
const pages: StructuredDataReconciliationPageResult[] = [];
|
|
137
|
+
const sectionsByRef = new Map(
|
|
138
|
+
options.sections.map((section) => [section.sectionRef, section] as const),
|
|
139
|
+
);
|
|
140
|
+
const pagePayloadsByPageRef = new Map<string, StructuredDataPagePayloadLike[]>();
|
|
141
|
+
for (const pagePayload of options.pagePayloads ?? []) {
|
|
142
|
+
const pagePayloads = pagePayloadsByPageRef.get(pagePayload.pageRef) ?? [];
|
|
143
|
+
pagePayloads.push(pagePayload);
|
|
144
|
+
pagePayloadsByPageRef.set(pagePayload.pageRef, pagePayloads);
|
|
145
|
+
}
|
|
146
|
+
const catalogByType = new Map<string, StructuredDataCatalogEntry>();
|
|
147
|
+
const catalogCounts = new Map<string, number>();
|
|
148
|
+
|
|
149
|
+
for (const entry of options.catalog) {
|
|
150
|
+
catalogByType.set(entry['@type'], entry);
|
|
151
|
+
catalogCounts.set(entry['@type'], (catalogCounts.get(entry['@type']) ?? 0) + 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const [entityType, count] of catalogCounts.entries()) {
|
|
155
|
+
if (count > 1) {
|
|
156
|
+
findings.push({
|
|
157
|
+
kind: 'catalog-override',
|
|
158
|
+
entityType,
|
|
159
|
+
count,
|
|
160
|
+
message:
|
|
161
|
+
`Merged catalog contains ${count} entries for @type="${entityType}". ` +
|
|
162
|
+
'Replacement semantics are last-wins; duplicate array entries indicate an unresolved override.',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const page of options.pages) {
|
|
168
|
+
const pageFindingStart = findings.length;
|
|
169
|
+
const sectionPayloadContexts: ReconciliationPayloadContext[] = [];
|
|
170
|
+
const isTemplatedRoute = page.routePath.includes('[');
|
|
171
|
+
|
|
172
|
+
for (const sectionRef of page.sectionRefs) {
|
|
173
|
+
const section = sectionsByRef.get(sectionRef);
|
|
174
|
+
if (!section) {
|
|
175
|
+
findings.push({
|
|
176
|
+
kind: 'spec-violation',
|
|
177
|
+
code: 'missing-section-definition',
|
|
178
|
+
pageRef: page.ref,
|
|
179
|
+
routePath: page.routePath,
|
|
180
|
+
sectionRef,
|
|
181
|
+
message:
|
|
182
|
+
`Page "${page.ref}" references sectionRef "${sectionRef}" but no matching section ` +
|
|
183
|
+
'definition was supplied to the structured-data reconciliation harness.',
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!section.getStructuredData) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const rawStructuredData = section.getStructuredData();
|
|
193
|
+
const normalizedPayloads = normalizeStructuredDataReturn(rawStructuredData);
|
|
194
|
+
|
|
195
|
+
if (normalizedPayloads === null) {
|
|
196
|
+
findings.push({
|
|
197
|
+
kind: 'spec-violation',
|
|
198
|
+
code: 'invalid-payload-shape',
|
|
199
|
+
pageRef: page.ref,
|
|
200
|
+
routePath: page.routePath,
|
|
201
|
+
sectionRef,
|
|
202
|
+
message:
|
|
203
|
+
`Section "${sectionRef}" on page "${page.ref}" returned an invalid ` +
|
|
204
|
+
'`getStructuredData()` shape. Expected object, array, null, or undefined.',
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const candidate of normalizedPayloads) {
|
|
210
|
+
if (!isPlainObject(candidate)) {
|
|
211
|
+
findings.push({
|
|
212
|
+
kind: 'spec-violation',
|
|
213
|
+
code: 'invalid-payload-shape',
|
|
214
|
+
pageRef: page.ref,
|
|
215
|
+
routePath: page.routePath,
|
|
216
|
+
sectionRef,
|
|
217
|
+
message:
|
|
218
|
+
`Section "${sectionRef}" on page "${page.ref}" returned a non-object structured-data payload.`,
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (typeof candidate['@type'] !== 'string' || candidate['@type'].trim().length === 0) {
|
|
224
|
+
findings.push({
|
|
225
|
+
kind: 'spec-violation',
|
|
226
|
+
code: 'missing-entity-type',
|
|
227
|
+
pageRef: page.ref,
|
|
228
|
+
routePath: page.routePath,
|
|
229
|
+
sectionRef,
|
|
230
|
+
entityId: typeof candidate['@id'] === 'string' ? candidate['@id'] : undefined,
|
|
231
|
+
message:
|
|
232
|
+
`Section "${sectionRef}" on page "${page.ref}" returned a structured-data payload ` +
|
|
233
|
+
'without a non-empty string `@type`.',
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
sectionPayloadContexts.push({
|
|
239
|
+
pageRef: page.ref,
|
|
240
|
+
routePath: page.routePath,
|
|
241
|
+
sectionRef,
|
|
242
|
+
payload: candidate as StructuredDataPayload,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const contextsByRoutePath = new Map<string, ReconciliationPayloadContext[]>();
|
|
248
|
+
const collectionFindingCountByRoutePath = new Map<string, number>();
|
|
249
|
+
const pagePayloadsForPage = pagePayloadsByPageRef.get(page.ref) ?? [];
|
|
250
|
+
pagePayloadsByPageRef.delete(page.ref);
|
|
251
|
+
const shouldEmitManifestResult =
|
|
252
|
+
(sectionPayloadContexts.length > 0 && (!isTemplatedRoute || pagePayloadsForPage.length === 0))
|
|
253
|
+
|| findings.length > pageFindingStart
|
|
254
|
+
|| pagePayloadsForPage.length === 0;
|
|
255
|
+
if (shouldEmitManifestResult) {
|
|
256
|
+
const manifestPayloadContexts =
|
|
257
|
+
isTemplatedRoute && pagePayloadsForPage.length > 0
|
|
258
|
+
? []
|
|
259
|
+
: [...sectionPayloadContexts];
|
|
260
|
+
contextsByRoutePath.set(page.routePath, manifestPayloadContexts);
|
|
261
|
+
collectionFindingCountByRoutePath.set(page.routePath, findings.length - pageFindingStart);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const pagePayload of pagePayloadsForPage) {
|
|
265
|
+
const routePath = pagePayload.routePath ?? page.routePath;
|
|
266
|
+
const payloadContexts = contextsByRoutePath.get(routePath) ?? [];
|
|
267
|
+
if (
|
|
268
|
+
payloadContexts.length === 0
|
|
269
|
+
&& isTemplatedRoute
|
|
270
|
+
&& sectionPayloadContexts.length > 0
|
|
271
|
+
) {
|
|
272
|
+
payloadContexts.push(
|
|
273
|
+
...sectionPayloadContexts.map((context) => ({
|
|
274
|
+
...context,
|
|
275
|
+
routePath,
|
|
276
|
+
})),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
contextsByRoutePath.set(routePath, payloadContexts);
|
|
280
|
+
const collectionFindingStart = findings.length;
|
|
281
|
+
collectNormalizedPayloadContexts({
|
|
282
|
+
rawStructuredData: pagePayload.payloads,
|
|
283
|
+
pageRef: page.ref,
|
|
284
|
+
routePath,
|
|
285
|
+
sectionRef: pagePayload.sourceRef ?? `page:${page.ref}`,
|
|
286
|
+
findings,
|
|
287
|
+
into: payloadContexts,
|
|
288
|
+
});
|
|
289
|
+
collectionFindingCountByRoutePath.set(
|
|
290
|
+
routePath,
|
|
291
|
+
(collectionFindingCountByRoutePath.get(routePath) ?? 0)
|
|
292
|
+
+ findings.length - collectionFindingStart,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const [routePath, payloadContexts] of contextsByRoutePath.entries()) {
|
|
297
|
+
const declaredEntityIds = new Set<string>();
|
|
298
|
+
for (const payloadContext of payloadContexts) {
|
|
299
|
+
collectDeclaredEntityIds(payloadContext.payload, declaredEntityIds);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const routeFindingStart = findings.length;
|
|
303
|
+
for (const payloadContext of payloadContexts) {
|
|
304
|
+
validatePayloadAgainstCatalog(payloadContext, catalogByType, declaredEntityIds, findings);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
pages.push({
|
|
308
|
+
pageRef: page.ref,
|
|
309
|
+
routePath,
|
|
310
|
+
payloadCount: payloadContexts.length,
|
|
311
|
+
findingCount:
|
|
312
|
+
(collectionFindingCountByRoutePath.get(routePath) ?? 0)
|
|
313
|
+
+ findings.length - routeFindingStart,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const [pageRef, pagePayloads] of pagePayloadsByPageRef.entries()) {
|
|
319
|
+
for (const pagePayload of pagePayloads) {
|
|
320
|
+
findings.push({
|
|
321
|
+
kind: 'spec-violation',
|
|
322
|
+
code: 'orphan-page-payload',
|
|
323
|
+
pageRef,
|
|
324
|
+
routePath: pagePayload.routePath ?? '',
|
|
325
|
+
sectionRef: pagePayload.sourceRef ?? `page:${pageRef}`,
|
|
326
|
+
message:
|
|
327
|
+
`Structured-data page payload "${pagePayload.sourceRef ?? `page:${pageRef}`}" ` +
|
|
328
|
+
`references pageRef "${pageRef}" but no matching page manifest was supplied.`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
findings,
|
|
335
|
+
pages,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function collectNormalizedPayloadContexts(input: {
|
|
340
|
+
rawStructuredData: unknown;
|
|
341
|
+
pageRef: string;
|
|
342
|
+
routePath: string;
|
|
343
|
+
sectionRef: string;
|
|
344
|
+
findings: StructuredDataReconciliationFinding[];
|
|
345
|
+
into: ReconciliationPayloadContext[];
|
|
346
|
+
}): void {
|
|
347
|
+
const {
|
|
348
|
+
rawStructuredData,
|
|
349
|
+
pageRef,
|
|
350
|
+
routePath,
|
|
351
|
+
sectionRef,
|
|
352
|
+
findings,
|
|
353
|
+
into,
|
|
354
|
+
} = input;
|
|
355
|
+
const normalizedPayloads = normalizeStructuredDataReturn(rawStructuredData);
|
|
356
|
+
|
|
357
|
+
if (normalizedPayloads === null) {
|
|
358
|
+
findings.push({
|
|
359
|
+
kind: 'spec-violation',
|
|
360
|
+
code: 'invalid-payload-shape',
|
|
361
|
+
pageRef,
|
|
362
|
+
routePath,
|
|
363
|
+
sectionRef,
|
|
364
|
+
message:
|
|
365
|
+
`Structured-data source "${sectionRef}" on page "${pageRef}" returned an invalid ` +
|
|
366
|
+
'structured-data shape. Expected object, array, null, or undefined.',
|
|
367
|
+
});
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const candidate of normalizedPayloads) {
|
|
372
|
+
if (!isPlainObject(candidate)) {
|
|
373
|
+
findings.push({
|
|
374
|
+
kind: 'spec-violation',
|
|
375
|
+
code: 'invalid-payload-shape',
|
|
376
|
+
pageRef,
|
|
377
|
+
routePath,
|
|
378
|
+
sectionRef,
|
|
379
|
+
message:
|
|
380
|
+
`Structured-data source "${sectionRef}" on page "${pageRef}" returned a non-object structured-data payload.`,
|
|
381
|
+
});
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (typeof candidate['@type'] !== 'string' || candidate['@type'].trim().length === 0) {
|
|
386
|
+
findings.push({
|
|
387
|
+
kind: 'spec-violation',
|
|
388
|
+
code: 'missing-entity-type',
|
|
389
|
+
pageRef,
|
|
390
|
+
routePath,
|
|
391
|
+
sectionRef,
|
|
392
|
+
entityId: typeof candidate['@id'] === 'string' ? candidate['@id'] : undefined,
|
|
393
|
+
message:
|
|
394
|
+
`Structured-data source "${sectionRef}" on page "${pageRef}" returned a structured-data payload ` +
|
|
395
|
+
'without a non-empty string `@type`.',
|
|
396
|
+
});
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
into.push({
|
|
401
|
+
pageRef,
|
|
402
|
+
routePath,
|
|
403
|
+
sectionRef,
|
|
404
|
+
payload: candidate as StructuredDataPayload,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function validatePayloadAgainstCatalog(
|
|
410
|
+
payloadContext: ReconciliationPayloadContext,
|
|
411
|
+
catalogByType: ReadonlyMap<string, StructuredDataCatalogEntry>,
|
|
412
|
+
declaredEntityIds: ReadonlySet<string>,
|
|
413
|
+
findings: StructuredDataReconciliationFinding[],
|
|
414
|
+
): void {
|
|
415
|
+
const { payload, pageRef, routePath, sectionRef } = payloadContext;
|
|
416
|
+
const entityType = payload['@type'];
|
|
417
|
+
const entityId = typeof payload['@id'] === 'string' ? payload['@id'] : undefined;
|
|
418
|
+
const catalogEntry = catalogByType.get(entityType);
|
|
419
|
+
|
|
420
|
+
if (!catalogEntry) {
|
|
421
|
+
findings.push({
|
|
422
|
+
kind: 'catalog-miss',
|
|
423
|
+
pageRef,
|
|
424
|
+
routePath,
|
|
425
|
+
sectionRef,
|
|
426
|
+
entityType,
|
|
427
|
+
entityId,
|
|
428
|
+
message:
|
|
429
|
+
`Structured-data payload @type="${entityType}" emitted by section "${sectionRef}" ` +
|
|
430
|
+
`on page "${pageRef}" does not exist in the merged catalog.`,
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (const requiredField of catalogEntry.requiredFields) {
|
|
436
|
+
if (!hasPresentFieldValue(payload, requiredField)) {
|
|
437
|
+
findings.push({
|
|
438
|
+
kind: 'spec-violation',
|
|
439
|
+
code: 'missing-required-field',
|
|
440
|
+
pageRef,
|
|
441
|
+
routePath,
|
|
442
|
+
sectionRef,
|
|
443
|
+
entityType,
|
|
444
|
+
entityId,
|
|
445
|
+
field: requiredField,
|
|
446
|
+
message:
|
|
447
|
+
`Structured-data payload @type="${entityType}" emitted by section "${sectionRef}" ` +
|
|
448
|
+
`on page "${pageRef}" is missing required field "${requiredField}".`,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
for (const deprecatedField of catalogEntry.deprecatedFields ?? []) {
|
|
454
|
+
if (!hasPresentFieldValue(payload, deprecatedField.field)) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
findings.push({
|
|
459
|
+
kind: 'deprecated-field',
|
|
460
|
+
pageRef,
|
|
461
|
+
routePath,
|
|
462
|
+
sectionRef,
|
|
463
|
+
entityType,
|
|
464
|
+
entityId,
|
|
465
|
+
field: deprecatedField.field,
|
|
466
|
+
replacedBy: deprecatedField.replacedBy,
|
|
467
|
+
deprecatedSince: deprecatedField.deprecatedSince,
|
|
468
|
+
message:
|
|
469
|
+
deprecatedField.replacedBy === null
|
|
470
|
+
? `Structured-data payload @type="${entityType}" emitted by section "${sectionRef}" ` +
|
|
471
|
+
`on page "${pageRef}" uses deprecated field "${deprecatedField.field}" ` +
|
|
472
|
+
`(deprecated since ${deprecatedField.deprecatedSince}).`
|
|
473
|
+
: `Structured-data payload @type="${entityType}" emitted by section "${sectionRef}" ` +
|
|
474
|
+
`on page "${pageRef}" uses deprecated field "${deprecatedField.field}". ` +
|
|
475
|
+
`Use "${deprecatedField.replacedBy}" instead ` +
|
|
476
|
+
`(deprecated since ${deprecatedField.deprecatedSince}).`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const referencedIds = new Set<string>();
|
|
481
|
+
collectReferencedEntityIds(payload, referencedIds, true);
|
|
482
|
+
for (const referencedId of referencedIds) {
|
|
483
|
+
if (declaredEntityIds.has(referencedId)) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
findings.push({
|
|
488
|
+
kind: 'cross-entity-reference-drift',
|
|
489
|
+
pageRef,
|
|
490
|
+
routePath,
|
|
491
|
+
sectionRef,
|
|
492
|
+
entityType,
|
|
493
|
+
entityId,
|
|
494
|
+
missingReferenceId: referencedId,
|
|
495
|
+
message:
|
|
496
|
+
`Structured-data payload @type="${entityType}" emitted by section "${sectionRef}" ` +
|
|
497
|
+
`on page "${pageRef}" references @id "${referencedId}" but no emitted entity on the page defines it.`,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function normalizeStructuredDataReturn(value: unknown): unknown[] | null {
|
|
503
|
+
if (value === null || value === undefined) {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (Array.isArray(value)) {
|
|
508
|
+
return value.filter((item) => item !== null && item !== undefined);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (isPlainObject(value)) {
|
|
512
|
+
return [value];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function hasPresentFieldValue(payload: StructuredDataPayload, fieldName: string): boolean {
|
|
519
|
+
if (!Object.prototype.hasOwnProperty.call(payload, fieldName)) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const value = payload[fieldName];
|
|
524
|
+
return value !== null && value !== undefined;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function collectDeclaredEntityIds(value: unknown, into: Set<string>): void {
|
|
528
|
+
if (Array.isArray(value)) {
|
|
529
|
+
for (const item of value) {
|
|
530
|
+
collectDeclaredEntityIds(item, into);
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!isPlainObject(value)) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const entityId = typeof value['@id'] === 'string' ? value['@id'] : null;
|
|
540
|
+
const entityType = typeof value['@type'] === 'string' ? value['@type'] : null;
|
|
541
|
+
if (entityId && entityType) {
|
|
542
|
+
into.add(entityId);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
for (const child of Object.values(value)) {
|
|
546
|
+
collectDeclaredEntityIds(child, into);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function collectReferencedEntityIds(
|
|
551
|
+
value: unknown,
|
|
552
|
+
into: Set<string>,
|
|
553
|
+
isRootPayload: boolean,
|
|
554
|
+
): void {
|
|
555
|
+
if (Array.isArray(value)) {
|
|
556
|
+
for (const item of value) {
|
|
557
|
+
collectReferencedEntityIds(item, into, false);
|
|
558
|
+
}
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (!isPlainObject(value)) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const entityId = typeof value['@id'] === 'string' ? value['@id'] : null;
|
|
567
|
+
const entityType = typeof value['@type'] === 'string' ? value['@type'] : null;
|
|
568
|
+
if (!isRootPayload && entityId && !entityType) {
|
|
569
|
+
into.add(entityId);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
for (const child of Object.values(value)) {
|
|
573
|
+
collectReferencedEntityIds(child, into, false);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
578
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
579
|
+
}
|