@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,134 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
4
+
5
+ import { buildLocalizedRoutePath } from '../internal-routing.utils';
6
+
7
+ describe('buildLocalizedRoutePath', () => {
8
+ describe('default locale (un-prefixed routing)', () => {
9
+ it('returns the route verbatim for the default locale', () => {
10
+ expect(
11
+ buildLocalizedRoutePath({
12
+ locale: AvailableLanguage.EN,
13
+ defaultLocale: AvailableLanguage.EN,
14
+ routePath: '/pricing',
15
+ }),
16
+ ).toBe('/pricing');
17
+ });
18
+
19
+ it('returns "/" verbatim for the default-locale root', () => {
20
+ expect(
21
+ buildLocalizedRoutePath({
22
+ locale: AvailableLanguage.EN,
23
+ defaultLocale: AvailableLanguage.EN,
24
+ routePath: '/',
25
+ }),
26
+ ).toBe('/');
27
+ });
28
+ });
29
+
30
+ describe('non-default locale (prefixed routing)', () => {
31
+ it('prefixes a typical route with /<locale>', () => {
32
+ expect(
33
+ buildLocalizedRoutePath({
34
+ locale: AvailableLanguage.FR,
35
+ defaultLocale: AvailableLanguage.EN,
36
+ routePath: '/pricing',
37
+ }),
38
+ ).toBe('/fr/pricing');
39
+ });
40
+
41
+ it('collapses the root path to /<locale> (NOT /<locale>/)', () => {
42
+ expect(
43
+ buildLocalizedRoutePath({
44
+ locale: AvailableLanguage.FR,
45
+ defaultLocale: AvailableLanguage.EN,
46
+ routePath: '/',
47
+ }),
48
+ ).toBe('/fr');
49
+ });
50
+
51
+ it('handles deeper routes', () => {
52
+ expect(
53
+ buildLocalizedRoutePath({
54
+ locale: AvailableLanguage.FR,
55
+ defaultLocale: AvailableLanguage.EN,
56
+ routePath: '/blog/launch',
57
+ }),
58
+ ).toBe('/fr/blog/launch');
59
+ });
60
+ });
61
+
62
+ /**
63
+ * Phase 6 fourth audit fix M-Κ — coverage gap follow-ups for the
64
+ * three "tricky path shape" cases the original test grid skipped.
65
+ * `buildLocalizedRoutePath` is a low-level prefixer that does NOT
66
+ * try to interpret query strings, hash fragments, or already-
67
+ * prefixed paths — those are the responsibility of the calling
68
+ * primitive (`WebsiteInternalLink` / `WebsiteInternalButton`,
69
+ * which both throw on already-prefixed paths). These tests pin
70
+ * the helper's literal behavior so a future "be smart" refactor
71
+ * cannot silently change the contract.
72
+ */
73
+ describe('path shape edge cases (M-Κ — pin literal behavior)', () => {
74
+ it('preserves a query string by suffix-pasting (no parsing)', () => {
75
+ expect(
76
+ buildLocalizedRoutePath({
77
+ locale: AvailableLanguage.FR,
78
+ defaultLocale: AvailableLanguage.EN,
79
+ routePath: '/pricing?plan=pro',
80
+ }),
81
+ ).toBe('/fr/pricing?plan=pro');
82
+ });
83
+
84
+ it('preserves a hash fragment by suffix-pasting (no parsing)', () => {
85
+ expect(
86
+ buildLocalizedRoutePath({
87
+ locale: AvailableLanguage.FR,
88
+ defaultLocale: AvailableLanguage.EN,
89
+ routePath: '/pricing#tiers',
90
+ }),
91
+ ).toBe('/fr/pricing#tiers');
92
+ });
93
+
94
+ it('preserves both query and fragment in order', () => {
95
+ expect(
96
+ buildLocalizedRoutePath({
97
+ locale: AvailableLanguage.FR,
98
+ defaultLocale: AvailableLanguage.EN,
99
+ routePath: '/pricing?plan=pro#tiers',
100
+ }),
101
+ ).toBe('/fr/pricing?plan=pro#tiers');
102
+ });
103
+
104
+ it('DOES NOT detect already-prefixed paths — produces a double-prefix on FR pages', () => {
105
+ // Documents the helper's deliberate dumb-prefix contract: the
106
+ // caller is responsible for guarding against accidental locale
107
+ // prefixes. `WebsiteInternalLink` / `WebsiteInternalButton`
108
+ // both throw before reaching this helper when the routePath
109
+ // begins with a known locale segment (see their
110
+ // `detectAccidentalLocalePrefix` guard).
111
+ expect(
112
+ buildLocalizedRoutePath({
113
+ locale: AvailableLanguage.FR,
114
+ defaultLocale: AvailableLanguage.EN,
115
+ routePath: '/fr/pricing',
116
+ }),
117
+ ).toBe('/fr/fr/pricing');
118
+ });
119
+
120
+ it('DOES NOT detect already-prefixed paths — silently passes on default-locale pages', () => {
121
+ // The other half of the silent-bug surface: on a default-locale
122
+ // page (no prefix added), an already-prefixed path renders
123
+ // unchanged, so the EN visitor lands on the FR page with no
124
+ // visible warning. Same caller-responsibility contract.
125
+ expect(
126
+ buildLocalizedRoutePath({
127
+ locale: AvailableLanguage.EN,
128
+ defaultLocale: AvailableLanguage.EN,
129
+ routePath: '/fr/pricing',
130
+ }),
131
+ ).toBe('/fr/pricing');
132
+ });
133
+ });
134
+ });
@@ -0,0 +1,77 @@
1
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
+
3
+ /**
4
+ * @wildo_source:part:start saas.website.routing.localized-path facet:layer:core facet:family:website
5
+ *
6
+ * `buildLocalizedRoutePath(opts)` — pure, isomorphic helper that
7
+ * returns the locale-prefixed pathname for a given route, honoring the
8
+ * marketing-site routing convention enforced by `buildAstroI18nConfig`
9
+ * (`prefixDefaultLocale: false`).
10
+ *
11
+ * **The architectural problem this solves**: the marketing site mounts the
12
+ * default locale at the
13
+ * un-prefixed path (`/`, `/pricing`, `/contact`) and every other locale
14
+ * under `/<locale>/…` (`/fr/`, `/fr/pricing`, `/fr/contact`). Internal
15
+ * links authored as raw `href="/pricing"` therefore work on
16
+ * default-locale pages and SILENTLY BREAK on non-default-locale pages
17
+ * (a French visitor on `/fr/index` clicking "Pricing" lands on the
18
+ * English `/pricing`, falling out of their locale).
19
+ *
20
+ * **Why a helper rather than asking authors to construct the path**:
21
+ * the rule is a framework-level routing convention — every consumer
22
+ * authors links the same way, every consumer must apply the same
23
+ * prefix logic, and getting it wrong produces a silent locale-leak
24
+ * that test suites do not catch (they navigate inside a single
25
+ * locale). A single helper makes the rule the framework's
26
+ * responsibility, not the section author's.
27
+ *
28
+ * **Routing rule** (matches `buildAstroI18nConfig` and the head
29
+ * renderer's `buildAbsoluteUrl` helper, which delegates here as the
30
+ * single source of truth):
31
+ * - When `locale === defaultLocale`: return `routePath` verbatim
32
+ * (the un-prefixed path).
33
+ * - Otherwise: return `/<locale>${routePath}`, with the special
34
+ * case that `routePath === '/'` collapses to `/<locale>` (no
35
+ * trailing slash) so the locale-prefixed root does not become
36
+ * the awkward `/<locale>/`.
37
+ *
38
+ * **Inputs are validated by the calling layer**, not here:
39
+ * - `locale` is `AvailableLanguage` (branded), so the type system
40
+ * prevents an unknown locale from reaching this function.
41
+ * - `routePath` is the same `routePath` field validated by the page
42
+ * manifest schema (`WebsitePageRoutePathSchema` enforces it
43
+ * starts with `/`). We do not re-validate here — duplicate
44
+ * validation costs CPU on every link render with no incremental
45
+ * safety, and the page-manifest layer fails loud at config-load
46
+ * time if a malformed path leaks in.
47
+ *
48
+ * **Boundary**: pure function, zero React / DOM / fs dependencies.
49
+ * Safe to import from both the React-island bundle (consumed by
50
+ * `WebsiteInternalLink`) and the SSR head renderer (consumed by
51
+ * `buildAbsoluteUrl`). The package's routing tests pin this behavior.
52
+ */
53
+ export interface BuildLocalizedRoutePathOptions {
54
+ /** The currently-active locale (typically from `useWebsiteRuntime().locale`). */
55
+ locale: AvailableLanguage;
56
+ /** The site's default locale (typically from `useWebsiteRuntime().defaultLocale`). */
57
+ defaultLocale: AvailableLanguage;
58
+ /**
59
+ * The page's route path, exactly as declared in
60
+ * `WebsitePageManifest.routePath` (always starts with `/`).
61
+ */
62
+ routePath: string;
63
+ }
64
+
65
+ export function buildLocalizedRoutePath(opts: BuildLocalizedRoutePathOptions): string {
66
+ const { locale, defaultLocale, routePath } = opts;
67
+ const localeStr = String(locale);
68
+ const defaultLocaleStr = String(defaultLocale);
69
+ if (localeStr === defaultLocaleStr) {
70
+ return routePath;
71
+ }
72
+ if (routePath === '/') {
73
+ return `/${localeStr}`;
74
+ }
75
+ return `/${localeStr}${routePath}`;
76
+ }
77
+ /** @wildo_source:part:end saas.website.routing.localized-path */
package/src/index.ts ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @wildo-package @wildo-ai/saas-website
3
+ *
4
+ * SaaS marketing/landing website engine — schemas + helpers + layout
5
+ * placeholders + Astro/MDX adapters + SEO/AEO/GEO builder library.
6
+ *
7
+ * Per `saas-website-slice.md` D6/D15: this engine ships **NO concrete
8
+ * business sections** (no Hero, no CTA, no Pricing). It ships the
9
+ * structural and helper machinery; consumers author every business
10
+ * section themselves via the `defineWebsiteSection` factory and the
11
+ * accompanying SKILLs in `framework-knowledge`.
12
+ *
13
+ * @wildo-boundary
14
+ * - This barrel MAY re-export from any subdirectory of `src/`.
15
+ * - Subdirectories MUST observe their own boundary contracts (see
16
+ * each subdirectory's own `index.ts` `@wildo-boundary` block):
17
+ * - `schemas/` : NO React, NO Astro, NO `~wildo-frontend-lib/**`
18
+ * - `seo/` : NO React, NO Astro
19
+ * - `core/` : MAY use React; NO `~wildo-frontend-lib/**` (Q17=B)
20
+ * - `astro/` : MAY use Astro; NO React component runtime imports
21
+ * - `mdx/` : MAY use `@mdx-js/*`
22
+ * - `integration/` : MAY use React; MAY import the
23
+ * `@wildo-ai/saas-models/public-runtime` SUBPATH,
24
+ * the `@wildo-ai/presets-components-models/public-runtime`
25
+ * SUBPATH, `@wildo-ai/zod-decorators`,
26
+ * plus the SSR / Astro-middleware tier
27
+ * (`axios`, `jsonwebtoken`, `socket.io-client`,
28
+ * `reflect-metadata`) — D12 revised
29
+ * 2026-04-19 (second same-day loosening);
30
+ * the ROOT barrels of `@wildo-ai/saas-models`
31
+ * and `@wildo-ai/presets-components-models`
32
+ * are FORBIDDEN (they boot the decorator
33
+ * framework / pull the heavy registry);
34
+ * the saas-app DI / backend stack
35
+ * (`inversify`, `mongoose`, `express`,
36
+ * `pino`, `helmet`, `argon2`, `bcrypt`,
37
+ * the `@wildo-ai/saas-frontend-lib` /
38
+ * `@wildo-ai/saas-backend-lib` /
39
+ * `@wildo-ai/external-connectors-models`
40
+ * root + subpath surfaces) is FORBIDDEN;
41
+ * MUST NOT import `~wildo-frontend-lib/**`
42
+ * (Q17=B / D16)
43
+ *
44
+ * D12 (client-bundle import discipline, all `core/`/`schemas/`/`integration/`
45
+ * files): the entire saas-website source tree ships into a browser bundle
46
+ * (Astro SSG + React islands). Every shared model atom MUST come from a
47
+ * side-effect-free `/public-runtime` subpath:
48
+ * - `@wildo-ai/saas-models/public-runtime` is the primary door
49
+ * (saas-models-public-runtime.md K-6 — consumers see one path).
50
+ * - `@wildo-ai/presets-components-models/public-runtime` is the
51
+ * internal seam for symbols not (yet) re-exported by saas-models.
52
+ * - `@wildo-ai/zod-decorators` is allowed to import (first revision
53
+ * 2026-04-19). The package itself is side-effect-free; only an
54
+ * explicit `initZodDecorators()` call mutates `z.ZodType.prototype`,
55
+ * which neither saas-website nor the saas-models /public-runtime
56
+ * barrel does.
57
+ * - `axios`, `jsonwebtoken`, `socket.io-client`, `reflect-metadata` are
58
+ * allowed for SSR / Astro-middleware code paths (second same-day
59
+ * revision 2026-04-19). The website engine needs them to provide
60
+ * middleware utilities (HTTP proxying to the SaaS backend, JWT
61
+ * verification of cross-frontend handoff codes, real-time push,
62
+ * decorator-metadata reads). They are SSR-tier — discipline
63
+ * (and a future strict/loose entry split for React islands vs
64
+ * middleware) keeps them out of the React-island bundle.
65
+ *
66
+ * The ROOT barrel of `@wildo-ai/saas-models` is forbidden everywhere in
67
+ * this package — it calls `initZodDecorators()` at module load and pulls
68
+ * `@wildo-ai/external-connectors-models` + the full schema graph into
69
+ * the bundle. Same forbid applies to the root barrel of
70
+ * `@wildo-ai/presets-components-models`, the saas-app DI runtime
71
+ * (`inversify`, `@wildo-ai/saas-frontend-lib`, `@wildo-ai/saas-backend-lib`),
72
+ * and pure-backend libs (`mongoose`, `express`, `pino`, `helmet`,
73
+ * `argon2`, `bcrypt`). Enforced by `src/__tests__/bundle-isolation.test.ts`.
74
+ *
75
+ * These boundaries are enforced by review + the bundle-isolation test
76
+ * above until Phase 8 promotes them to a programmatic ESLint check
77
+ * (cf. `saas-website-slice.md`
78
+ * §"DEFERRED to Phase 8 — boundary lint mechanism design").
79
+ *
80
+ * Phase 1+2 status: schemas + runtime contexts/hooks + layout
81
+ * primitives + L1 placeholders + factories are all exported. Phase 5
82
+ * adds the Astro adapter; Phase 5.7 the `integration/` layer.
83
+ *
84
+ * D18 (L1 boundary, Phase 2): the L1 primitives (`WebsiteHeading`,
85
+ * `WebsiteText`, `WebsiteButton`, `WebsiteLink`, `WebsiteImage`) are
86
+ * self-contained placeholders — they do NOT consume
87
+ * `ComponentRegistryService` from `saas-frontend-lib` and do NOT
88
+ * re-export `saas-frontend-lib` primitives. Consumers override at the
89
+ * L1 boundary by barrel-re-exporting their preferred styled variant
90
+ * from their app's `app/website/primitives/<Primitive>.tsx`. Phase 6
91
+ * dogfood will revisit and decide whether to promote a registry-based
92
+ * override mechanism.
93
+ */
94
+
95
+ export * from './schemas/refs/page-ref.schemas';
96
+ export * from './schemas/refs/section-ref.schemas';
97
+ export * from './schemas/label-keys/website-label-key.schemas';
98
+ export * from './schemas/sections/website-section-category.shared';
99
+ export * from './schemas/sections/website-section-definition.shared.schemas';
100
+ export * from './schemas/structured-data/website-structured-data.shared.schemas';
101
+ export * from './schemas/structured-data/structured-data-reconciliation.shared';
102
+ export * from './schemas/design-tokens/website-design-tokens.shared.schemas';
103
+ export * from './schemas/manifests/website-page-manifest.shared.schemas';
104
+ export * from './schemas/manifests/website-root-config.shared.schemas';
105
+ export * from './schemas/blog/blog-post-frontmatter.shared.schemas';
106
+ export * from './schemas/validators/label-pack.validator';
107
+
108
+ export * from './core/contexts/WebsiteRuntimeContext';
109
+ export * from './core/contexts/useWebsiteRuntime';
110
+ export * from './core/contexts/WebsitePageContext';
111
+ export * from './core/contexts/useWebsitePage';
112
+ export * from './core/contexts/WebsiteSectionContext';
113
+ export * from './core/contexts/useWebsiteSection';
114
+ export * from './core/hooks/useWebsiteLabel';
115
+ export * from './core/hooks/useWebsiteDesignTokens';
116
+ export * from './core/factories/define-website-section';
117
+ export * from './core/factories/define-website-page-manifest';
118
+ export * from './core/layouts/WebsitePageLayout';
119
+ export * from './core/layouts/WebsiteSection';
120
+
121
+ export * from './core/anonymous-session/website-anonymous-session-client';
122
+ export * from './core/anonymous-session/inbound-contact-form.schema';
123
+ export * from './core/anonymous-session/InboundContactForm';
124
+
125
+ export * from './components/low-level/WebsiteHeading';
126
+ export * from './components/low-level/WebsiteText';
127
+ export * from './components/low-level/WebsiteLink';
128
+ export * from './components/low-level/WebsiteInternalLink';
129
+ export * from './core/routing/internal-routing.utils';
130
+ export * from './components/low-level/WebsiteButton';
131
+ export * from './components/low-level/WebsiteInternalButton';
132
+ export * from './components/low-level/WebsiteImage';
@@ -0,0 +1,296 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+
3
+ import { WebsitePageLayout } from '../core/layouts/WebsitePageLayout';
4
+ import type { WebsitePageRef } from '../schemas/refs/page-ref.schemas';
5
+
6
+ import {
7
+ parseBlogPostFrontmatter,
8
+ type ParseBlogPostFrontmatterContext,
9
+ } from './blog-post-collection.config';
10
+ import {
11
+ WebsiteMdxProvider,
12
+ type WebsiteMdxComponentsMap,
13
+ } from './website-mdx-provider';
14
+
15
+ /**
16
+ * @wildo_source:part:start saas.website.blog.post-renderer facet:layer:mdx facet:family:website
17
+ *
18
+ * `<BlogPost>` — engine-owned per-post page renderer for the marketing
19
+ * blog adapter.
20
+ *
21
+ * # What it does, end-to-end
22
+ *
23
+ * 1. **Strict frontmatter parse.** Reads the raw `entry.data` Astro
24
+ * hands the page (passthrough at the content-collection layer per
25
+ * Path Z, see `blog-post-collection.config.ts`) and runs it
26
+ * through `BlogPostFrontmatterSchema` (Zod 4) — branded `slug` /
27
+ * `authorRef` / `tags`, ISO-8601 `publishedAt`, optional
28
+ * `coverImageUrl`. A failure throws synchronously with a
29
+ * `[blog-post] (source="…")` message; Astro halts the build and
30
+ * points the operator to the offending MDX file.
31
+ * 2. **Renders `<WebsitePageLayout>` with a stable blog-post page
32
+ * ref.** The framework reserves `'blog-post'` as the
33
+ * `WebsitePageRef` for every blog post (see `BLOG_POST_PAGE_REF`
34
+ * below for the rationale). All blog posts share one page ref
35
+ * because the SECTION ref under that page is what's
36
+ * blog-author-controlled (see point 3) — the page-level chrome,
37
+ * label scope, and analytics signal are the same across the entire
38
+ * blog surface.
39
+ * 3. **Wraps the MDX body in `<WebsiteMdxProvider>`.** The consumer
40
+ * passes a `mdxComponents` map (typically containing one or two
41
+ * business-section refs they want available inside post bodies —
42
+ * `<CTASection ref="post-cta" />`, etc.). The provider scopes the
43
+ * map for the duration of the render and ONLY the duration of the
44
+ * render, so two blog posts on the same site can ship different
45
+ * provider configs without leaking.
46
+ * 4. **Emits a hero block (title / summary / publish date / author
47
+ * handle / cover image)** from the parsed frontmatter, then yields
48
+ * to the MDX body. Hero markup is intentionally minimal — the
49
+ * consumer styles it via `WebsiteDesignTokens` custom properties.
50
+ * No CSS file ships from this component; everything is inline
51
+ * style + design-token references.
52
+ *
53
+ * # Why a blog-specific page renderer (vs. the generic page bridge)
54
+ *
55
+ * The `createWebsitePageBridgeIsland` factory takes a `PageComponent`
56
+ * — a React component the page author writes. A blog post has NO React
57
+ * component; the MDX file IS the component (compiled by `@astrojs/mdx`
58
+ * to a default-export React tree). Reusing the page-bridge factory
59
+ * would force the consumer to write a ten-line wrapper per post that
60
+ * imports the MDX module, calls `<MdxBody />`, and re-passes the
61
+ * frontmatter — a cold violation of the "every blog post is one MDX
62
+ * file, period" principle. `<BlogPost>` is the framework's
63
+ * single ten-line wrapper, written once.
64
+ *
65
+ * # Why no per-post `pageRef` / per-section `sectionRef` knobs
66
+ *
67
+ * Two reasons:
68
+ *
69
+ * - **Label-key scope semantics.** A blog post's body has no
70
+ * framework-routed labels (the prose IS in the MDX). The section
71
+ * refs that DO need labels are those an MDX file embeds via the
72
+ * `mdxComponents` map — and those sections were already registered
73
+ * in the consumer's `WEBSITE_SECTIONS` registry with their
74
+ * own `expectedLabelKeys`. Letting blog authors pass arbitrary
75
+ * `sectionRef`s would create unbounded label-key surface area that
76
+ * `assertLabelPacksValid` cannot validate at build start.
77
+ * - **Analytics + chrome consistency.** Marketing teams treat "the
78
+ * blog" as one funnel. A single `pageRef='blog-post'` keeps the
79
+ * `data-website-page-ref` attribute on the layout root constant
80
+ * across every post, so the analytics ingest sees one blog-post
81
+ * page event regardless of slug. Per-post overrides would fragment
82
+ * this without buying any operator value.
83
+ *
84
+ * # Hydration + bundle boundary
85
+ *
86
+ * This component is NOT itself a Bridge factory — it's plain React
87
+ * meant to be rendered inside an Astro `.astro` page. The consumer's
88
+ * per-post Astro template (typically `src/pages/[locale]/blog/[slug].astro`)
89
+ * imports `<BlogPost>` directly and renders it; the surrounding
90
+ * `<WebsitePageBridgeIsland>` provides the runtime context. The MDX
91
+ * runtime (`@mdx-js/react`) is React-only and pulls into the React
92
+ * island bundle — bundle-isolation tests walk `mdx.ts` as their own
93
+ * entry to keep the browser-facing closure auditable.
94
+ *
95
+ * Authoring example:
96
+ *
97
+ * ```astro
98
+ * ---
99
+ * // src/pages/blog/[slug].astro
100
+ * import { getCollection, getEntry } from 'astro:content';
101
+ * import { BlogPost } from '@wildo-ai/saas-website/mdx';
102
+ * import { BLOG_POST_MDX_COMPONENTS } from '@/blog/mdx-components';
103
+ *
104
+ * export async function getStaticPaths() {
105
+ * const posts = await getCollection('blog');
106
+ * return posts
107
+ * .filter((p) => p.data.locale === 'en')
108
+ * .map((p) => ({ params: { slug: p.data.slug }, props: { entry: p } }));
109
+ * }
110
+ *
111
+ * const { entry } = Astro.props;
112
+ * const { Content } = await entry.render();
113
+ * ---
114
+ * <BlogPost
115
+ * rawFrontmatter={entry.data}
116
+ * source={entry.id}
117
+ * mdxComponents={BLOG_POST_MDX_COMPONENTS}
118
+ * client:load
119
+ * >
120
+ * <Content />
121
+ * </BlogPost>
122
+ * ```
123
+ */
124
+
125
+ /**
126
+ * Reserved `WebsitePageRef` for every blog post page. Exported so
127
+ * consumers can register a matching `BLOG_POST_PAGE_MANIFEST` in
128
+ * their site root config (the page manifest carries the navigation
129
+ * + label-pack scope; the renderer carries the structural layout).
130
+ *
131
+ * Branded as a plain string literal (cast through `unknown`) because:
132
+ * - The `WebsitePageRef` brand is enforced via Zod's `.brand<>()`,
133
+ * and parsing a string literal at module load time would require
134
+ * dragging the Zod runtime into module init — gratuitous when the
135
+ * ref is a compile-time constant.
136
+ * - `WebsitePageLayout` re-parses any incoming `pageRef` string
137
+ * defensively (see `WebsitePageLayoutProps.pageRef` JSDoc), so the
138
+ * brand is re-asserted at render time anyway.
139
+ */
140
+ export const BLOG_POST_PAGE_REF: WebsitePageRef = 'blog-post' as unknown as WebsitePageRef;
141
+
142
+ export interface BlogPostProps {
143
+ /**
144
+ * Raw frontmatter object Astro hands the page from `entry.data` (or
145
+ * equivalent). The renderer validates this through
146
+ * `BlogPostFrontmatterSchema` synchronously — invalid shapes throw
147
+ * with a `[blog-post]`-prefixed message. Pass `entry.data` directly;
148
+ * do NOT pre-parse on the consumer side (the framework owns the
149
+ * validation surface so a future schema change lands in ONE place).
150
+ */
151
+ rawFrontmatter: unknown;
152
+ /**
153
+ * Diagnostic source identifier echoed into validation errors —
154
+ * typically `entry.id` from Astro's content collection (`'en/why-context.mdx'`
155
+ * or similar). Optional but strongly recommended; without it,
156
+ * `parseBlogPostFrontmatter` throws with a generic
157
+ * `[blog-post] Frontmatter failed …` message that does not name the
158
+ * offending file.
159
+ */
160
+ source?: string;
161
+ /**
162
+ * MDX-tag → React-component map made available inside the post
163
+ * body via `<WebsiteMdxProvider>`. Typically a small object literal
164
+ * exporting one or two business sections the consumer wants
165
+ * available everywhere in the blog surface
166
+ * (`<CTASection ref="post-cta" />`, `<NewsletterSignup />`, etc.).
167
+ *
168
+ * Empty maps are valid — a post body using only standard markdown
169
+ * (no embedded React) needs no overrides.
170
+ */
171
+ mdxComponents?: WebsiteMdxComponentsMap;
172
+ /**
173
+ * Per-page chrome overrides forwarded straight into
174
+ * `<WebsitePageLayout>`. `null` suppresses the slot entirely;
175
+ * `undefined` (the default) inherits the runtime navigation
176
+ * config's header/footer components. Most blog posts leave both
177
+ * unset to inherit the site-wide chrome.
178
+ */
179
+ header?: ReactNode | null;
180
+ footer?: ReactNode | null;
181
+ /**
182
+ * The compiled MDX body — typically `<Content />` from Astro's
183
+ * `await entry.render()` call. Rendered inside the
184
+ * `<WebsiteMdxProvider>` boundary so its inline `<TagName />`
185
+ * references resolve against `mdxComponents`.
186
+ */
187
+ children: ReactNode;
188
+ }
189
+
190
+ /**
191
+ * Empty default for `mdxComponents` — defined at module scope (NOT
192
+ * inline in the destructure default) so the object identity is
193
+ * stable across re-renders. Stable identity matters here because
194
+ * `<MDXProvider>`'s internal context value is `useMemo`-derived from
195
+ * the `components` prop; inline `{}` would invalidate every render.
196
+ */
197
+ const EMPTY_MDX_COMPONENTS: WebsiteMdxComponentsMap = Object.freeze({});
198
+
199
+ export function BlogPost(props: BlogPostProps): ReactNode {
200
+ const {
201
+ rawFrontmatter,
202
+ source,
203
+ mdxComponents = EMPTY_MDX_COMPONENTS,
204
+ header,
205
+ footer,
206
+ children,
207
+ } = props;
208
+
209
+ /**
210
+ * Build the diagnostic context once per render. We do not memoize
211
+ * (one object literal per render is cheaper than the `useMemo`
212
+ * accounting), but we also do not pass `source: undefined`
213
+ * unconditionally because `parseBlogPostFrontmatter` switches on
214
+ * `ctx.source !== undefined` for the error-suffix; passing `{}`
215
+ * when undefined keeps the suffix absent in that case.
216
+ */
217
+ const parseCtx: ParseBlogPostFrontmatterContext =
218
+ source !== undefined ? { source } : {};
219
+ const frontmatter = parseBlogPostFrontmatter(rawFrontmatter, parseCtx);
220
+
221
+ /**
222
+ * The hero markup intentionally uses semantic HTML5 elements
223
+ * (`<header>`, `<h1>`, `<time>`, `<address>`) rather than raw
224
+ * `<div>`s so screen readers and crawlers see the post structure
225
+ * without consumer styling intervention. The styling story is:
226
+ * design tokens via the surrounding `WebsiteRuntimeContext` +
227
+ * the consumer's stylesheet — NO CSS ships from this file.
228
+ */
229
+ return (
230
+ <WebsitePageLayout
231
+ pageRef={BLOG_POST_PAGE_REF}
232
+ header={header}
233
+ footer={footer}
234
+ >
235
+ <article data-website-blog-post-slug={frontmatter.slug as unknown as string}>
236
+ <header>
237
+ {frontmatter.coverImageUrl !== undefined ? (
238
+ <img
239
+ src={frontmatter.coverImageUrl}
240
+ alt=""
241
+ loading="eager"
242
+ data-website-blog-post-cover
243
+ />
244
+ ) : null}
245
+ <h1>{frontmatter.title}</h1>
246
+ <p data-website-blog-post-summary>{frontmatter.summary}</p>
247
+ {/*
248
+ * Meta line is a `<div>`, not a `<p>` — `<address>` is a flow-
249
+ * content element and cannot be a descendant of `<p>` per HTML5
250
+ * (browsers auto-close the `<p>` on `<address>`, which produces
251
+ * a hydration mismatch in React 19). The `<div>` keeps the
252
+ * semantic `<time>` + `<address>` landmarks intact for screen
253
+ * readers + crawlers without violating the parsing rules.
254
+ */}
255
+ <div data-website-blog-post-meta>
256
+ <time dateTime={frontmatter.publishedAt}>
257
+ {frontmatter.publishedAt}
258
+ </time>
259
+ {' · '}
260
+ <address data-website-blog-post-author>
261
+ {frontmatter.authorRef as unknown as string}
262
+ </address>
263
+ </div>
264
+ {frontmatter.tags.length > 0 ? (
265
+ <ul data-website-blog-post-tags>
266
+ {frontmatter.tags.map((tag) => (
267
+ <li key={tag as unknown as string}>
268
+ {tag as unknown as string}
269
+ </li>
270
+ ))}
271
+ </ul>
272
+ ) : null}
273
+ </header>
274
+ <WebsiteMdxProvider components={mdxComponents}>
275
+ {children}
276
+ </WebsiteMdxProvider>
277
+ </article>
278
+ </WebsitePageLayout>
279
+ );
280
+ }
281
+ BlogPost.displayName = 'BlogPost';
282
+
283
+ /**
284
+ * Type alias re-export so consumers writing `BLOG_POST_MDX_COMPONENTS:
285
+ * BlogPostMdxComponents = { … }` have a stable name (the underlying
286
+ * type is the same `WebsiteMdxComponentsMap` from the provider).
287
+ */
288
+ export type BlogPostMdxComponents = WebsiteMdxComponentsMap;
289
+
290
+ /**
291
+ * Convenience type — the Astro `ComponentType` consumers write inline
292
+ * inside a `mdxComponents` map. Re-exported so consumer authoring
293
+ * doesn't need to import from `react` directly.
294
+ */
295
+ export type BlogPostMdxComponent = ComponentType<any>;
296
+ /** @wildo_source:part:end saas.website.blog.post-renderer */