@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,293 @@
1
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
+
3
+ import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';
4
+ import type { BlogPostFrontmatter } from '../schemas/blog/blog-post-frontmatter.shared.schemas';
5
+ import type { WebsiteStructuredDataPayload } from '../schemas/structured-data/website-structured-data.shared.schemas';
6
+
7
+ import { escapeHtmlAttribute, escapeHtmlText } from './internal/html-escape';
8
+ import { renderPageStructuredData } from './structured-data.renderer';
9
+ import type { WebsiteSiteContext } from './website-site-context';
10
+
11
+ /**
12
+ * @wildo_source:part:start saas.website.blog.head-renderer facet:layer:astro facet:family:website
13
+ *
14
+ * `renderBlogPostHeadHtml(opts)` — sibling of `renderWebsitePageHeadHtml`
15
+ * that emits the SEO `<head>` block for an individual blog post,
16
+ * sourcing per-render copy from a validated `BlogPostFrontmatter`
17
+ * instead of the page manifest's `metaLabelKeys`.
18
+ *
19
+ * # Why a separate renderer (not an overload of the page-head renderer)
20
+ *
21
+ * The framework-mindset rule "every type / bucket / field must carry
22
+ * its semantic weight" pushes against overloading
23
+ * `renderWebsitePageHeadHtml` with a "blog-post mode" flag:
24
+ *
25
+ * - The two renderers have STRUCTURALLY different inputs.
26
+ * `renderWebsitePageHeadHtml` reads `manifest.metaLabelKeys.{titleKey,
27
+ * descriptionKey, ogImageUrl}` + the matching `labelPack` lookup;
28
+ * this renderer reads `frontmatter.{title, summary, coverImageUrl,
29
+ * publishedAt, authorRef, tags}` directly — no label-pack
30
+ * dependency at all (blog body copy is per-post, not
31
+ * per-locale-pack, by design).
32
+ * - The two renderers emit DIFFERENT meta surfaces.
33
+ * `og:type=website` vs. `og:type=article`; the article variant
34
+ * additionally emits `article:published_time`,
35
+ * `article:author`, and `article:tag` (one per `frontmatter.tags`
36
+ * entry). Folding both into one helper would require an
37
+ * `articleMeta?: { ... }` discriminated branch that carries no
38
+ * value when called from a non-blog page — exactly the kind of
39
+ * "convenience union" the framework rules forbid.
40
+ * - The blog-post route itself uses a parameterized `routePath`
41
+ * (`/blog/[slug]`) that needs the SLUG substituted at render time
42
+ * to produce the absolute canonical/hreflang URLs. Page-level
43
+ * routes are always literal — keeping the substitution in this
44
+ * renderer (rather than at the manifest layer) means the page
45
+ * manifest stays free of per-render concerns.
46
+ *
47
+ * Both renderers share the exact same hreflang/canonical/OG locale
48
+ * composition logic — duplicating ~30 lines of stable URL composition
49
+ * is the lesser evil compared to threading a generic options bag
50
+ * through both. If a third renderer ever lands (a "campaign-page" or
51
+ * "tag-page" head emitter, say), the right move is to extract a
52
+ * shared `composeHreflangAndCanonical(...)` helper at that point;
53
+ * doing it now would be premature against the YAGNI line.
54
+ *
55
+ * # The `routePath` argument
56
+ *
57
+ * The blog-post page manifest's `routePath` is `/blog/[slug]` (Astro
58
+ * route-parameter syntax). At render time the consumer's `[slug].astro`
59
+ * has already resolved the slug, so we accept the FULLY-SUBSTITUTED
60
+ * locale-neutral path (e.g. `/blog/why-context-beats-content`) here.
61
+ * `buildLocalizedRoutePath` then handles the locale-prefix decision,
62
+ * mirroring how `WebsiteInternalLink` and `renderWebsitePageHeadHtml`
63
+ * handle their own routes — the locale-prefix rule never lives in two
64
+ * places.
65
+ *
66
+ * # Failure modes
67
+ *
68
+ * Throws nothing — all required frontmatter fields are guaranteed by
69
+ * `parseBlogPostFrontmatter` upstream (same Zod 4 schema). The
70
+ * optional `coverImageUrl` is the only branch: when absent, the
71
+ * Twitter card kind degrades to `summary` (text-only) and `og:image`
72
+ * is omitted entirely.
73
+ */
74
+
75
+ export interface RenderBlogPostHeadHtmlOptions {
76
+ /**
77
+ * Composed `WebsiteSiteContext` — the renderer reads
78
+ * `websiteConfig.{defaultLocale, locales, publicOrigin}` for the
79
+ * URL/locale surface and `rootConfig.seo.{siteName}` for the
80
+ * brand fallback. `seo.defaultOgImageUrl` is intentionally NOT
81
+ * consulted: blog posts that ship without a `coverImageUrl` get a
82
+ * smaller-card degradation rather than a generic site-wide image
83
+ * (the default OG image is meaningful for marketing pages but
84
+ * misleading for blog posts).
85
+ */
86
+ siteContext: WebsiteSiteContext;
87
+ /**
88
+ * The locale Astro resolved (with default-locale fallback) for the
89
+ * current request. Drives the `<html lang>` value upstream + the
90
+ * `og:locale` line and the canonical URL's locale-prefix decision.
91
+ */
92
+ locale: AvailableLanguage;
93
+ /**
94
+ * The fully-substituted locale-neutral route path for the post —
95
+ * typically `/blog/<slug>` with `<slug>` already resolved by the
96
+ * consumer's `[slug].astro` `getStaticPaths()`. Passing the
97
+ * substituted path (rather than the parameterized one) keeps the
98
+ * renderer agnostic of the consumer's route-parameter naming.
99
+ */
100
+ routePath: string;
101
+ /**
102
+ * Validated frontmatter — the result of `parseBlogPostFrontmatter`.
103
+ * Carrying the parsed (branded) shape here means an unvalidated
104
+ * `entry.data` cannot reach this renderer accidentally (the
105
+ * branded-type signature would reject a raw `unknown`).
106
+ */
107
+ frontmatter: BlogPostFrontmatter;
108
+ /**
109
+ * AI-training opt-out (D-AG3) — when `true`, the renderer emits
110
+ * `<meta name="robots" content="noai, noimageai">`. The sibling
111
+ * `_headers` entry is produced by `renderBlogPostNoaiHeaders()`
112
+ * (one knob, two channels — see the page-head renderer's
113
+ * companion).
114
+ *
115
+ * **Why a renderer-level option, not a frontmatter field**: the
116
+ * blog-post frontmatter schema (`BlogPostFrontmatterSchema`)
117
+ * doesn't yet expose a per-post AI-opt-out knob — when it does,
118
+ * the consumer's `[slug].astro` will read it from the parsed
119
+ * frontmatter and pass it through here. Keeping the option at
120
+ * the renderer surface NOW means a third-party blog-post catalog
121
+ * (campaigns, customer stories) can opt out without authoring a
122
+ * new frontmatter schema first.
123
+ */
124
+ noaiOptOut?: boolean;
125
+ }
126
+
127
+ export interface BuildBlogPostStructuredDataPayloadOptions {
128
+ frontmatter: BlogPostFrontmatter;
129
+ canonicalUrl: string;
130
+ siteOrigin: string;
131
+ siteName: string;
132
+ locale: AvailableLanguage;
133
+ }
134
+
135
+ export function buildBlogPostStructuredDataPayload(
136
+ opts: BuildBlogPostStructuredDataPayloadOptions,
137
+ ): WebsiteStructuredDataPayload {
138
+ const { frontmatter, canonicalUrl, siteOrigin, siteName, locale } = opts;
139
+ return {
140
+ '@type': 'BlogPosting',
141
+ '@id': `${canonicalUrl}#blog-post`,
142
+ headline: frontmatter.title,
143
+ description: frontmatter.summary,
144
+ datePublished: frontmatter.publishedAt,
145
+ author: {
146
+ '@type': 'Person',
147
+ name: String(frontmatter.authorRef),
148
+ },
149
+ publisher: {
150
+ '@type': 'Organization',
151
+ name: siteName,
152
+ url: siteOrigin,
153
+ },
154
+ mainEntityOfPage: canonicalUrl,
155
+ url: canonicalUrl,
156
+ inLanguage: String(locale),
157
+ ...(frontmatter.coverImageUrl ? { image: frontmatter.coverImageUrl } : {}),
158
+ ...(frontmatter.tags.length > 0 ? { keywords: frontmatter.tags.join(', ') } : {}),
159
+ };
160
+ }
161
+
162
+ export function renderBlogPostHeadHtml(opts: RenderBlogPostHeadHtmlOptions): string {
163
+ const { siteContext, locale, routePath, frontmatter, noaiOptOut } = opts;
164
+ const { rootConfig, websiteConfig } = siteContext;
165
+
166
+ const title = frontmatter.title;
167
+ const description = frontmatter.summary;
168
+ const ogImageUrl = frontmatter.coverImageUrl;
169
+ const titleAttr = escapeHtmlAttribute(title);
170
+ const descriptionAttr = escapeHtmlAttribute(description);
171
+ const titleText = escapeHtmlText(title);
172
+
173
+ const siteOrigin = websiteConfig.publicOrigin;
174
+ const defaultLocale = websiteConfig.defaultLocale;
175
+ const currentLocaleStr = String(locale);
176
+
177
+ /**
178
+ * Same locale-prefix composition rule as `renderWebsitePageHeadHtml`
179
+ * — delegated to the shared `buildLocalizedRoutePath` helper so a
180
+ * future change to the prefix policy lands in ONE place and propagates
181
+ * consistently to internal links + page heads + blog-post heads.
182
+ */
183
+ function buildAbsoluteUrl(forLocale: AvailableLanguage): string {
184
+ const path = buildLocalizedRoutePath({
185
+ locale: forLocale,
186
+ defaultLocale,
187
+ routePath,
188
+ });
189
+ return `${siteOrigin}${path}`;
190
+ }
191
+
192
+ const canonicalUrl = buildAbsoluteUrl(locale);
193
+
194
+ const hreflangLines: string[] = [];
195
+ for (const entry of websiteConfig.locales) {
196
+ const localeStr = String(entry.locale);
197
+ hreflangLines.push(
198
+ `<link rel="alternate" hreflang="${escapeHtmlAttribute(localeStr)}" href="${escapeHtmlAttribute(buildAbsoluteUrl(entry.locale))}" />`,
199
+ );
200
+ }
201
+ hreflangLines.push(
202
+ `<link rel="alternate" hreflang="x-default" href="${escapeHtmlAttribute(buildAbsoluteUrl(defaultLocale))}" />`,
203
+ );
204
+
205
+ const ogImageLines: string[] = [];
206
+ if (ogImageUrl !== undefined) {
207
+ const ogImageAttr = escapeHtmlAttribute(ogImageUrl);
208
+ ogImageLines.push(
209
+ `<meta property="og:image" content="${ogImageAttr}" />`,
210
+ `<meta name="twitter:image" content="${ogImageAttr}" />`,
211
+ );
212
+ }
213
+ const twitterCardKind = ogImageUrl !== undefined ? 'summary_large_image' : 'summary';
214
+
215
+ const ogLocaleAlternateLines: string[] = [];
216
+ for (const entry of websiteConfig.locales) {
217
+ const localeStr = String(entry.locale);
218
+ if (localeStr === currentLocaleStr) continue;
219
+ ogLocaleAlternateLines.push(
220
+ `<meta property="og:locale:alternate" content="${escapeHtmlAttribute(localeStr)}" />`,
221
+ );
222
+ }
223
+
224
+ /**
225
+ * `og:article:*` block — the per-post metadata that distinguishes
226
+ * the blog-post head from the page head. The Open Graph "article"
227
+ * vertical defines:
228
+ * - `article:published_time` (ISO-8601 — already validated by
229
+ * the frontmatter schema's `z.iso.datetime`).
230
+ * - `article:author` (free text or URL — we emit the author
231
+ * ref as a free-text string; a future author-registry
232
+ * resolution would emit the author's profile URL instead).
233
+ * - `article:tag` (one tag per emission; the OG spec uses
234
+ * repeated `<meta property="article:tag">` lines, NOT a
235
+ * comma-joined list).
236
+ *
237
+ * `article:section` (the OG "section" field, e.g. "Engineering" /
238
+ * "Product") is intentionally NOT emitted — Wonder Todos has no
239
+ * blog-section taxonomy yet, and emitting an empty section would
240
+ * be worse than omission.
241
+ */
242
+ const articleMetaLines: string[] = [
243
+ `<meta property="article:published_time" content="${escapeHtmlAttribute(frontmatter.publishedAt)}" />`,
244
+ `<meta property="article:author" content="${escapeHtmlAttribute(frontmatter.authorRef as unknown as string)}" />`,
245
+ ];
246
+ for (const tag of frontmatter.tags) {
247
+ articleMetaLines.push(
248
+ `<meta property="article:tag" content="${escapeHtmlAttribute(tag as unknown as string)}" />`,
249
+ );
250
+ }
251
+
252
+ const siteNameAttr = escapeHtmlAttribute(rootConfig.seo.siteName);
253
+ const canonicalAttr = escapeHtmlAttribute(canonicalUrl);
254
+ const ogLocaleAttr = escapeHtmlAttribute(currentLocaleStr);
255
+ const noaiMetaLines: string[] = [];
256
+ if (noaiOptOut === true) {
257
+ noaiMetaLines.push(`<meta name="robots" content="noai, noimageai" />`);
258
+ }
259
+
260
+ const structuredDataHtml = renderPageStructuredData({
261
+ pagePayloads: [
262
+ buildBlogPostStructuredDataPayload({
263
+ frontmatter,
264
+ canonicalUrl,
265
+ siteOrigin,
266
+ siteName: rootConfig.seo.siteName,
267
+ locale,
268
+ }),
269
+ ],
270
+ });
271
+
272
+ return [
273
+ `<title>${titleText}</title>`,
274
+ `<meta name="description" content="${descriptionAttr}" />`,
275
+ `<link rel="canonical" href="${canonicalAttr}" />`,
276
+ `<meta property="og:title" content="${titleAttr}" />`,
277
+ `<meta property="og:description" content="${descriptionAttr}" />`,
278
+ `<meta property="og:type" content="article" />`,
279
+ `<meta property="og:url" content="${canonicalAttr}" />`,
280
+ `<meta property="og:site_name" content="${siteNameAttr}" />`,
281
+ `<meta property="og:locale" content="${ogLocaleAttr}" />`,
282
+ ...ogLocaleAlternateLines,
283
+ ...ogImageLines,
284
+ `<meta name="twitter:card" content="${twitterCardKind}" />`,
285
+ `<meta name="twitter:title" content="${titleAttr}" />`,
286
+ `<meta name="twitter:description" content="${descriptionAttr}" />`,
287
+ ...articleMetaLines,
288
+ ...noaiMetaLines,
289
+ structuredDataHtml,
290
+ ...hreflangLines,
291
+ ].join('\n');
292
+ }
293
+ /** @wildo_source:part:end saas.website.blog.head-renderer */
@@ -0,0 +1,270 @@
1
+ import type { ComponentType, FunctionComponent, ReactNode } from 'react';
2
+
3
+ import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
4
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
5
+
6
+ import {
7
+ WebsiteRuntimeContextProvider,
8
+ type WebsiteRuntimeContextValue,
9
+ } from '../core/contexts/WebsiteRuntimeContext';
10
+ import { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';
11
+ import { WebsitePageLayout } from '../core/layouts/WebsitePageLayout';
12
+ import type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
13
+ import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
14
+ import type { WebsiteNavigationConfig } from '../schemas/manifests/website-root-config.shared.schemas';
15
+ import type { WebsitePageRef } from '../schemas/refs/page-ref.schemas';
16
+ import type { LabelPack } from '../schemas/validators/label-pack.validator';
17
+
18
+ /**
19
+ * @wildo_source:part:start saas.website.astro.bridge facet:layer:astro facet:family:website
20
+ *
21
+ * `createWebsitePageBridgeIsland(deps)` — factory that returns the
22
+ * React island a consumer's per-page `.astro` file mounts with a
23
+ * `client:idle` (or `client:load`) directive.
24
+ *
25
+ * **The hard architectural constraint this design solves**: Astro
26
+ * serializes React-island props via `devalue`-style JSON to transfer
27
+ * them across the SSR → client-hydration boundary. Function values —
28
+ * including `ComponentType` references — are NOT serializable. Any
29
+ * design that tried to pass `pageComponent`, `headerComponent`, or
30
+ * `footerComponent` AS island props would render correctly server-side
31
+ * (the function ref is in-process during SSR) and then mismatch on
32
+ * first hydration (the client receives `undefined`/broken placeholders
33
+ * for the function-typed fields). The Vitest suite would not catch
34
+ * this because Vitest renders React in a single process — there is no
35
+ * serialization boundary in unit tests.
36
+ *
37
+ * **The factory pattern fixes the constraint by closing over** the
38
+ * `pageComponent`, `pageRef`, and `navigation` (header / footer
39
+ * component refs) at MODULE LOAD TIME on the consumer side. The
40
+ * returned island accepts only PRIMITIVE-VALUED props (`locale`,
41
+ * `labelPack`, `designTokens`) — all JSON-serializable, all safe to
42
+ * cross the hydration boundary. The component refs never appear in
43
+ * the prop signature; they live in the lexical closure that the
44
+ * client-side bundle includes naturally as part of the island's
45
+ * compiled module.
46
+ *
47
+ * **Authoring pattern (per-page bridge module)**:
48
+ *
49
+ * ```tsx
50
+ * // consumer's src/pages-impl/landing.bridge.tsx
51
+ * import { createWebsitePageBridgeIsland } from '@wildo-ai/saas-website/astro-island';
52
+ * import { LandingPage, LANDING_PAGE_MANIFEST } from './landing.page';
53
+ * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';
54
+ *
55
+ * export const LandingBridgeIsland = createWebsitePageBridgeIsland({
56
+ * PageComponent: LandingPage,
57
+ * pageRef: LANDING_PAGE_MANIFEST.ref,
58
+ * navigation: WEBSITE_ROOT_CONFIG.navigation,
59
+ * });
60
+ * ```
61
+ *
62
+ * ```astro
63
+ * ---
64
+ * // consumer's src/pages/index.astro
65
+ * import {
66
+ * loadWebsitePageRuntime,
67
+ * renderWebsitePageHeadHtml,
68
+ * renderWebsiteDesignTokensCss,
69
+ * } from '@wildo-ai/saas-website/astro';
70
+ * import { LandingBridgeIsland } from '../pages-impl/landing.bridge';
71
+ * import { LANDING_PAGE_MANIFEST } from '../pages-impl/landing.page';
72
+ * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';
73
+ *
74
+ * const runtime = loadWebsitePageRuntime({
75
+ * manifest: LANDING_PAGE_MANIFEST,
76
+ * rootConfig: WEBSITE_ROOT_CONFIG,
77
+ * labelPacksDir: './src/i18n',
78
+ * astroLocale: Astro.currentLocale,
79
+ * });
80
+ * const headHtml = renderWebsitePageHeadHtml({
81
+ * manifest: LANDING_PAGE_MANIFEST,
82
+ * rootConfig: WEBSITE_ROOT_CONFIG,
83
+ * locale: runtime.locale,
84
+ * labelPack: runtime.labelPack,
85
+ * });
86
+ * const designTokensCss = renderWebsiteDesignTokensCss(
87
+ * WEBSITE_ROOT_CONFIG.designTokens,
88
+ * );
89
+ * ---
90
+ * <!doctype html>
91
+ * <html lang={runtime.locale}>
92
+ * <head>
93
+ * <Fragment set:html={headHtml} />
94
+ * <style set:html={designTokensCss}></style>
95
+ * </head>
96
+ * <body>
97
+ * <LandingBridgeIsland
98
+ * client:idle
99
+ * locale={runtime.locale}
100
+ * labelPack={runtime.labelPack}
101
+ * designTokens={WEBSITE_ROOT_CONFIG.designTokens}
102
+ * />
103
+ * </body>
104
+ * </html>
105
+ * ```
106
+ *
107
+ * **Why a per-page bridge file** (rather than constructing the island
108
+ * inline in the consumer's `.astro` frontmatter):
109
+ *
110
+ * Astro's `client:*` directive requires the JSX tag to reference a
111
+ * statically-resolvable module export at compile time — that is how
112
+ * Astro knows which module to bundle for client-side hydration. A
113
+ * factory call inside `.astro` frontmatter (`const Island =
114
+ * createWebsitePageBridgeIsland({...}); <Island client:idle/>`) does
115
+ * NOT round-trip through Astro's module-graph analyzer: Astro would
116
+ * see a runtime-constructed component reference and fail to bundle
117
+ * the closure for the client. The per-page bridge file gives Astro a
118
+ * static export to bind the directive to. The cost is one ~6-line
119
+ * file per page; the win is the only architecture that is safe
120
+ * against the H-2 hydration-mismatch failure mode AND uses Astro's
121
+ * native bundling pipeline.
122
+ *
123
+ * **Why `ComponentType` refs in `Deps` and not in `Props`**: this is
124
+ * the entire point of the redesign. `Deps` is consumed at module load
125
+ * time on the consumer side — the closure traps the function refs
126
+ * inside the returned component. `Props` is consumed at island-mount
127
+ * time and crosses Astro's serialization boundary. Keeping the two
128
+ * concerns lexically separate makes the boundary visible in the type
129
+ * signature.
130
+ *
131
+ * **Boundary** (D12 client-bundle discipline): this module ships into
132
+ * the React-island bundle. It MUST stay clear of `node:*` imports,
133
+ * fs / path helpers, and the SSR-tier specifiers
134
+ * (`axios`, `jsonwebtoken`, …). The bundle-isolation test catches
135
+ * regressions both globally (closure-wide allow-list) AND with the
136
+ * targeted Phase-5 negative test that asserts `node:fs` / `node:path`
137
+ * reach this island closure ONLY through `astro.ts`, never through
138
+ * `index.ts` or `companion-exports.ts`.
139
+ */
140
+
141
+ /**
142
+ * Module-load-time dependencies — closed over inside the returned
143
+ * island. NEVER cross the hydration boundary as serialized props.
144
+ */
145
+ export interface CreateWebsitePageBridgeIslandDeps {
146
+ /**
147
+ * The page's React component. Typically the same value that was
148
+ * passed to `defineWebsitePageManifest({ pageComponent })` — the
149
+ * authoring convention re-imports it here to keep the closure
150
+ * explicit and the per-page bridge file self-contained.
151
+ *
152
+ * Typed as `ComponentType<any>` to mirror
153
+ * `WebsitePageManifest.pageComponent` and dodge React's `ComponentType`
154
+ * contravariance: a narrower `ComponentType<TProps>` returned by
155
+ * `defineWebsitePageManifest<TProps>` would not be assignable to a
156
+ * `ComponentType<unknown>` slot, blocking every per-page bridge
157
+ * call site. The component is invoked with no props at the
158
+ * `<PageComponent />` JSX call site below, so widening here is
159
+ * runtime-safe.
160
+ */
161
+ PageComponent: ComponentType<any>;
162
+ /**
163
+ * The active page's ref. Carried as a primitive (string-branded)
164
+ * value so the inner `WebsitePageLayout` can register the
165
+ * `WebsitePageContext` without re-resolving it from the manifest.
166
+ */
167
+ pageRef: WebsitePageRef;
168
+ /**
169
+ * Site-wide navigation slot bindings (header / footer components).
170
+ * Closed over here rather than passed as island props because both
171
+ * fields are `ComponentType` refs that cannot survive Astro's
172
+ * serialization boundary.
173
+ */
174
+ navigation: WebsiteNavigationConfig;
175
+ }
176
+
177
+ /**
178
+ * Hydration-boundary props — what the consumer's `.astro` page passes
179
+ * to `<LandingBridgeIsland client:idle ...{primitiveProps}/>`. Every
180
+ * field is JSON-serializable.
181
+ */
182
+ export interface WebsitePageBridgeIslandProps {
183
+ /** The locale Astro resolved (with default-locale fallback). */
184
+ locale: AvailableLanguage;
185
+ /**
186
+ * The site's default locale. Carried as an island prop (not closed
187
+ * over in `Deps`) because it is JSON-primitive and consumers
188
+ * already pass `runtime.locale` from the per-locale Astro page
189
+ * frontmatter — passing the matching `runtime.defaultLocale` from
190
+ * the same `loadWebsitePageRuntime(...)` return value keeps the
191
+ * authoring story symmetric. Required for `WebsiteInternalLink`
192
+ * (Phase 6 fix H-β) which needs both locales to compute the
193
+ * correct prefix.
194
+ */
195
+ defaultLocale: AvailableLanguage;
196
+ /** Already-loaded + already-shape-validated label pack. */
197
+ labelPack: LabelPack;
198
+ /** Design-token bundle from `WebsiteRootConfig.designTokens`. */
199
+ designTokens: WebsiteDesignTokens;
200
+ /**
201
+ * Fully-composed label key the framework reads to render the
202
+ * "Skip to main content" affordance (Phase 6 third audit fix M-α).
203
+ * Mirrors `WebsiteRootConfig.skipToMainContentLabelKey`. Carried as
204
+ * a JSON-serializable string prop (NOT closed over in `Deps`)
205
+ * because it is a primitive value the consumer already has on the
206
+ * root config in the `.astro` frontmatter — passing it as an island
207
+ * prop keeps the per-page bridge module dependency-free of the
208
+ * site root config (the bridge stays page-local; only the per-page
209
+ * `.astro` page knows about `WEBSITE_ROOT_CONFIG`).
210
+ */
211
+ skipToMainContentLabelKey: WebsiteLabelKey;
212
+ /** Optional website-facing public providers materialized at build time. */
213
+ frontendProviders?: FrontendProvidersBlock;
214
+ /**
215
+ * Optional backend API origin (no trailing slash) forwarded into
216
+ * `WebsiteRuntimeContext.apiOrigin` for anonymous-capture sections
217
+ * (lead / draft-note forms) to construct their
218
+ * `WebsiteAnonymousSessionClient`. A JSON-serializable string, so it
219
+ * crosses the hydration boundary as an island prop; the `.astro` page
220
+ * resolves the per-environment value from
221
+ * `WildoMarketingWebsiteConfig.apiOrigin`. Omitted on pages with no
222
+ * capture section.
223
+ */
224
+ apiOrigin?: string;
225
+ }
226
+
227
+ /**
228
+ * Returns a FUNCTION component, not the wider `ComponentType` union. The island IS a function
229
+ * component — Astro hydrates nothing else here — and the union is not callable, so the wider
230
+ * declaration cost callers the ability to invoke the island directly. Mirrors
231
+ * `createWebsiteBlogPostBridgeIsland`, whose suite depends on that call. `FunctionComponent` is
232
+ * assignable to `ComponentType`, so no consumer narrows.
233
+ */
234
+ export function createWebsitePageBridgeIsland(
235
+ deps: CreateWebsitePageBridgeIslandDeps,
236
+ ): FunctionComponent<WebsitePageBridgeIslandProps> {
237
+ const { PageComponent, pageRef, navigation } = deps;
238
+
239
+ function WebsitePageBridgeIsland({
240
+ locale,
241
+ defaultLocale,
242
+ labelPack,
243
+ designTokens,
244
+ skipToMainContentLabelKey,
245
+ frontendProviders,
246
+ apiOrigin,
247
+ }: WebsitePageBridgeIslandProps): ReactNode {
248
+ const runtimeValue: WebsiteRuntimeContextValue = {
249
+ locale,
250
+ defaultLocale,
251
+ labelPack,
252
+ designTokens,
253
+ navigation,
254
+ skipToMainContentLabelKey,
255
+ frontendProviderRegistry: createFrontendWebsiteProviderRegistry(frontendProviders),
256
+ apiOrigin,
257
+ };
258
+ return (
259
+ <WebsiteRuntimeContextProvider value={runtimeValue}>
260
+ <WebsitePageLayout pageRef={pageRef}>
261
+ <PageComponent />
262
+ </WebsitePageLayout>
263
+ </WebsiteRuntimeContextProvider>
264
+ );
265
+ }
266
+ WebsitePageBridgeIsland.displayName = 'WebsitePageBridgeIsland';
267
+
268
+ return WebsitePageBridgeIsland;
269
+ }
270
+ /** @wildo_source:part:end saas.website.astro.bridge */
@@ -0,0 +1,113 @@
1
+ import type { WebsiteSectionDefinition } from '../schemas/sections/website-section-definition.shared.schemas';
2
+ import type { WebsiteSectionRef } from '../schemas/refs/section-ref.schemas';
3
+ import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
4
+ import type { ExpectedLabelKeysBySectionRef } from '../schemas/validators/label-pack.validator';
5
+
6
+ /**
7
+ * @wildo_source:part:start saas.website.astro.label-key-collector facet:layer:astro facet:family:website
8
+ *
9
+ * `collectExpectedLabelKeysFromSections(sections)` — pure, no-I/O
10
+ * collector that materializes the
11
+ * `ExpectedLabelKeysBySectionRef` map the label-pack validator
12
+ * (`validateLabelPackAgainstManifest`) consumes.
13
+ *
14
+ * **Why an explicit-list collector and not a JSX walker**
15
+ * (Phase 5 plan originally proposed walking each manifest's
16
+ * `pageComponent` to discover its `defineWebsiteSection` calls —
17
+ * we deliberately rejected that approach):
18
+ *
19
+ * - **No runtime React introspection.** A JSX walker would either
20
+ * (a) require booting React + a virtual DOM at build time just to
21
+ * render every page once and observe its section composition, or
22
+ * (b) require static AST analysis of the `pageComponent` source
23
+ * file to discover `<HeroSection/>`-style imports. Both add a
24
+ * non-trivial dependency surface (jsdom or a TS AST walker) and
25
+ * both fail silently on dynamic composition (a section rendered
26
+ * conditionally by a feature flag is invisible to the static
27
+ * walker but visible to the renderer, and vice versa).
28
+ * - **The consumer already has the list.** Each page-impl module
29
+ * already imports its sections (that's how JSX renders them);
30
+ * `WebsiteRootConfig` already lists every page manifest; the
31
+ * consumer's `website-root.config.ts` is therefore one line away
32
+ * from also exporting a flat `[HeroSection, PricingSection, …]`
33
+ * array. Asking for that array trades a frame of build-time magic
34
+ * for a single hand-authored line and turns "did the build see
35
+ * this section?" into a TypeScript-checked import — a far better
36
+ * trade than a JSX walker.
37
+ * - **Section reuse across pages is naturally handled.** Two pages
38
+ * reusing the same `HeroSection` definition (with the same
39
+ * `sectionRef`) collide silently in a JSX walker (which would
40
+ * register the section twice with identical expected keys).
41
+ * Here, the array deduplicates per `sectionRef` (a same-ref
42
+ * duplicate with NON-matching `expectedLabelKeys` is a hard
43
+ * authoring bug — see refinement below).
44
+ *
45
+ * **Refinement — duplicate `sectionRef` with non-matching keys**:
46
+ * if the same `sectionRef` appears twice with different
47
+ * `expectedLabelKeys`, the collector throws. This catches the
48
+ * "two distinct sections accidentally using the same ref" bug
49
+ * (typically a copy-paste during section authoring) at build time
50
+ * with an actionable message rather than letting the second
51
+ * `Map.set` silently overwrite the first.
52
+ *
53
+ * **Pure / no I/O**: the collector is invoked at build start by
54
+ * the Astro adapter and at audit time by `wildo website audit`;
55
+ * it does not read files and does not boot React.
56
+ *
57
+ * @param sections Every `WebsiteSectionDefinition` registered on the
58
+ * site. Order does not matter — the result is an
59
+ * unordered map keyed by `sectionRef`.
60
+ * @returns A `Map<WebsiteSectionRef, readonly WebsiteLabelKey[]>`
61
+ * ready to feed into `validateLabelPackAgainstManifest`.
62
+ * @throws When two distinct sections share a `sectionRef` but
63
+ * declare different `expectedLabelKeys` arrays (length or
64
+ * membership) — the collector's hard-error path.
65
+ */
66
+ export function collectExpectedLabelKeysFromSections(
67
+ sections: readonly WebsiteSectionDefinition[],
68
+ ): ExpectedLabelKeysBySectionRef {
69
+ const map = new Map<WebsiteSectionRef, readonly WebsiteLabelKey[]>();
70
+
71
+ for (const section of sections) {
72
+ const existing = map.get(section.sectionRef);
73
+ if (existing === undefined) {
74
+ map.set(section.sectionRef, section.expectedLabelKeys);
75
+ continue;
76
+ }
77
+ if (!areLabelKeyArraysEqual(existing, section.expectedLabelKeys)) {
78
+ throw new Error(
79
+ `[collectExpectedLabelKeysFromSections] Duplicate sectionRef ` +
80
+ `"${section.sectionRef as unknown as string}" registered with ` +
81
+ `non-matching expectedLabelKeys. ` +
82
+ `First registration: [${(existing as readonly string[]).join(', ')}]. ` +
83
+ `Second registration: [${(section.expectedLabelKeys as readonly string[]).join(', ')}]. ` +
84
+ `Either rename one section's sectionRef or align both expectedLabelKeys arrays.`,
85
+ );
86
+ }
87
+ }
88
+
89
+ return map;
90
+ }
91
+
92
+ /**
93
+ * Order-insensitive equality for two `readonly WebsiteLabelKey[]`.
94
+ * Used by the collector to validate that a duplicate-ref registration
95
+ * is intentional (same keys) vs. a copy-paste bug (different keys).
96
+ *
97
+ * Quadratic in the array length, but `expectedLabelKeys` is bounded by
98
+ * the number of slots a single section consumes (typically <20), so
99
+ * the cost is negligible compared to surface readability of the
100
+ * comparison.
101
+ */
102
+ function areLabelKeyArraysEqual(
103
+ a: readonly WebsiteLabelKey[],
104
+ b: readonly WebsiteLabelKey[],
105
+ ): boolean {
106
+ if (a.length !== b.length) return false;
107
+ const aSet = new Set(a as readonly string[]);
108
+ for (const key of b as readonly string[]) {
109
+ if (!aSet.has(key)) return false;
110
+ }
111
+ return true;
112
+ }
113
+ /** @wildo_source:part:end saas.website.astro.label-key-collector */