@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.
Files changed (383) hide show
  1. package/LICENSE +34 -0
  2. package/dist/esm/.builder.pid +9 -0
  3. package/dist/esm/astro/blog-post-bridge.d.ts +324 -0
  4. package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -0
  5. package/dist/esm/astro/blog-post-bridge.js +57 -0
  6. package/dist/esm/astro/blog-post-bridge.js.map +1 -0
  7. package/dist/esm/astro/blog-post-head.renderer.d.ts +129 -0
  8. package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -0
  9. package/dist/esm/astro/blog-post-head.renderer.js +139 -0
  10. package/dist/esm/astro/blog-post-head.renderer.js.map +1 -0
  11. package/dist/esm/astro/bridge-runtime.d.ts +224 -0
  12. package/dist/esm/astro/bridge-runtime.d.ts.map +1 -0
  13. package/dist/esm/astro/bridge-runtime.js +30 -0
  14. package/dist/esm/astro/bridge-runtime.js.map +1 -0
  15. package/dist/esm/astro/collect-expected-label-keys.d.ts +65 -0
  16. package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -0
  17. package/dist/esm/astro/collect-expected-label-keys.js +101 -0
  18. package/dist/esm/astro/collect-expected-label-keys.js.map +1 -0
  19. package/dist/esm/astro/headers-renderer.d.ts +32 -0
  20. package/dist/esm/astro/headers-renderer.d.ts.map +1 -0
  21. package/dist/esm/astro/headers-renderer.js +64 -0
  22. package/dist/esm/astro/headers-renderer.js.map +1 -0
  23. package/dist/esm/astro/i18n-routing.helper.d.ts +91 -0
  24. package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -0
  25. package/dist/esm/astro/i18n-routing.helper.js +23 -0
  26. package/dist/esm/astro/i18n-routing.helper.js.map +1 -0
  27. package/dist/esm/astro/internal/html-escape.d.ts +31 -0
  28. package/dist/esm/astro/internal/html-escape.d.ts.map +1 -0
  29. package/dist/esm/astro/internal/html-escape.js +40 -0
  30. package/dist/esm/astro/internal/html-escape.js.map +1 -0
  31. package/dist/esm/astro/label-pack-loader.d.ts +134 -0
  32. package/dist/esm/astro/label-pack-loader.d.ts.map +1 -0
  33. package/dist/esm/astro/label-pack-loader.js +298 -0
  34. package/dist/esm/astro/label-pack-loader.js.map +1 -0
  35. package/dist/esm/astro/llms-txt-server.d.ts +28 -0
  36. package/dist/esm/astro/llms-txt-server.d.ts.map +1 -0
  37. package/dist/esm/astro/llms-txt-server.js +33 -0
  38. package/dist/esm/astro/llms-txt-server.js.map +1 -0
  39. package/dist/esm/astro/pick-labels-for-page.d.ts +63 -0
  40. package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -0
  41. package/dist/esm/astro/pick-labels-for-page.js +73 -0
  42. package/dist/esm/astro/pick-labels-for-page.js.map +1 -0
  43. package/dist/esm/astro/robots-renderer.d.ts +85 -0
  44. package/dist/esm/astro/robots-renderer.d.ts.map +1 -0
  45. package/dist/esm/astro/robots-renderer.js +157 -0
  46. package/dist/esm/astro/robots-renderer.js.map +1 -0
  47. package/dist/esm/astro/sitemap-coverage.d.ts +101 -0
  48. package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -0
  49. package/dist/esm/astro/sitemap-coverage.js +67 -0
  50. package/dist/esm/astro/sitemap-coverage.js.map +1 -0
  51. package/dist/esm/astro/structured-data.renderer.d.ts +21 -0
  52. package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -0
  53. package/dist/esm/astro/structured-data.renderer.js +142 -0
  54. package/dist/esm/astro/structured-data.renderer.js.map +1 -0
  55. package/dist/esm/astro/website-page-head.renderer.d.ts +93 -0
  56. package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -0
  57. package/dist/esm/astro/website-page-head.renderer.js +184 -0
  58. package/dist/esm/astro/website-page-head.renderer.js.map +1 -0
  59. package/dist/esm/astro/website-page-runtime.helper.d.ts +112 -0
  60. package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -0
  61. package/dist/esm/astro/website-page-runtime.helper.js +60 -0
  62. package/dist/esm/astro/website-page-runtime.helper.js.map +1 -0
  63. package/dist/esm/astro/website-site-context.d.ts +126 -0
  64. package/dist/esm/astro/website-site-context.d.ts.map +1 -0
  65. package/dist/esm/astro/website-site-context.js +37 -0
  66. package/dist/esm/astro/website-site-context.js.map +1 -0
  67. package/dist/esm/astro-island.d.ts +56 -0
  68. package/dist/esm/astro-island.d.ts.map +1 -0
  69. package/dist/esm/astro-island.js +61 -0
  70. package/dist/esm/astro-island.js.map +1 -0
  71. package/dist/esm/astro.d.ts +151 -0
  72. package/dist/esm/astro.d.ts.map +1 -0
  73. package/dist/esm/astro.js +151 -0
  74. package/dist/esm/astro.js.map +1 -0
  75. package/dist/esm/companion-exports.d.ts +38 -0
  76. package/dist/esm/companion-exports.d.ts.map +1 -0
  77. package/dist/esm/companion-exports.js +38 -0
  78. package/dist/esm/companion-exports.js.map +1 -0
  79. package/dist/esm/components/low-level/WebsiteButton.d.ts +73 -0
  80. package/dist/esm/components/low-level/WebsiteButton.d.ts.map +1 -0
  81. package/dist/esm/components/low-level/WebsiteButton.js +68 -0
  82. package/dist/esm/components/low-level/WebsiteButton.js.map +1 -0
  83. package/dist/esm/components/low-level/WebsiteHeading.d.ts +78 -0
  84. package/dist/esm/components/low-level/WebsiteHeading.d.ts.map +1 -0
  85. package/dist/esm/components/low-level/WebsiteHeading.js +41 -0
  86. package/dist/esm/components/low-level/WebsiteHeading.js.map +1 -0
  87. package/dist/esm/components/low-level/WebsiteImage.d.ts +73 -0
  88. package/dist/esm/components/low-level/WebsiteImage.d.ts.map +1 -0
  89. package/dist/esm/components/low-level/WebsiteImage.js +21 -0
  90. package/dist/esm/components/low-level/WebsiteImage.js.map +1 -0
  91. package/dist/esm/components/low-level/WebsiteInternalButton.d.ts +90 -0
  92. package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -0
  93. package/dist/esm/components/low-level/WebsiteInternalButton.js +47 -0
  94. package/dist/esm/components/low-level/WebsiteInternalButton.js.map +1 -0
  95. package/dist/esm/components/low-level/WebsiteInternalLink.d.ts +99 -0
  96. package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -0
  97. package/dist/esm/components/low-level/WebsiteInternalLink.js +68 -0
  98. package/dist/esm/components/low-level/WebsiteInternalLink.js.map +1 -0
  99. package/dist/esm/components/low-level/WebsiteLink.d.ts +50 -0
  100. package/dist/esm/components/low-level/WebsiteLink.d.ts.map +1 -0
  101. package/dist/esm/components/low-level/WebsiteLink.js +22 -0
  102. package/dist/esm/components/low-level/WebsiteLink.js.map +1 -0
  103. package/dist/esm/components/low-level/WebsiteText.d.ts +57 -0
  104. package/dist/esm/components/low-level/WebsiteText.d.ts.map +1 -0
  105. package/dist/esm/components/low-level/WebsiteText.js +57 -0
  106. package/dist/esm/components/low-level/WebsiteText.js.map +1 -0
  107. package/dist/esm/config/define-website-config.d.ts +83 -0
  108. package/dist/esm/config/define-website-config.d.ts.map +1 -0
  109. package/dist/esm/config/define-website-config.js +126 -0
  110. package/dist/esm/config/define-website-config.js.map +1 -0
  111. package/dist/esm/config/index.d.ts +38 -0
  112. package/dist/esm/config/index.d.ts.map +1 -0
  113. package/dist/esm/config/index.js +38 -0
  114. package/dist/esm/config/index.js.map +1 -0
  115. package/dist/esm/config/load-website-config.d.ts +136 -0
  116. package/dist/esm/config/load-website-config.d.ts.map +1 -0
  117. package/dist/esm/config/load-website-config.js +177 -0
  118. package/dist/esm/config/load-website-config.js.map +1 -0
  119. package/dist/esm/config/wildo-website-config.schemas.d.ts +251 -0
  120. package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -0
  121. package/dist/esm/config/wildo-website-config.schemas.js +302 -0
  122. package/dist/esm/config/wildo-website-config.schemas.js.map +1 -0
  123. package/dist/esm/config-loader.d.ts +35 -0
  124. package/dist/esm/config-loader.d.ts.map +1 -0
  125. package/dist/esm/config-loader.js +35 -0
  126. package/dist/esm/config-loader.js.map +1 -0
  127. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +38 -0
  128. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -0
  129. package/dist/esm/core/anonymous-session/InboundContactForm.js +77 -0
  130. package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -0
  131. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +41 -0
  132. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -0
  133. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +58 -0
  134. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -0
  135. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +81 -0
  136. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -0
  137. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +177 -0
  138. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -0
  139. package/dist/esm/core/contexts/WebsitePageContext.d.ts +40 -0
  140. package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -0
  141. package/dist/esm/core/contexts/WebsitePageContext.js +8 -0
  142. package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -0
  143. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +162 -0
  144. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -0
  145. package/dist/esm/core/contexts/WebsiteRuntimeContext.js +22 -0
  146. package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -0
  147. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts +47 -0
  148. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -0
  149. package/dist/esm/core/contexts/WebsiteSectionContext.js +8 -0
  150. package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -0
  151. package/dist/esm/core/contexts/useWebsitePage.d.ts +13 -0
  152. package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -0
  153. package/dist/esm/core/contexts/useWebsitePage.js +22 -0
  154. package/dist/esm/core/contexts/useWebsitePage.js.map +1 -0
  155. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +19 -0
  156. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -0
  157. package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -0
  158. package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -0
  159. package/dist/esm/core/contexts/useWebsiteSection.d.ts +12 -0
  160. package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -0
  161. package/dist/esm/core/contexts/useWebsiteSection.js +22 -0
  162. package/dist/esm/core/contexts/useWebsiteSection.js.map +1 -0
  163. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +17 -0
  164. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -0
  165. package/dist/esm/core/external-providers/frontend-provider-registry.website.js +23 -0
  166. package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -0
  167. package/dist/esm/core/factories/define-website-page-manifest.d.ts +70 -0
  168. package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -0
  169. package/dist/esm/core/factories/define-website-page-manifest.js +51 -0
  170. package/dist/esm/core/factories/define-website-page-manifest.js.map +1 -0
  171. package/dist/esm/core/factories/define-website-section.d.ts +101 -0
  172. package/dist/esm/core/factories/define-website-section.d.ts.map +1 -0
  173. package/dist/esm/core/factories/define-website-section.js +71 -0
  174. package/dist/esm/core/factories/define-website-section.js.map +1 -0
  175. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts +23 -0
  176. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -0
  177. package/dist/esm/core/hooks/useWebsiteDesignTokens.js +25 -0
  178. package/dist/esm/core/hooks/useWebsiteDesignTokens.js.map +1 -0
  179. package/dist/esm/core/hooks/useWebsiteLabel.d.ts +68 -0
  180. package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -0
  181. package/dist/esm/core/hooks/useWebsiteLabel.js +103 -0
  182. package/dist/esm/core/hooks/useWebsiteLabel.js.map +1 -0
  183. package/dist/esm/core/layouts/WebsitePageLayout.d.ts +87 -0
  184. package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -0
  185. package/dist/esm/core/layouts/WebsitePageLayout.js +170 -0
  186. package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -0
  187. package/dist/esm/core/layouts/WebsiteSection.d.ts +91 -0
  188. package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -0
  189. package/dist/esm/core/layouts/WebsiteSection.js +26 -0
  190. package/dist/esm/core/layouts/WebsiteSection.js.map +1 -0
  191. package/dist/esm/core/routing/internal-routing.utils.d.ts +65 -0
  192. package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -0
  193. package/dist/esm/core/routing/internal-routing.utils.js +14 -0
  194. package/dist/esm/core/routing/internal-routing.utils.js.map +1 -0
  195. package/dist/esm/index.d.ts +129 -0
  196. package/dist/esm/index.d.ts.map +1 -0
  197. package/dist/esm/index.js +129 -0
  198. package/dist/esm/index.js.map +1 -0
  199. package/dist/esm/mdx/BlogPost.d.ts +193 -0
  200. package/dist/esm/mdx/BlogPost.d.ts.map +1 -0
  201. package/dist/esm/mdx/BlogPost.js +161 -0
  202. package/dist/esm/mdx/BlogPost.js.map +1 -0
  203. package/dist/esm/mdx/blog-post-collection.config.d.ts +139 -0
  204. package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -0
  205. package/dist/esm/mdx/blog-post-collection.config.js +27 -0
  206. package/dist/esm/mdx/blog-post-collection.config.js.map +1 -0
  207. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts +12 -0
  208. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -0
  209. package/dist/esm/mdx/blog-post-frontmatter-source.js +13 -0
  210. package/dist/esm/mdx/blog-post-frontmatter-source.js.map +1 -0
  211. package/dist/esm/mdx/index.d.ts +22 -0
  212. package/dist/esm/mdx/index.d.ts.map +1 -0
  213. package/dist/esm/mdx/index.js +22 -0
  214. package/dist/esm/mdx/index.js.map +1 -0
  215. package/dist/esm/mdx/website-mdx-provider.d.ts +90 -0
  216. package/dist/esm/mdx/website-mdx-provider.d.ts.map +1 -0
  217. package/dist/esm/mdx/website-mdx-provider.js +8 -0
  218. package/dist/esm/mdx/website-mdx-provider.js.map +1 -0
  219. package/dist/esm/mdx.d.ts +38 -0
  220. package/dist/esm/mdx.d.ts.map +1 -0
  221. package/dist/esm/mdx.js +38 -0
  222. package/dist/esm/mdx.js.map +1 -0
  223. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts +75 -0
  224. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -0
  225. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js +85 -0
  226. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js.map +1 -0
  227. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +120 -0
  228. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -0
  229. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +152 -0
  230. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -0
  231. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts +73 -0
  232. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -0
  233. package/dist/esm/schemas/label-keys/website-label-key.schemas.js +76 -0
  234. package/dist/esm/schemas/label-keys/website-label-key.schemas.js.map +1 -0
  235. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts +209 -0
  236. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -0
  237. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js +218 -0
  238. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js.map +1 -0
  239. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +431 -0
  240. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -0
  241. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +438 -0
  242. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -0
  243. package/dist/esm/schemas/refs/page-ref.schemas.d.ts +40 -0
  244. package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -0
  245. package/dist/esm/schemas/refs/page-ref.schemas.js +43 -0
  246. package/dist/esm/schemas/refs/page-ref.schemas.js.map +1 -0
  247. package/dist/esm/schemas/refs/section-ref.schemas.d.ts +41 -0
  248. package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -0
  249. package/dist/esm/schemas/refs/section-ref.schemas.js +44 -0
  250. package/dist/esm/schemas/refs/section-ref.schemas.js.map +1 -0
  251. package/dist/esm/schemas/sections/website-section-category.shared.d.ts +63 -0
  252. package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -0
  253. package/dist/esm/schemas/sections/website-section-category.shared.js +64 -0
  254. package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -0
  255. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +114 -0
  256. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -0
  257. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +69 -0
  258. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -0
  259. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +100 -0
  260. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -0
  261. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +355 -0
  262. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -0
  263. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +58 -0
  264. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -0
  265. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js +2 -0
  266. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -0
  267. package/dist/esm/schemas/validators/label-pack.validator.d.ts +144 -0
  268. package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -0
  269. package/dist/esm/schemas/validators/label-pack.validator.js +100 -0
  270. package/dist/esm/schemas/validators/label-pack.validator.js.map +1 -0
  271. package/dist/tsconfig.build.tsbuildinfo +1 -0
  272. package/package.json +143 -0
  273. package/src/__tests__/bundle-isolation.test.ts +607 -0
  274. package/src/astro/__tests__/blog-post-bridge.test.tsx +288 -0
  275. package/src/astro/__tests__/blog-post-head.renderer.test.ts +249 -0
  276. package/src/astro/__tests__/bridge-runtime.test.tsx +198 -0
  277. package/src/astro/__tests__/collect-expected-label-keys.test.ts +69 -0
  278. package/src/astro/__tests__/headers-renderer.test.ts +143 -0
  279. package/src/astro/__tests__/i18n-routing.helper.test.ts +116 -0
  280. package/src/astro/__tests__/label-pack-loader.test.ts +305 -0
  281. package/src/astro/__tests__/llms-txt-server.test.ts +127 -0
  282. package/src/astro/__tests__/pick-labels-for-page.test.ts +60 -0
  283. package/src/astro/__tests__/robots-renderer.test.ts +228 -0
  284. package/src/astro/__tests__/sitemap-coverage.test.ts +230 -0
  285. package/src/astro/__tests__/structured-data.renderer.test.ts +172 -0
  286. package/src/astro/__tests__/website-page-head.renderer.test.ts +424 -0
  287. package/src/astro/__tests__/website-page-runtime.helper.test.ts +157 -0
  288. package/src/astro/__tests__/website-site-context.test.ts +121 -0
  289. package/src/astro/blog-post-bridge.tsx +404 -0
  290. package/src/astro/blog-post-head.renderer.ts +293 -0
  291. package/src/astro/bridge-runtime.tsx +270 -0
  292. package/src/astro/collect-expected-label-keys.ts +113 -0
  293. package/src/astro/headers-renderer.ts +82 -0
  294. package/src/astro/i18n-routing.helper.ts +116 -0
  295. package/src/astro/internal/html-escape.ts +41 -0
  296. package/src/astro/label-pack-loader.ts +385 -0
  297. package/src/astro/llms-txt-server.ts +56 -0
  298. package/src/astro/pick-labels-for-page.ts +85 -0
  299. package/src/astro/robots-renderer.ts +171 -0
  300. package/src/astro/sitemap-coverage.ts +186 -0
  301. package/src/astro/structured-data.renderer.ts +171 -0
  302. package/src/astro/website-page-head.renderer.ts +279 -0
  303. package/src/astro/website-page-runtime.helper.ts +188 -0
  304. package/src/astro/website-site-context.ts +136 -0
  305. package/src/astro-island.ts +61 -0
  306. package/src/astro.ts +151 -0
  307. package/src/companion-exports.ts +38 -0
  308. package/src/components/low-level/WebsiteButton.tsx +118 -0
  309. package/src/components/low-level/WebsiteHeading.tsx +96 -0
  310. package/src/components/low-level/WebsiteImage.tsx +96 -0
  311. package/src/components/low-level/WebsiteInternalButton.tsx +172 -0
  312. package/src/components/low-level/WebsiteInternalLink.tsx +193 -0
  313. package/src/components/low-level/WebsiteLink.tsx +71 -0
  314. package/src/components/low-level/WebsiteText.tsx +75 -0
  315. package/src/components/low-level/__tests__/WebsiteInternalButton.test.tsx +169 -0
  316. package/src/components/low-level/__tests__/WebsiteInternalLink.test.tsx +202 -0
  317. package/src/components/low-level/__tests__/primitives.test.tsx +141 -0
  318. package/src/config/__tests__/define-website-config.test.ts +231 -0
  319. package/src/config/__tests__/load-website-config.test.ts +215 -0
  320. package/src/config/define-website-config.ts +134 -0
  321. package/src/config/index.ts +38 -0
  322. package/src/config/load-website-config.ts +229 -0
  323. package/src/config/wildo-website-config.schemas.ts +322 -0
  324. package/src/config-loader.ts +35 -0
  325. package/src/core/__tests__/WebsitePageLayout.test.tsx +184 -0
  326. package/src/core/__tests__/WebsiteSection.test.tsx +85 -0
  327. package/src/core/__tests__/contexts.test.tsx +133 -0
  328. package/src/core/__tests__/core-boundary.test.ts +85 -0
  329. package/src/core/__tests__/define-website-page-manifest.test.tsx +111 -0
  330. package/src/core/__tests__/define-website-section.test.tsx +89 -0
  331. package/src/core/__tests__/useWebsiteDesignTokens.test.tsx +41 -0
  332. package/src/core/__tests__/useWebsiteLabel.test.tsx +117 -0
  333. package/src/core/anonymous-session/InboundContactForm.tsx +157 -0
  334. package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +44 -0
  335. package/src/core/anonymous-session/inbound-contact-form.schema.ts +62 -0
  336. package/src/core/anonymous-session/website-anonymous-session-client.ts +194 -0
  337. package/src/core/contexts/WebsitePageContext.tsx +52 -0
  338. package/src/core/contexts/WebsiteRuntimeContext.tsx +175 -0
  339. package/src/core/contexts/WebsiteSectionContext.tsx +59 -0
  340. package/src/core/contexts/useWebsitePage.ts +28 -0
  341. package/src/core/contexts/useWebsiteRuntime.ts +34 -0
  342. package/src/core/contexts/useWebsiteSection.ts +28 -0
  343. package/src/core/external-providers/frontend-provider-registry.website.ts +62 -0
  344. package/src/core/factories/define-website-page-manifest.ts +102 -0
  345. package/src/core/factories/define-website-section.ts +128 -0
  346. package/src/core/hooks/useWebsiteDesignTokens.ts +26 -0
  347. package/src/core/hooks/useWebsiteLabel.ts +121 -0
  348. package/src/core/layouts/WebsitePageLayout.tsx +311 -0
  349. package/src/core/layouts/WebsiteSection.tsx +139 -0
  350. package/src/core/routing/__tests__/internal-routing.utils.test.ts +134 -0
  351. package/src/core/routing/internal-routing.utils.ts +77 -0
  352. package/src/index.ts +132 -0
  353. package/src/mdx/BlogPost.tsx +296 -0
  354. package/src/mdx/__tests__/BlogPost.test.tsx +218 -0
  355. package/src/mdx/__tests__/blog-post-collection.config.test.ts +127 -0
  356. package/src/mdx/__tests__/website-mdx-provider.test.tsx +82 -0
  357. package/src/mdx/blog-post-collection.config.ts +177 -0
  358. package/src/mdx/blog-post-frontmatter-source.ts +31 -0
  359. package/src/mdx/index.ts +39 -0
  360. package/src/mdx/website-mdx-provider.tsx +95 -0
  361. package/src/mdx.ts +38 -0
  362. package/src/schemas/__tests__/blog-frontmatter.test.ts +50 -0
  363. package/src/schemas/__tests__/companion-exports-react-free.test.ts +51 -0
  364. package/src/schemas/__tests__/design-tokens.test.ts +163 -0
  365. package/src/schemas/__tests__/label-key.test.ts +46 -0
  366. package/src/schemas/__tests__/label-pack-validator.test.ts +146 -0
  367. package/src/schemas/__tests__/page-manifest.test.ts +66 -0
  368. package/src/schemas/__tests__/refs.test.ts +37 -0
  369. package/src/schemas/__tests__/root-config.test.ts +292 -0
  370. package/src/schemas/__tests__/section-definition.test.ts +47 -0
  371. package/src/schemas/__tests__/structured-data-reconciliation.test.ts +543 -0
  372. package/src/schemas/blog/blog-post-frontmatter.shared.schemas.ts +111 -0
  373. package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +166 -0
  374. package/src/schemas/label-keys/website-label-key.schemas.ts +88 -0
  375. package/src/schemas/manifests/website-page-manifest.shared.schemas.ts +266 -0
  376. package/src/schemas/manifests/website-root-config.shared.schemas.ts +504 -0
  377. package/src/schemas/refs/page-ref.schemas.ts +49 -0
  378. package/src/schemas/refs/section-ref.schemas.ts +50 -0
  379. package/src/schemas/sections/website-section-category.shared.ts +65 -0
  380. package/src/schemas/sections/website-section-definition.shared.schemas.ts +120 -0
  381. package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +579 -0
  382. package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +62 -0
  383. 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
+ }