@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
package/LICENSE ADDED
@@ -0,0 +1,34 @@
1
+ Copyright (c) 2026 P2C Consulting, s.à r.l.-S
2
+ RCS Luxembourg B307591
3
+ 6C, Porte de France, L-4360 Esch-sur-Alzette, Luxembourg
4
+
5
+ All rights reserved.
6
+
7
+ This software and its accompanying materials (the "Software") are the
8
+ confidential and proprietary property of P2C Consulting, s.à r.l.-S (the
9
+ "Licensor"). The Software is made available for viewing only. No licence,
10
+ right, or permission of any kind is granted by its publication or by its
11
+ availability on any package registry or source repository.
12
+
13
+ Without the Licensor's prior written permission, you may not use, copy,
14
+ reproduce, modify, adapt, translate, merge, publish, distribute, sublicense,
15
+ sell, host, or create derivative works of the Software, in whole or in part,
16
+ nor use it to provide a service to any third party.
17
+
18
+ Publication of the Software in any public registry is for distribution to
19
+ authorised recipients and for technical evaluation of its packaging only. It
20
+ does not place the Software in the public domain, does not constitute an open
21
+ source release, and does not waive any right of the Licensor.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
27
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
28
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29
+
30
+ Third-party components distributed with the Software remain governed by their
31
+ own licences.
32
+
33
+ Contact: P2C Consulting, s.à r.l.-S, 6C, Porte de France, L-4360
34
+ Esch-sur-Alzette, Luxembourg.
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "writerPid": 95147,
4
+ "orchestratorPid": 94847,
5
+ "orchestratorSessionId": null,
6
+ "orchestratorStartedAt": "Mon Aug 31 09:18:19 2026",
7
+ "distDirectory": "/Users/paulcayon/Projects/Wildo.ai/dev/wildo-ai/engine/saas-website/dist/esm",
8
+ "acquiredAt": "2026-08-31T07:18:20.760Z"
9
+ }
@@ -0,0 +1,324 @@
1
+ import type { ComponentType, FunctionComponent } from 'react';
2
+ import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
3
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
4
+ import { BLOG_POST_PAGE_REF } from '../mdx/BlogPost';
5
+ import type { WebsiteMdxComponentsMap } from '../mdx/website-mdx-provider';
6
+ import type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
7
+ import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
8
+ import type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';
9
+ import type { LabelPack } from '../schemas/validators/label-pack.validator';
10
+ /**
11
+ * @wildo_source:part:start saas.website.blog.bridge facet:layer:astro facet:family:website
12
+ *
13
+ * `createWebsiteBlogPostBridgeIsland(deps)` — factory that returns the
14
+ * React island a consumer's per-locale `*.bridge.tsx` module exports for
15
+ * mounting blog post pages. **Sibling** of `createWebsitePageBridgeIsland`
16
+ * but adapted to the dynamic-routing constraint of the marketing-blog
17
+ * adapter (Phase 7, D-6 = Option A).
18
+ *
19
+ * # Why blog posts cannot reuse the page bridge factory
20
+ *
21
+ * Three structural differences between marketing pages and blog posts
22
+ * forced this dedicated factory shape:
23
+ *
24
+ * 1. **Per-slug body resolution.** A marketing page (`landing`,
25
+ * `pricing`, etc.) has ONE `PageComponent` known at bridge-module
26
+ * load time — the page bridge closes over it. A blog post's body
27
+ * is the compiled MDX module, which varies per slug. Closing the
28
+ * MDX body inside the factory at module load time would force one
29
+ * bridge file per post (defeats Q16=B "one MDX file per post").
30
+ * Instead, the factory closes over a `slugToMdxBody: Record<string,
31
+ * ComponentType>` map — typically built from
32
+ * `import.meta.glob('./content/blog/<locale>/*.mdx', { eager: true })`
33
+ * on the consumer side — and the island accepts the active `slug`
34
+ * as a primitive (JSON-serializable) prop.
35
+ *
36
+ * 2. **Layout already wrapped by `<BlogPost>`.** The page bridge wraps
37
+ * its `PageComponent` in `<WebsitePageLayout pageRef={…}>`. The
38
+ * `<BlogPost>` renderer ALREADY wraps its tree in
39
+ * `<WebsitePageLayout pageRef={BLOG_POST_PAGE_REF}>` (the framework
40
+ * reserves the page ref for the entire blog surface — see
41
+ * `BLOG_POST_PAGE_REF` JSDoc in `BlogPost.tsx`). Reusing the page
42
+ * bridge would double-wrap the layout. This factory instead
43
+ * provides the runtime context and yields directly to `<BlogPost>`,
44
+ * which owns the layout boundary itself.
45
+ *
46
+ * 3. **Astro's React-children context-propagation gap.** Astro
47
+ * renders React component children INDEPENDENTLY of the parent
48
+ * component when the boundary is `.astro` ↔ `.tsx`, even on the
49
+ * server side. This means
50
+ * `<BlogPost client:load><Content /></BlogPost>` written in an
51
+ * `.astro` file would NOT propagate the `<WebsiteMdxProvider>`
52
+ * context to the React components embedded inside `<Content />`
53
+ * (the embedded `<CTASection />` would render WITHOUT the engine's
54
+ * MDX provider scope; the `mdxComponents` map would be ignored).
55
+ * Confirmed via the Astro docs, `@astrojs/react` integration
56
+ * readme, and the documented "Bridging two JSX runtimes" gotcha.
57
+ *
58
+ * The fix: the entire blog post tree (BlogPost + MDX body +
59
+ * embedded section components) lives inside ONE React island. The
60
+ * MDX module is imported as a React component via Astro's
61
+ * `import.meta.glob`, and the bridge renders it as JSX children
62
+ * of `<BlogPost>` from INSIDE the island — no `.astro` ↔ `.tsx`
63
+ * boundary is crossed during the body render.
64
+ *
65
+ * # Why hydration is required (vs. pure SSR)
66
+ *
67
+ * The `WebsiteRuntimeContextProvider` + `<BlogPost>` tree could in
68
+ * theory render on the server only (no `client:*` directive). But:
69
+ *
70
+ * - Embedded sections (`<CTASection />`) typically have their own
71
+ * interactivity (analytics handlers, A/B variant flippers, etc.)
72
+ * that need React event listeners to attach client-side.
73
+ * - The framework's `useWebsiteLabel(...)` hook chain assumes a live
74
+ * React render tree — a pure-SSR tree would freeze label resolution
75
+ * at build time, but any future client-driven label override (e.g.
76
+ * a debug-mode label inspector) would silently break.
77
+ *
78
+ * The factory therefore returns an island that the consumer's `.astro`
79
+ * mounts with `client:load` (or `client:idle` for posts known to be
80
+ * non-interactive). The hydration cost is bounded by the per-locale MDX
81
+ * glob — see "Bundle scaling cliff" below.
82
+ *
83
+ * # Bundle scaling cliff (deferred trade-off)
84
+ *
85
+ * `import.meta.glob({ eager: true })` materializes EVERY post's
86
+ * compiled JS into the per-locale React-island bundle (~5–20 KB per
87
+ * post depending on length and embedded section count). For the
88
+ * Phase 7 dogfood (≤10 posts) this is fine and matches the documented
89
+ * Astro+MDX pattern. Past ~50 posts this becomes a real budget concern.
90
+ * The factory's API is forward-compatible with `{ eager: false }`
91
+ * (lazy loaders): swap the consumer's glob to lazy mode and the
92
+ * `slugToMdxBody[slug]` lookup returns a `() => Promise<{ default:
93
+ * ComponentType }>`; wrap in `React.lazy()` + `<Suspense>` and the
94
+ * factory call site stays unchanged. The migration is a consumer-side
95
+ * change, not an engine API break.
96
+ *
97
+ * # Authoring pattern
98
+ *
99
+ * **Per-locale bridge file** (consumer side, ONE per locale):
100
+ *
101
+ * ```tsx
102
+ * // examples/<app>/website/src/pages-impl/blog-post-en.bridge.tsx
103
+ * import type { ComponentType } from 'react';
104
+ *
105
+ * import { createWebsiteBlogPostBridgeIsland }
106
+ * from '@wildo-ai/saas-website/mdx';
107
+ * import { BLOG_POST_MDX_COMPONENTS }
108
+ * from './blog-post.mdx-components';
109
+ * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';
110
+ *
111
+ * // Static analyzable glob — Vite resolves at build time. The glob
112
+ * // pattern MUST be a single string literal (no concatenation, no
113
+ * // variable interpolation); Vite parses it AST-side. The `as` cast
114
+ * // narrows the unknown record to the per-MDX-file shape Astro's mdx
115
+ * // integration emits (default export = the compiled React component).
116
+ * const rawModules = import.meta.glob('./content/blog/en/*.mdx', {
117
+ * eager: true,
118
+ * }) as Record<string, { default: ComponentType }>;
119
+ *
120
+ * const slugToMdxBody: Record<string, ComponentType> = {};
121
+ * for (const [path, mod] of Object.entries(rawModules)) {
122
+ * // './content/blog/en/why-context.mdx' -> 'why-context'
123
+ * const slug = path.split('/').pop()!.replace(/\.mdx$/, '');
124
+ * slugToMdxBody[slug] = mod.default;
125
+ * }
126
+ *
127
+ * export const BlogPostEnBridgeIsland =
128
+ * createWebsiteBlogPostBridgeIsland({
129
+ * slugToMdxBody,
130
+ * navigation: WEBSITE_ROOT_CONFIG.navigation,
131
+ * mdxComponents: BLOG_POST_MDX_COMPONENTS,
132
+ * });
133
+ * ```
134
+ *
135
+ * **Per-locale `[slug].astro` shell** (consumer side):
136
+ *
137
+ * ```astro
138
+ * ---
139
+ * import { getCollection } from 'astro:content';
140
+ * import {
141
+ * loadWebsitePageRuntime,
142
+ * renderBlogPostHeadHtml,
143
+ * renderWebsiteDesignTokensCss,
144
+ * } from '@wildo-ai/saas-website/astro';
145
+ * import { parseBlogPostFrontmatter } from '@wildo-ai/saas-website/mdx';
146
+ * import { BlogPostEnBridgeIsland } from '@/pages-impl/blog-post-en.bridge';
147
+ * import { BLOG_POST_PAGE_MANIFEST } from '@/pages-impl/blog-post.page';
148
+ * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';
149
+ *
150
+ * export async function getStaticPaths() {
151
+ * const posts = await getCollection('blog');
152
+ * const defaultLocale = WEBSITE_SITE_CONTEXT.websiteConfig.defaultLocale;
153
+ * return posts
154
+ * .filter((p) => p.data.locale === defaultLocale)
155
+ * .map((p) => ({ params: { slug: p.data.slug }, props: { entry: p } }));
156
+ * }
157
+ * const { entry } = Astro.props;
158
+ *
159
+ * const runtime = loadWebsitePageRuntime({
160
+ * manifest: BLOG_POST_PAGE_MANIFEST,
161
+ * siteContext: WEBSITE_SITE_CONTEXT,
162
+ * labelPacksDir: './src/i18n',
163
+ * astroLocale: Astro.currentLocale,
164
+ * });
165
+ * const frontmatter = parseBlogPostFrontmatter(entry.data, { source: entry.id });
166
+ * const headHtml = renderBlogPostHeadHtml({
167
+ * siteContext: WEBSITE_SITE_CONTEXT,
168
+ * locale: runtime.locale,
169
+ * routePath: `/blog/${frontmatter.slug}`,
170
+ * frontmatter,
171
+ * });
172
+ * const designTokensCss = renderWebsiteDesignTokensCss(
173
+ * WEBSITE_SITE_CONTEXT.rootConfig.designTokens,
174
+ * );
175
+ * ---
176
+ * <!doctype html>
177
+ * <html lang={runtime.locale}>
178
+ * <head>
179
+ * <meta charset="UTF-8" />
180
+ * <meta name="viewport" content="width=device-width,initial-scale=1.0" />
181
+ * <Fragment set:html={headHtml} />
182
+ * <style set:html={designTokensCss}></style>
183
+ * </head>
184
+ * <body>
185
+ * <BlogPostEnBridgeIsland
186
+ * client:load
187
+ * slug={frontmatter.slug}
188
+ * rawFrontmatter={entry.data}
189
+ * source={entry.id}
190
+ * locale={runtime.locale}
191
+ * defaultLocale={runtime.defaultLocale}
192
+ * labelPack={runtime.labelPack}
193
+ * designTokens={WEBSITE_SITE_CONTEXT.rootConfig.designTokens}
194
+ * skipToMainContentLabelKey={
195
+ * WEBSITE_SITE_CONTEXT.rootConfig.skipToMainContentLabelKey
196
+ * }
197
+ * />
198
+ * </body>
199
+ * </html>
200
+ * ```
201
+ *
202
+ * # Boundary
203
+ *
204
+ * Strictly client-bundle-safe (re-exported from `mdx.ts` — NOT from
205
+ * `astro-island.ts`, because this module hard-imports the MDX adapter
206
+ * whose `@mdx-js/react` peer is optional; walked by the
207
+ * bundle-isolation test). MUST stay clear of `node:*` and SSR-tier
208
+ * specifiers.
209
+ */
210
+ /**
211
+ * Module-load-time dependencies — closed over inside the returned
212
+ * island. NEVER cross the hydration boundary as serialized props.
213
+ */
214
+ export interface CreateWebsiteBlogPostBridgeIslandDeps {
215
+ /**
216
+ * Per-slug map of compiled MDX bodies (each entry is a React
217
+ * component — the `.default` export of the MDX module). Typically
218
+ * built from `import.meta.glob('./content/blog/<locale>/*.mdx',
219
+ * { eager: true })` with the path-key normalized to the slug. Closed
220
+ * over at module-load time so the entire MDX body graph for the
221
+ * locale lands inside ONE React island bundle (D-6 Option A) — this
222
+ * is the structural fix for Astro's React-children context-propagation
223
+ * gap (see file preamble).
224
+ *
225
+ * The map is read by `slugToMdxBody[props.slug]` at island render
226
+ * time. A missing slug throws a clear framework-owned error (rather than
227
+ * rendering an empty body silently); the consumer's `getStaticPaths`
228
+ * is responsible for not emitting routes for slugs absent from the
229
+ * map.
230
+ *
231
+ * **Why `ComponentType<any>` and not a stricter generic**: matches
232
+ * the upstream `@mdx-js/react` convention (`MDXContent` from
233
+ * `mdx/types` is parameterized over an open `MDXProps` shape). MDX
234
+ * compilation hands the body a compiler-managed props object that
235
+ * the type system cannot narrow at the framework boundary; using
236
+ * `<any>` here keeps the consumer's
237
+ * `import.meta.glob(...) as Record<string, { default: ComponentType }>`
238
+ * cast assignable without an additional wrap/unwrap. Same posture
239
+ * as `WebsiteMdxComponentsMap` (intentional symmetry).
240
+ */
241
+ slugToMdxBody: Readonly<Record<string, ComponentType<any>>>;
242
+ /**
243
+ * Site-wide navigation slot bindings (header / footer components).
244
+ * Closed over here for the same reason as the page bridge: both
245
+ * fields are `ComponentType` refs that cannot survive Astro's
246
+ * serialization boundary.
247
+ */
248
+ navigation: WebsiteNavigationConfig;
249
+ /**
250
+ * MDX-tag → React-component map forwarded into `<BlogPost>` and
251
+ * scoped via `<WebsiteMdxProvider>`. Typically a small object
252
+ * literal exporting the consumer-authored business sections that
253
+ * blog posts can embed (`<CTASection />`, etc.). Closed over here
254
+ * (rather than passed as an island prop) because the values are
255
+ * `ComponentType` refs.
256
+ */
257
+ mdxComponents?: WebsiteMdxComponentsMap;
258
+ }
259
+ /**
260
+ * Hydration-boundary props — what the consumer's `.astro` page passes
261
+ * to `<BlogPostEnBridgeIsland client:load ...{primitiveProps}/>`.
262
+ * Every field is JSON-serializable.
263
+ */
264
+ export interface WebsiteBlogPostBridgeIslandProps {
265
+ /**
266
+ * Slug of the post being rendered. Used by the island to look up
267
+ * the matching MDX body in `deps.slugToMdxBody`. Carried as a plain
268
+ * string (not the branded `BlogPostSlug`) because Astro's
269
+ * serialization boundary unwraps brands.
270
+ */
271
+ slug: string;
272
+ /**
273
+ * Raw frontmatter object Astro hands the page from `entry.data`.
274
+ * Forwarded straight to `<BlogPost>` for strict Zod-4 validation.
275
+ * The bridge does NOT re-validate (single validation surface, owned
276
+ * by the renderer).
277
+ */
278
+ rawFrontmatter: unknown;
279
+ /**
280
+ * Diagnostic source identifier echoed into validation errors —
281
+ * typically `entry.id` from Astro's content collection. Optional
282
+ * but recommended for actionable error messages.
283
+ */
284
+ source?: string;
285
+ /** The locale Astro resolved (with default-locale fallback). */
286
+ locale: AvailableLanguage;
287
+ /**
288
+ * The site's default locale. Required for `WebsiteInternalLink`
289
+ * locale-prefix logic (mirrors the page bridge's `defaultLocale`
290
+ * island prop).
291
+ */
292
+ defaultLocale: AvailableLanguage;
293
+ /** Already-loaded + already-shape-validated label pack. */
294
+ labelPack: LabelPack;
295
+ /** Design-token bundle from `WebsiteRootConfig.designTokens`. */
296
+ designTokens: WebsiteDesignTokens;
297
+ /**
298
+ * Fully-composed label key the framework reads to render the
299
+ * "Skip to main content" affordance. Mirrors
300
+ * `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as a
301
+ * primitive island prop for the same reason as the page bridge.
302
+ */
303
+ skipToMainContentLabelKey: WebsiteLabelKey;
304
+ /** Optional website-facing public providers materialized at build time. */
305
+ frontendProviders?: FrontendProvidersBlock;
306
+ }
307
+ /**
308
+ * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function
309
+ * component — Astro hydrates nothing else here — and the union is not callable, so declaring it
310
+ * cost callers the ability to invoke the island directly (which the bridge suites do, to observe
311
+ * the synchronous throw for an unregistered slug without routing it through React's reconciler
312
+ * error path). `FunctionComponent` is assignable to `ComponentType`, so no consumer narrows.
313
+ */
314
+ export declare function createWebsiteBlogPostBridgeIsland(deps: CreateWebsiteBlogPostBridgeIslandDeps): FunctionComponent<WebsiteBlogPostBridgeIslandProps>;
315
+ /**
316
+ * Re-export of the framework-reserved blog-post page ref. Consumers
317
+ * authoring `BLOG_POST_PAGE_MANIFEST` need this constant to satisfy
318
+ * the manifest's `ref` field; re-exporting it from the bridge module
319
+ * means the consumer's per-locale bridge file can grab both symbols
320
+ * from the one `@wildo-ai/saas-website/mdx` import it already has.
321
+ */
322
+ export { BLOG_POST_PAGE_REF };
323
+ /** @wildo_source:part:end saas.website.blog.bridge */
324
+ //# sourceMappingURL=blog-post-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blog-post-bridge.d.ts","sourceRoot":"","sources":["../../../../src/astro/blog-post-bridge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAa,MAAM,OAAO,CAAC;AAEzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAO9E,OAAO,EAAY,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAC3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+DAA+D,CAAC;AACzG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AACvF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yDAAyD,CAAC;AACvG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuMG;AAEH;;;GAGG;AACH,MAAM,WAAW,qCAAqC;IACpD;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5D;;;;;OAKG;IACH,UAAU,EAAE,uBAAuB,CAAC;IACpC;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,uBAAuB,CAAC;CACzC;AAED;;;;GAIG;AACH,MAAM,WAAW,gCAAgC;IAC/C;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,cAAc,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;OAIG;IACH,aAAa,EAAE,iBAAiB,CAAC;IACjC,2DAA2D;IAC3D,SAAS,EAAE,SAAS,CAAC;IACrB,iEAAiE;IACjE,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;OAKG;IACH,yBAAyB,EAAE,eAAe,CAAC;IAC3C,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;CAC5C;AAID;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,qCAAqC,GAC1C,iBAAiB,CAAC,gCAAgC,CAAC,CAiErD;AAED;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAC9B,sDAAsD"}
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { WebsiteRuntimeContextProvider, } from '../core/contexts/WebsiteRuntimeContext.js';
3
+ import { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website.js';
4
+ import { BlogPost, BLOG_POST_PAGE_REF } from '../mdx/BlogPost.js';
5
+ const EMPTY_MDX_COMPONENTS = Object.freeze({});
6
+ /**
7
+ * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function
8
+ * component — Astro hydrates nothing else here — and the union is not callable, so declaring it
9
+ * cost callers the ability to invoke the island directly (which the bridge suites do, to observe
10
+ * the synchronous throw for an unregistered slug without routing it through React's reconciler
11
+ * error path). `FunctionComponent` is assignable to `ComponentType`, so no consumer narrows.
12
+ */
13
+ export function createWebsiteBlogPostBridgeIsland(deps) {
14
+ const { slugToMdxBody, navigation, mdxComponents = EMPTY_MDX_COMPONENTS, } = deps;
15
+ function WebsiteBlogPostBridgeIsland({ slug, rawFrontmatter, source, locale, defaultLocale, labelPack, designTokens, skipToMainContentLabelKey, frontendProviders, }) {
16
+ const MdxBody = slugToMdxBody[slug];
17
+ if (MdxBody === undefined) {
18
+ /**
19
+ * Loud failure — the consumer's `getStaticPaths` is supposed to
20
+ * only emit routes for slugs present in the MDX glob. A missing
21
+ * lookup means the glob and the collection are out of sync; the
22
+ * operator needs an actionable error string with the offending
23
+ * slug + the keys that ARE present (truncated to keep the build
24
+ * log readable).
25
+ */
26
+ const knownKeys = Object.keys(slugToMdxBody);
27
+ const known = knownKeys.length <= 5
28
+ ? knownKeys.join(', ')
29
+ : `${knownKeys.slice(0, 5).join(', ')}, … (${knownKeys.length} total)`;
30
+ throw new Error(`[blog-post-bridge] No MDX body registered for slug="${slug}". ` +
31
+ `The bridge factory's slugToMdxBody map only knows: ${known}. ` +
32
+ `Verify the consumer's import.meta.glob('...content/blog/<locale>/*.mdx') ` +
33
+ `pattern matches the source file naming.`);
34
+ }
35
+ const runtimeValue = {
36
+ locale,
37
+ defaultLocale,
38
+ labelPack,
39
+ designTokens,
40
+ navigation,
41
+ skipToMainContentLabelKey,
42
+ frontendProviderRegistry: createFrontendWebsiteProviderRegistry(frontendProviders),
43
+ };
44
+ return (_jsx(WebsiteRuntimeContextProvider, { value: runtimeValue, children: _jsx(BlogPost, { rawFrontmatter: rawFrontmatter, source: source, mdxComponents: mdxComponents, children: _jsx(MdxBody, {}) }) }));
45
+ }
46
+ WebsiteBlogPostBridgeIsland.displayName = 'WebsiteBlogPostBridgeIsland';
47
+ return WebsiteBlogPostBridgeIsland;
48
+ }
49
+ /**
50
+ * Re-export of the framework-reserved blog-post page ref. Consumers
51
+ * authoring `BLOG_POST_PAGE_MANIFEST` need this constant to satisfy
52
+ * the manifest's `ref` field; re-exporting it from the bridge module
53
+ * means the consumer's per-locale bridge file can grab both symbols
54
+ * from the one `@wildo-ai/saas-website/mdx` import it already has.
55
+ */
56
+ export { BLOG_POST_PAGE_REF };
57
+ //# sourceMappingURL=blog-post-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blog-post-bridge.js","sourceRoot":"","sources":["../../../../src/astro/blog-post-bridge.tsx"],"names":[],"mappings":";AAKA,OAAO,EACL,6BAA6B,GAE9B,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,qCAAqC,EAAE,MAAM,+DAA+D,CAAC;AACtH,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAmT/D,MAAM,oBAAoB,GAA4B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,UAAU,iCAAiC,CAC/C,IAA2C;IAE3C,MAAM,EACJ,aAAa,EACb,UAAU,EACV,aAAa,GAAG,oBAAoB,GACrC,GAAG,IAAI,CAAC;IAET,SAAS,2BAA2B,CAAC,EACnC,IAAI,EACJ,cAAc,EACd,MAAM,EACN,MAAM,EACN,aAAa,EACb,SAAS,EACT,YAAY,EACZ,yBAAyB,EACzB,iBAAiB,GACgB;QACjC,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B;;;;;;;eAOG;YACH,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YAC7C,MAAM,KAAK,GACT,SAAS,CAAC,MAAM,IAAI,CAAC;gBACnB,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;gBACtB,CAAC,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,SAAS,CAAC,MAAM,SAAS,CAAC;YAC3E,MAAM,IAAI,KAAK,CACb,uDAAuD,IAAI,KAAK;gBAC9D,sDAAsD,KAAK,IAAI;gBAC/D,2EAA2E;gBAC3E,yCAAyC,CAC5C,CAAC;QACJ,CAAC;QAED,MAAM,YAAY,GAA+B;YAC/C,MAAM;YACN,aAAa;YACb,SAAS;YACT,YAAY;YACZ,UAAU;YACV,yBAAyB;YACzB,wBAAwB,EAAE,qCAAqC,CAAC,iBAAiB,CAAC;SACnF,CAAC;QACF,OAAO,CACL,KAAC,6BAA6B,IAAC,KAAK,EAAE,YAAY,YAChD,KAAC,QAAQ,IACP,cAAc,EAAE,cAAc,EAC9B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,aAAa,YAE5B,KAAC,OAAO,KAAG,GACF,GACmB,CACjC,CAAC;IACJ,CAAC;IACD,2BAA2B,CAAC,WAAW,GAAG,6BAA6B,CAAC;IAExE,OAAO,2BAA2B,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,OAAO,EAAE,kBAAkB,EAAE,CAAC","sourcesContent":["import type { ComponentType, FunctionComponent, ReactNode } from 'react';\n\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport {\n WebsiteRuntimeContextProvider,\n type WebsiteRuntimeContextValue,\n} from '../core/contexts/WebsiteRuntimeContext';\nimport { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';\nimport { BlogPost, BLOG_POST_PAGE_REF } from '../mdx/BlogPost';\nimport type { WebsiteMdxComponentsMap } from '../mdx/website-mdx-provider';\nimport type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\n/**\n * @wildo_source:part:start saas.website.blog.bridge facet:layer:astro facet:family:website\n *\n * `createWebsiteBlogPostBridgeIsland(deps)` — factory that returns the\n * React island a consumer's per-locale `*.bridge.tsx` module exports for\n * mounting blog post pages. **Sibling** of `createWebsitePageBridgeIsland`\n * but adapted to the dynamic-routing constraint of the marketing-blog\n * adapter (Phase 7, D-6 = Option A).\n *\n * # Why blog posts cannot reuse the page bridge factory\n *\n * Three structural differences between marketing pages and blog posts\n * forced this dedicated factory shape:\n *\n * 1. **Per-slug body resolution.** A marketing page (`landing`,\n * `pricing`, etc.) has ONE `PageComponent` known at bridge-module\n * load time — the page bridge closes over it. A blog post's body\n * is the compiled MDX module, which varies per slug. Closing the\n * MDX body inside the factory at module load time would force one\n * bridge file per post (defeats Q16=B \"one MDX file per post\").\n * Instead, the factory closes over a `slugToMdxBody: Record<string,\n * ComponentType>` map — typically built from\n * `import.meta.glob('./content/blog/<locale>/*.mdx', { eager: true })`\n * on the consumer side — and the island accepts the active `slug`\n * as a primitive (JSON-serializable) prop.\n *\n * 2. **Layout already wrapped by `<BlogPost>`.** The page bridge wraps\n * its `PageComponent` in `<WebsitePageLayout pageRef={…}>`. The\n * `<BlogPost>` renderer ALREADY wraps its tree in\n * `<WebsitePageLayout pageRef={BLOG_POST_PAGE_REF}>` (the framework\n * reserves the page ref for the entire blog surface — see\n * `BLOG_POST_PAGE_REF` JSDoc in `BlogPost.tsx`). Reusing the page\n * bridge would double-wrap the layout. This factory instead\n * provides the runtime context and yields directly to `<BlogPost>`,\n * which owns the layout boundary itself.\n *\n * 3. **Astro's React-children context-propagation gap.** Astro\n * renders React component children INDEPENDENTLY of the parent\n * component when the boundary is `.astro` ↔ `.tsx`, even on the\n * server side. This means\n * `<BlogPost client:load><Content /></BlogPost>` written in an\n * `.astro` file would NOT propagate the `<WebsiteMdxProvider>`\n * context to the React components embedded inside `<Content />`\n * (the embedded `<CTASection />` would render WITHOUT the engine's\n * MDX provider scope; the `mdxComponents` map would be ignored).\n * Confirmed via the Astro docs, `@astrojs/react` integration\n * readme, and the documented \"Bridging two JSX runtimes\" gotcha.\n *\n * The fix: the entire blog post tree (BlogPost + MDX body +\n * embedded section components) lives inside ONE React island. The\n * MDX module is imported as a React component via Astro's\n * `import.meta.glob`, and the bridge renders it as JSX children\n * of `<BlogPost>` from INSIDE the island — no `.astro` ↔ `.tsx`\n * boundary is crossed during the body render.\n *\n * # Why hydration is required (vs. pure SSR)\n *\n * The `WebsiteRuntimeContextProvider` + `<BlogPost>` tree could in\n * theory render on the server only (no `client:*` directive). But:\n *\n * - Embedded sections (`<CTASection />`) typically have their own\n * interactivity (analytics handlers, A/B variant flippers, etc.)\n * that need React event listeners to attach client-side.\n * - The framework's `useWebsiteLabel(...)` hook chain assumes a live\n * React render tree — a pure-SSR tree would freeze label resolution\n * at build time, but any future client-driven label override (e.g.\n * a debug-mode label inspector) would silently break.\n *\n * The factory therefore returns an island that the consumer's `.astro`\n * mounts with `client:load` (or `client:idle` for posts known to be\n * non-interactive). The hydration cost is bounded by the per-locale MDX\n * glob — see \"Bundle scaling cliff\" below.\n *\n * # Bundle scaling cliff (deferred trade-off)\n *\n * `import.meta.glob({ eager: true })` materializes EVERY post's\n * compiled JS into the per-locale React-island bundle (~5–20 KB per\n * post depending on length and embedded section count). For the\n * Phase 7 dogfood (≤10 posts) this is fine and matches the documented\n * Astro+MDX pattern. Past ~50 posts this becomes a real budget concern.\n * The factory's API is forward-compatible with `{ eager: false }`\n * (lazy loaders): swap the consumer's glob to lazy mode and the\n * `slugToMdxBody[slug]` lookup returns a `() => Promise<{ default:\n * ComponentType }>`; wrap in `React.lazy()` + `<Suspense>` and the\n * factory call site stays unchanged. The migration is a consumer-side\n * change, not an engine API break.\n *\n * # Authoring pattern\n *\n * **Per-locale bridge file** (consumer side, ONE per locale):\n *\n * ```tsx\n * // examples/<app>/website/src/pages-impl/blog-post-en.bridge.tsx\n * import type { ComponentType } from 'react';\n *\n * import { createWebsiteBlogPostBridgeIsland }\n * from '@wildo-ai/saas-website/mdx';\n * import { BLOG_POST_MDX_COMPONENTS }\n * from './blog-post.mdx-components';\n * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';\n *\n * // Static analyzable glob — Vite resolves at build time. The glob\n * // pattern MUST be a single string literal (no concatenation, no\n * // variable interpolation); Vite parses it AST-side. The `as` cast\n * // narrows the unknown record to the per-MDX-file shape Astro's mdx\n * // integration emits (default export = the compiled React component).\n * const rawModules = import.meta.glob('./content/blog/en/*.mdx', {\n * eager: true,\n * }) as Record<string, { default: ComponentType }>;\n *\n * const slugToMdxBody: Record<string, ComponentType> = {};\n * for (const [path, mod] of Object.entries(rawModules)) {\n * // './content/blog/en/why-context.mdx' -> 'why-context'\n * const slug = path.split('/').pop()!.replace(/\\.mdx$/, '');\n * slugToMdxBody[slug] = mod.default;\n * }\n *\n * export const BlogPostEnBridgeIsland =\n * createWebsiteBlogPostBridgeIsland({\n * slugToMdxBody,\n * navigation: WEBSITE_ROOT_CONFIG.navigation,\n * mdxComponents: BLOG_POST_MDX_COMPONENTS,\n * });\n * ```\n *\n * **Per-locale `[slug].astro` shell** (consumer side):\n *\n * ```astro\n * ---\n * import { getCollection } from 'astro:content';\n * import {\n * loadWebsitePageRuntime,\n * renderBlogPostHeadHtml,\n * renderWebsiteDesignTokensCss,\n * } from '@wildo-ai/saas-website/astro';\n * import { parseBlogPostFrontmatter } from '@wildo-ai/saas-website/mdx';\n * import { BlogPostEnBridgeIsland } from '@/pages-impl/blog-post-en.bridge';\n * import { BLOG_POST_PAGE_MANIFEST } from '@/pages-impl/blog-post.page';\n * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';\n *\n * export async function getStaticPaths() {\n * const posts = await getCollection('blog');\n * const defaultLocale = WEBSITE_SITE_CONTEXT.websiteConfig.defaultLocale;\n * return posts\n * .filter((p) => p.data.locale === defaultLocale)\n * .map((p) => ({ params: { slug: p.data.slug }, props: { entry: p } }));\n * }\n * const { entry } = Astro.props;\n *\n * const runtime = loadWebsitePageRuntime({\n * manifest: BLOG_POST_PAGE_MANIFEST,\n * siteContext: WEBSITE_SITE_CONTEXT,\n * labelPacksDir: './src/i18n',\n * astroLocale: Astro.currentLocale,\n * });\n * const frontmatter = parseBlogPostFrontmatter(entry.data, { source: entry.id });\n * const headHtml = renderBlogPostHeadHtml({\n * siteContext: WEBSITE_SITE_CONTEXT,\n * locale: runtime.locale,\n * routePath: `/blog/${frontmatter.slug}`,\n * frontmatter,\n * });\n * const designTokensCss = renderWebsiteDesignTokensCss(\n * WEBSITE_SITE_CONTEXT.rootConfig.designTokens,\n * );\n * ---\n * <!doctype html>\n * <html lang={runtime.locale}>\n * <head>\n * <meta charset=\"UTF-8\" />\n * <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\" />\n * <Fragment set:html={headHtml} />\n * <style set:html={designTokensCss}></style>\n * </head>\n * <body>\n * <BlogPostEnBridgeIsland\n * client:load\n * slug={frontmatter.slug}\n * rawFrontmatter={entry.data}\n * source={entry.id}\n * locale={runtime.locale}\n * defaultLocale={runtime.defaultLocale}\n * labelPack={runtime.labelPack}\n * designTokens={WEBSITE_SITE_CONTEXT.rootConfig.designTokens}\n * skipToMainContentLabelKey={\n * WEBSITE_SITE_CONTEXT.rootConfig.skipToMainContentLabelKey\n * }\n * />\n * </body>\n * </html>\n * ```\n *\n * # Boundary\n *\n * Strictly client-bundle-safe (re-exported from `mdx.ts` — NOT from\n * `astro-island.ts`, because this module hard-imports the MDX adapter\n * whose `@mdx-js/react` peer is optional; walked by the\n * bundle-isolation test). MUST stay clear of `node:*` and SSR-tier\n * specifiers.\n */\n\n/**\n * Module-load-time dependencies — closed over inside the returned\n * island. NEVER cross the hydration boundary as serialized props.\n */\nexport interface CreateWebsiteBlogPostBridgeIslandDeps {\n /**\n * Per-slug map of compiled MDX bodies (each entry is a React\n * component — the `.default` export of the MDX module). Typically\n * built from `import.meta.glob('./content/blog/<locale>/*.mdx',\n * { eager: true })` with the path-key normalized to the slug. Closed\n * over at module-load time so the entire MDX body graph for the\n * locale lands inside ONE React island bundle (D-6 Option A) — this\n * is the structural fix for Astro's React-children context-propagation\n * gap (see file preamble).\n *\n * The map is read by `slugToMdxBody[props.slug]` at island render\n * time. A missing slug throws a clear framework-owned error (rather than\n * rendering an empty body silently); the consumer's `getStaticPaths`\n * is responsible for not emitting routes for slugs absent from the\n * map.\n *\n * **Why `ComponentType<any>` and not a stricter generic**: matches\n * the upstream `@mdx-js/react` convention (`MDXContent` from\n * `mdx/types` is parameterized over an open `MDXProps` shape). MDX\n * compilation hands the body a compiler-managed props object that\n * the type system cannot narrow at the framework boundary; using\n * `<any>` here keeps the consumer's\n * `import.meta.glob(...) as Record<string, { default: ComponentType }>`\n * cast assignable without an additional wrap/unwrap. Same posture\n * as `WebsiteMdxComponentsMap` (intentional symmetry).\n */\n slugToMdxBody: Readonly<Record<string, ComponentType<any>>>;\n /**\n * Site-wide navigation slot bindings (header / footer components).\n * Closed over here for the same reason as the page bridge: both\n * fields are `ComponentType` refs that cannot survive Astro's\n * serialization boundary.\n */\n navigation: WebsiteNavigationConfig;\n /**\n * MDX-tag → React-component map forwarded into `<BlogPost>` and\n * scoped via `<WebsiteMdxProvider>`. Typically a small object\n * literal exporting the consumer-authored business sections that\n * blog posts can embed (`<CTASection />`, etc.). Closed over here\n * (rather than passed as an island prop) because the values are\n * `ComponentType` refs.\n */\n mdxComponents?: WebsiteMdxComponentsMap;\n}\n\n/**\n * Hydration-boundary props — what the consumer's `.astro` page passes\n * to `<BlogPostEnBridgeIsland client:load ...{primitiveProps}/>`.\n * Every field is JSON-serializable.\n */\nexport interface WebsiteBlogPostBridgeIslandProps {\n /**\n * Slug of the post being rendered. Used by the island to look up\n * the matching MDX body in `deps.slugToMdxBody`. Carried as a plain\n * string (not the branded `BlogPostSlug`) because Astro's\n * serialization boundary unwraps brands.\n */\n slug: string;\n /**\n * Raw frontmatter object Astro hands the page from `entry.data`.\n * Forwarded straight to `<BlogPost>` for strict Zod-4 validation.\n * The bridge does NOT re-validate (single validation surface, owned\n * by the renderer).\n */\n rawFrontmatter: unknown;\n /**\n * Diagnostic source identifier echoed into validation errors —\n * typically `entry.id` from Astro's content collection. Optional\n * but recommended for actionable error messages.\n */\n source?: string;\n /** The locale Astro resolved (with default-locale fallback). */\n locale: AvailableLanguage;\n /**\n * The site's default locale. Required for `WebsiteInternalLink`\n * locale-prefix logic (mirrors the page bridge's `defaultLocale`\n * island prop).\n */\n defaultLocale: AvailableLanguage;\n /** Already-loaded + already-shape-validated label pack. */\n labelPack: LabelPack;\n /** Design-token bundle from `WebsiteRootConfig.designTokens`. */\n designTokens: WebsiteDesignTokens;\n /**\n * Fully-composed label key the framework reads to render the\n * \"Skip to main content\" affordance. Mirrors\n * `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as a\n * primitive island prop for the same reason as the page bridge.\n */\n skipToMainContentLabelKey: WebsiteLabelKey;\n /** Optional website-facing public providers materialized at build time. */\n frontendProviders?: FrontendProvidersBlock;\n}\n\nconst EMPTY_MDX_COMPONENTS: WebsiteMdxComponentsMap = Object.freeze({});\n\n/**\n * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function\n * component — Astro hydrates nothing else here — and the union is not callable, so declaring it\n * cost callers the ability to invoke the island directly (which the bridge suites do, to observe\n * the synchronous throw for an unregistered slug without routing it through React's reconciler\n * error path). `FunctionComponent` is assignable to `ComponentType`, so no consumer narrows.\n */\nexport function createWebsiteBlogPostBridgeIsland(\n deps: CreateWebsiteBlogPostBridgeIslandDeps,\n): FunctionComponent<WebsiteBlogPostBridgeIslandProps> {\n const {\n slugToMdxBody,\n navigation,\n mdxComponents = EMPTY_MDX_COMPONENTS,\n } = deps;\n\n function WebsiteBlogPostBridgeIsland({\n slug,\n rawFrontmatter,\n source,\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n skipToMainContentLabelKey,\n frontendProviders,\n }: WebsiteBlogPostBridgeIslandProps): ReactNode {\n const MdxBody = slugToMdxBody[slug];\n if (MdxBody === undefined) {\n /**\n * Loud failure — the consumer's `getStaticPaths` is supposed to\n * only emit routes for slugs present in the MDX glob. A missing\n * lookup means the glob and the collection are out of sync; the\n * operator needs an actionable error string with the offending\n * slug + the keys that ARE present (truncated to keep the build\n * log readable).\n */\n const knownKeys = Object.keys(slugToMdxBody);\n const known =\n knownKeys.length <= 5\n ? knownKeys.join(', ')\n : `${knownKeys.slice(0, 5).join(', ')}, … (${knownKeys.length} total)`;\n throw new Error(\n `[blog-post-bridge] No MDX body registered for slug=\"${slug}\". ` +\n `The bridge factory's slugToMdxBody map only knows: ${known}. ` +\n `Verify the consumer's import.meta.glob('...content/blog/<locale>/*.mdx') ` +\n `pattern matches the source file naming.`,\n );\n }\n\n const runtimeValue: WebsiteRuntimeContextValue = {\n locale,\n defaultLocale,\n labelPack,\n designTokens,\n navigation,\n skipToMainContentLabelKey,\n frontendProviderRegistry: createFrontendWebsiteProviderRegistry(frontendProviders),\n };\n return (\n <WebsiteRuntimeContextProvider value={runtimeValue}>\n <BlogPost\n rawFrontmatter={rawFrontmatter}\n source={source}\n mdxComponents={mdxComponents}\n >\n <MdxBody />\n </BlogPost>\n </WebsiteRuntimeContextProvider>\n );\n }\n WebsiteBlogPostBridgeIsland.displayName = 'WebsiteBlogPostBridgeIsland';\n\n return WebsiteBlogPostBridgeIsland;\n}\n\n/**\n * Re-export of the framework-reserved blog-post page ref. Consumers\n * authoring `BLOG_POST_PAGE_MANIFEST` need this constant to satisfy\n * the manifest's `ref` field; re-exporting it from the bridge module\n * means the consumer's per-locale bridge file can grab both symbols\n * from the one `@wildo-ai/saas-website/mdx` import it already has.\n */\nexport { BLOG_POST_PAGE_REF };\n/** @wildo_source:part:end saas.website.blog.bridge */\n"]}
@@ -0,0 +1,129 @@
1
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
+ import type { BlogPostFrontmatter } from '../schemas/blog/blog-post-frontmatter.shared.schemas';
3
+ import type { WebsiteStructuredDataPayload } from '../schemas/structured-data/website-structured-data.shared.schemas';
4
+ import type { WebsiteSiteContext } from './website-site-context';
5
+ /**
6
+ * @wildo_source:part:start saas.website.blog.head-renderer facet:layer:astro facet:family:website
7
+ *
8
+ * `renderBlogPostHeadHtml(opts)` — sibling of `renderWebsitePageHeadHtml`
9
+ * that emits the SEO `<head>` block for an individual blog post,
10
+ * sourcing per-render copy from a validated `BlogPostFrontmatter`
11
+ * instead of the page manifest's `metaLabelKeys`.
12
+ *
13
+ * # Why a separate renderer (not an overload of the page-head renderer)
14
+ *
15
+ * The framework-mindset rule "every type / bucket / field must carry
16
+ * its semantic weight" pushes against overloading
17
+ * `renderWebsitePageHeadHtml` with a "blog-post mode" flag:
18
+ *
19
+ * - The two renderers have STRUCTURALLY different inputs.
20
+ * `renderWebsitePageHeadHtml` reads `manifest.metaLabelKeys.{titleKey,
21
+ * descriptionKey, ogImageUrl}` + the matching `labelPack` lookup;
22
+ * this renderer reads `frontmatter.{title, summary, coverImageUrl,
23
+ * publishedAt, authorRef, tags}` directly — no label-pack
24
+ * dependency at all (blog body copy is per-post, not
25
+ * per-locale-pack, by design).
26
+ * - The two renderers emit DIFFERENT meta surfaces.
27
+ * `og:type=website` vs. `og:type=article`; the article variant
28
+ * additionally emits `article:published_time`,
29
+ * `article:author`, and `article:tag` (one per `frontmatter.tags`
30
+ * entry). Folding both into one helper would require an
31
+ * `articleMeta?: { ... }` discriminated branch that carries no
32
+ * value when called from a non-blog page — exactly the kind of
33
+ * "convenience union" the framework rules forbid.
34
+ * - The blog-post route itself uses a parameterized `routePath`
35
+ * (`/blog/[slug]`) that needs the SLUG substituted at render time
36
+ * to produce the absolute canonical/hreflang URLs. Page-level
37
+ * routes are always literal — keeping the substitution in this
38
+ * renderer (rather than at the manifest layer) means the page
39
+ * manifest stays free of per-render concerns.
40
+ *
41
+ * Both renderers share the exact same hreflang/canonical/OG locale
42
+ * composition logic — duplicating ~30 lines of stable URL composition
43
+ * is the lesser evil compared to threading a generic options bag
44
+ * through both. If a third renderer ever lands (a "campaign-page" or
45
+ * "tag-page" head emitter, say), the right move is to extract a
46
+ * shared `composeHreflangAndCanonical(...)` helper at that point;
47
+ * doing it now would be premature against the YAGNI line.
48
+ *
49
+ * # The `routePath` argument
50
+ *
51
+ * The blog-post page manifest's `routePath` is `/blog/[slug]` (Astro
52
+ * route-parameter syntax). At render time the consumer's `[slug].astro`
53
+ * has already resolved the slug, so we accept the FULLY-SUBSTITUTED
54
+ * locale-neutral path (e.g. `/blog/why-context-beats-content`) here.
55
+ * `buildLocalizedRoutePath` then handles the locale-prefix decision,
56
+ * mirroring how `WebsiteInternalLink` and `renderWebsitePageHeadHtml`
57
+ * handle their own routes — the locale-prefix rule never lives in two
58
+ * places.
59
+ *
60
+ * # Failure modes
61
+ *
62
+ * Throws nothing — all required frontmatter fields are guaranteed by
63
+ * `parseBlogPostFrontmatter` upstream (same Zod 4 schema). The
64
+ * optional `coverImageUrl` is the only branch: when absent, the
65
+ * Twitter card kind degrades to `summary` (text-only) and `og:image`
66
+ * is omitted entirely.
67
+ */
68
+ export interface RenderBlogPostHeadHtmlOptions {
69
+ /**
70
+ * Composed `WebsiteSiteContext` — the renderer reads
71
+ * `websiteConfig.{defaultLocale, locales, publicOrigin}` for the
72
+ * URL/locale surface and `rootConfig.seo.{siteName}` for the
73
+ * brand fallback. `seo.defaultOgImageUrl` is intentionally NOT
74
+ * consulted: blog posts that ship without a `coverImageUrl` get a
75
+ * smaller-card degradation rather than a generic site-wide image
76
+ * (the default OG image is meaningful for marketing pages but
77
+ * misleading for blog posts).
78
+ */
79
+ siteContext: WebsiteSiteContext;
80
+ /**
81
+ * The locale Astro resolved (with default-locale fallback) for the
82
+ * current request. Drives the `<html lang>` value upstream + the
83
+ * `og:locale` line and the canonical URL's locale-prefix decision.
84
+ */
85
+ locale: AvailableLanguage;
86
+ /**
87
+ * The fully-substituted locale-neutral route path for the post —
88
+ * typically `/blog/<slug>` with `<slug>` already resolved by the
89
+ * consumer's `[slug].astro` `getStaticPaths()`. Passing the
90
+ * substituted path (rather than the parameterized one) keeps the
91
+ * renderer agnostic of the consumer's route-parameter naming.
92
+ */
93
+ routePath: string;
94
+ /**
95
+ * Validated frontmatter — the result of `parseBlogPostFrontmatter`.
96
+ * Carrying the parsed (branded) shape here means an unvalidated
97
+ * `entry.data` cannot reach this renderer accidentally (the
98
+ * branded-type signature would reject a raw `unknown`).
99
+ */
100
+ frontmatter: BlogPostFrontmatter;
101
+ /**
102
+ * AI-training opt-out (D-AG3) — when `true`, the renderer emits
103
+ * `<meta name="robots" content="noai, noimageai">`. The sibling
104
+ * `_headers` entry is produced by `renderBlogPostNoaiHeaders()`
105
+ * (one knob, two channels — see the page-head renderer's
106
+ * companion).
107
+ *
108
+ * **Why a renderer-level option, not a frontmatter field**: the
109
+ * blog-post frontmatter schema (`BlogPostFrontmatterSchema`)
110
+ * doesn't yet expose a per-post AI-opt-out knob — when it does,
111
+ * the consumer's `[slug].astro` will read it from the parsed
112
+ * frontmatter and pass it through here. Keeping the option at
113
+ * the renderer surface NOW means a third-party blog-post catalog
114
+ * (campaigns, customer stories) can opt out without authoring a
115
+ * new frontmatter schema first.
116
+ */
117
+ noaiOptOut?: boolean;
118
+ }
119
+ export interface BuildBlogPostStructuredDataPayloadOptions {
120
+ frontmatter: BlogPostFrontmatter;
121
+ canonicalUrl: string;
122
+ siteOrigin: string;
123
+ siteName: string;
124
+ locale: AvailableLanguage;
125
+ }
126
+ export declare function buildBlogPostStructuredDataPayload(opts: BuildBlogPostStructuredDataPayloadOptions): WebsiteStructuredDataPayload;
127
+ export declare function renderBlogPostHeadHtml(opts: RenderBlogPostHeadHtmlOptions): string;
128
+ /** @wildo_source:part:end saas.website.blog.head-renderer */
129
+ //# sourceMappingURL=blog-post-head.renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blog-post-head.renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/blog-post-head.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAG9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sDAAsD,CAAC;AAChG,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,mEAAmE,CAAC;AAItH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAEH,MAAM,WAAW,6BAA6B;IAC5C;;;;;;;;;OASG;IACH,WAAW,EAAE,kBAAkB,CAAC;IAChC;;;;OAIG;IACH,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,WAAW,EAAE,mBAAmB,CAAC;IACjC;;;;;;;;;;;;;;;OAeG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,yCAAyC;IACxD,WAAW,EAAE,mBAAmB,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,wBAAgB,kCAAkC,CAChD,IAAI,EAAE,yCAAyC,GAC9C,4BAA4B,CAuB9B;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,6BAA6B,GAAG,MAAM,CAkIlF;AACD,6DAA6D"}