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