@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,322 @@
1
+ /**
2
+ * @wildo_source:part:start engine.saas-website.config facet:layer:engine facet:family:marketing-website
3
+ *
4
+ * Per-service marketing-website configuration block, authored in
5
+ * the website service folder as `wildo.website.config.ts`.
6
+ *
7
+ * # What this file's loaded value is used for TODAY
8
+ *
9
+ * This schema is the single source of truth for the marketing-site routing
10
+ * and origin surface. Every consumer reads here:
11
+ *
12
+ * - `defaultLocale` + `locales` — read by `buildAstroI18nConfig(...)`,
13
+ * `assertLabelPacksValid(...)`, `loadWebsitePageRuntime(...)` and
14
+ * `renderWebsitePageHeadHtml(...)` via the shared `WebsiteSiteContext`.
15
+ * - `publicOrigin` — read by the head renderer to compose absolute
16
+ * `<link rel="canonical">`, `og:url`, hreflang `href` URLs and
17
+ * the structured-data `mainEntityOfPage`. The previous
18
+ * `WebsiteRootConfig.seo.siteOrigin` was deleted.
19
+ * - `appOrigin` — has NO runtime consumer today. The
20
+ * marketing → app handoff link surface that will read it does
21
+ * not exist yet. Authored eagerly so the link-surface PR is a
22
+ * single framework-owned move; until then the field is exercised
23
+ * only by the schema parser.
24
+ * - `csp?` — read by the website-local `sync-nginx-csp` build step,
25
+ * which merges the app-static posture with `infra.csp?.<serviceKey>`,
26
+ * emits the generated artifact consumed by Astro pages, and rewrites
27
+ * the checked-in nginx marker file used by the website Docker image.
28
+ *
29
+ * Future companion subprocesses that introspect the marketing
30
+ * surface (locale-coverage audits, label-pack validation against
31
+ * authored locales, SEO-link sanity checks) will also read here —
32
+ * none exist today.
33
+ *
34
+ * # Why this is per-service website configuration
35
+ *
36
+ * Co-locating the schema with the engine package that owns its semantics
37
+ * keeps three concerns separate from application-root service declarations:
38
+ *
39
+ * 1. **Right reuse axis.** Marketing-site content (locales, SEO origins,
40
+ * future label-pack overrides, marketing-only CTAs) is conceptually
41
+ * different from saas-app-level service declarations (`path`,
42
+ * `serviceName`, `frontendType`, `sessionStorage`). The marketing
43
+ * config travels with the website service folder, NOT with the
44
+ * SaaS app root. Two unrelated lifecycles, two different review
45
+ * audiences.
46
+ * 2. **Engine boundary.** `platform-config-lib` no longer needs to
47
+ * depend on `@wildo-ai/presets-components-models` (the home of
48
+ * `AvailableLanguage`) just to type the marketing-site locale
49
+ * enum. Adding a new website primitive (a new SEO field, a new
50
+ * locale-pack id) belongs with the website package and its public
51
+ * configuration contract.
52
+ * 3. **Disambiguator hazard.** The previous root-level
53
+ * `marketingWebsite?` block silently assumed "one STATIC_WEBSITE
54
+ * service per SaaS app" — but nothing in the schema enforced it.
55
+ * Two STATIC_WEBSITE services would have silently shared one
56
+ * config block with no obvious "which one wins?" answer. The
57
+ * application configuration rejects more than one STATIC_WEBSITE (and
58
+ * TECH_DOC) service, so the per-service config-file lookup is
59
+ * deterministic by construction.
60
+ *
61
+ * # Schema decisions encoded here
62
+ *
63
+ * - `z.strictObject` so authoring an unknown key surfaces as a parse
64
+ * error at `defineWebsiteConfig` time instead of being silently
65
+ * dropped (same posture as `WildoSaasConfigSchema` and
66
+ * `WildoTechnicalDocConfigSchema`).
67
+ *
68
+ * - **No `enabled` field.** The gate is "is there a `wildo.website.config.ts`
69
+ * in a `frontendType: STATIC_WEBSITE` service folder?" — file
70
+ * presence under a STATIC_WEBSITE service IS the enable signal.
71
+ * The Astro build / dev companion / future audit CLI requires the
72
+ * file when it walks a STATIC_WEBSITE service folder, so a
73
+ * half-authored config simply cannot exist (the loader fails
74
+ * closed). Removing the boolean closes a foot-gun: an authored
75
+ * `enabled: false` block was previously a silent way to keep the
76
+ * config "warm" without shipping anything, and consumers had to
77
+ * remember to read the flag in addition to the file presence — a
78
+ * future read-the-config / forget-the-flag bug waiting to happen.
79
+ * A separate `services.<key>.enabled` pattern (orthogonal to
80
+ * marketing-website) is the right place for any "temporarily
81
+ * disable this service" story (deferred — see DASHBOARD).
82
+ *
83
+ * - URL fields are `z.url()` (Zod 4 native validator) so we get
84
+ * protocol + host parsing for free, matching the rest of the
85
+ * codebase.
86
+ *
87
+ * - `csp?` is typed against `WildoCspConfigSchema` from
88
+ * `@wildo-ai/platform-config-lib`, mirroring the docs service's
89
+ * `wildo.tech-doc.config.ts` contract. The parsed value is consumed by
90
+ * the shared `resolveCspPolicy()` / `emitCspArtifacts()` generator flow.
91
+ *
92
+ * - `defaultLocale ∈ locales` cross-check via `.superRefine` —
93
+ * keeping `defaultLocale` as a top-level field is product-explicit
94
+ * ("which locale URL is the canonical one") and decouples it from
95
+ * list ordering. Locale-path uniqueness AND locale-value
96
+ * uniqueness are also enforced — duplicate paths would silently
97
+ * overwrite each other in the Astro build and corrupt the sitemap;
98
+ * duplicate locales would produce duplicate hreflang entries.
99
+ *
100
+ * - Locale primitive is `AvailableLanguage`. The schema imports it
101
+ * from `@wildo-ai/presets-components-models/public-runtime` (the
102
+ * side-effect-free public-runtime seam), but **consumers authoring
103
+ * `wildo.website.config.ts` must import it from
104
+ * `@wildo-ai/saas-models/public-runtime`**. Both paths produce identical
105
+ * values; saas-models simply re-exports the presets atom. Picking
106
+ * one path consistently across consumer files prevents drift in
107
+ * docs / examples / search results. Values authored in
108
+ * `wildo.website.config.ts` are the lowercase BCP-47 strings
109
+ * (`'en'`, `'fr'`), NOT the UPPERCASE TS enum member names
110
+ * (`EN`, `FR`). `z.enum(AvailableLanguage)` parses against the
111
+ * enum VALUES, which is what we want.
112
+ */
113
+
114
+ import { WildoCspConfigSchema } from '@wildo-ai/platform-config-lib';
115
+ import { z } from 'zod';
116
+ import { AvailableLanguage } from '@wildo-ai/presets-components-models/public-runtime';
117
+
118
+ /**
119
+ * Per-locale configuration. Each authored locale gets one entry.
120
+ *
121
+ * `path` is the locale URL prefix segment (e.g. `'en'`, `'fr'`) — the
122
+ * Astro builder maps `/en/...` and `/fr/...` paths via this value. We
123
+ * do NOT auto-derive `path` from the locale string because some
124
+ * deployments may want a flat root for the default locale (`/...`
125
+ * instead of `/en/...`); keeping it explicit per locale leaves that
126
+ * door open without a config-shape change.
127
+ */
128
+ export const WebsiteLocaleConfigSchema = z.strictObject({
129
+ locale: z.enum(AvailableLanguage),
130
+ path: z
131
+ .string()
132
+ .min(1)
133
+ .regex(
134
+ /^[a-z0-9-]+$/,
135
+ 'locale path must be lowercase alphanumeric with hyphens (becomes a URL segment)',
136
+ ),
137
+ });
138
+ export type WebsiteLocaleConfig = z.infer<typeof WebsiteLocaleConfigSchema>;
139
+
140
+ /**
141
+ * Per-service marketing-website configuration block.
142
+ *
143
+ * Authored as `export default defineWebsiteConfig({...})` in
144
+ * `<website-service-root>/wildo.website.config.ts`. The Astro
145
+ * build loads this file once at bootstrap via jiti and composes
146
+ * a `WebsiteSiteContext`; helpers consume that composed context rather than
147
+ * this raw value.
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
152
+ * import { defineWebsiteConfig } from '@wildo-ai/saas-website';
153
+ *
154
+ * export default defineWebsiteConfig({
155
+ * publicOrigin: 'https://example.com',
156
+ * appOrigin: 'https://app.example.com',
157
+ * defaultLocale: AvailableLanguage.EN,
158
+ * locales: [
159
+ * { locale: AvailableLanguage.EN, path: 'en' },
160
+ * { locale: AvailableLanguage.FR, path: 'fr' },
161
+ * ],
162
+ * csp: { enabled: true, reportOnly: true },
163
+ * });
164
+ * ```
165
+ */
166
+ export const WildoMarketingWebsiteConfigSchema = z
167
+ .strictObject({
168
+ /**
169
+ * Public origin of the marketing site (apex / www) — canonical
170
+ * input the Astro head renderer emits as absolute URLs
171
+ * (`<link rel="canonical">`, `og:url`, hreflang `href`,
172
+ * sitemap base, structured-data `mainEntityOfPage`).
173
+ *
174
+ * MUST NOT end with `/` — the head renderer composes
175
+ * `${publicOrigin}${routePath}` and a trailing slash would emit
176
+ * `https://example.com//`. Surface the constraint loudly here
177
+ * rather than silently corrupting URLs at build time.
178
+ */
179
+ publicOrigin: z
180
+ .url()
181
+ .refine((value) => !value.endsWith('/'), {
182
+ error:
183
+ 'WildoMarketingWebsiteConfig.publicOrigin must NOT end with "/" (the head ' +
184
+ 'renderer concatenates publicOrigin + routePath; trailing slash would emit "//")',
185
+ }),
186
+
187
+ /**
188
+ * Origin of the SaaS application the marketing site links into
189
+ * (signup / login / pricing → checkout).
190
+ *
191
+ * **Today**: NO runtime consumer. The cross-origin handoff
192
+ * surface that will read it does not exist yet. Authored
193
+ * eagerly so the link-surface PR is a single framework-owned move
194
+ * — until then the field is exercised only by the schema
195
+ * parser.
196
+ *
197
+ * Distinct from the general `WildoSaasConfig.services.<name>`
198
+ * URL surface because the marketing site is a separate Astro
199
+ * project that does not load the full SaaS app graph at build
200
+ * time.
201
+ */
202
+ appOrigin: z.url(),
203
+
204
+ /**
205
+ * Backend API origin the marketing site POSTs to for anonymous
206
+ * captures (lead / draft-note forms) — e.g.
207
+ * `https://api.example.com`. Threaded through
208
+ * `WebsiteRuntimeContext.apiOrigin` into the React island that
209
+ * constructs the `WebsiteAnonymousSessionClient`.
210
+ *
211
+ * OPTIONAL because a pure brochure site with no capture forms never
212
+ * talks to an API — only sites that host an anonymous-capture
213
+ * section need it. When such a section renders without a resolved
214
+ * `apiOrigin`, the section surfaces a loud misconfiguration rather
215
+ * than silently POSTing nowhere.
216
+ *
217
+ * MUST NOT end with `/` — the anon client composes
218
+ * `${apiOrigin}${MAIN_API_BASE_PATH}${path}` and a trailing slash
219
+ * would emit a doubled `//` in the request URL. (The client also
220
+ * defensively strips trailing slashes, but the refine surfaces the
221
+ * mistake at authoring time.)
222
+ *
223
+ * Distinct from `appOrigin` (the human-facing app the marketing CTAs
224
+ * link into) and `publicOrigin` (the marketing site's own canonical
225
+ * origin): this is the machine-to-machine API host.
226
+ */
227
+ apiOrigin: z
228
+ .url()
229
+ .refine((value) => !value.endsWith('/'), {
230
+ error:
231
+ 'WildoMarketingWebsiteConfig.apiOrigin must NOT end with "/" (the anonymous ' +
232
+ 'client concatenates apiOrigin + MAIN_API_BASE_PATH + path; a trailing slash ' +
233
+ 'would emit "//")',
234
+ })
235
+ .optional(),
236
+
237
+ /**
238
+ * Optional Content-Security-Policy override for the marketing site.
239
+ * Consumed by the website-local `sync-nginx-csp` step, which emits
240
+ * the generated artifact for Astro page injection and rewrites the
241
+ * website nginx marker file from the same resolved output.
242
+ */
243
+ csp: WildoCspConfigSchema.optional(),
244
+
245
+ /**
246
+ * Default locale the marketing site serves for unmatched URLs
247
+ * and as the canonical hreflang signal. Must appear in
248
+ * `locales` below (cross-check enforced by the schema-level
249
+ * `.superRefine`).
250
+ *
251
+ * Read by `buildAstroI18nConfig(...)` (drives Astro's own
252
+ * `i18n.defaultLocale`) and by the head renderer (drives
253
+ * `<html lang="...">`, `og:locale`, hreflang `x-default`).
254
+ */
255
+ defaultLocale: z.enum(AvailableLanguage),
256
+
257
+ /**
258
+ * Ordered list of locales the marketing site builds. Order is
259
+ * preserved (used by the language switcher rendering and by
260
+ * the sitemap emitter — when the sitemap surface lands).
261
+ *
262
+ * Read by `buildAstroI18nConfig(...)` (Astro `i18n.locales`),
263
+ * by `assertLabelPacksValid(...)` (one label pack per locale),
264
+ * by `loadWebsitePageRuntime(...)` (per-page locale resolution),
265
+ * and by the head renderer (hreflang alternates).
266
+ */
267
+ locales: z.array(WebsiteLocaleConfigSchema).min(1),
268
+ })
269
+ .superRefine((config, ctx) => {
270
+ const declaredLocales = new Set(config.locales.map((entry) => entry.locale));
271
+ if (!declaredLocales.has(config.defaultLocale)) {
272
+ ctx.addIssue({
273
+ code: 'custom',
274
+ path: ['defaultLocale'],
275
+ message:
276
+ `defaultLocale '${config.defaultLocale}' is not declared in 'locales' ` +
277
+ `(declared: [${[...declaredLocales].join(', ')}]). The default locale ` +
278
+ `must be one of the locales the site actually builds.`,
279
+ });
280
+ }
281
+
282
+ // Locale-path uniqueness: two entries collapsing to the same URL
283
+ // segment would silently overwrite each other in the Astro build
284
+ // and corrupt the sitemap. Fail closed at parse time.
285
+ const pathOwners = new Map<string, AvailableLanguage>();
286
+ for (const entry of config.locales) {
287
+ const previous = pathOwners.get(entry.path);
288
+ if (previous !== undefined) {
289
+ ctx.addIssue({
290
+ code: 'custom',
291
+ path: ['locales'],
292
+ message:
293
+ `Locale path '${entry.path}' is declared by both '${previous}' and ` +
294
+ `'${entry.locale}'. Each locale must have a unique URL path segment.`,
295
+ });
296
+ continue;
297
+ }
298
+ pathOwners.set(entry.path, entry.locale);
299
+ }
300
+
301
+ // Locale-value uniqueness: same locale appearing twice would
302
+ // produce duplicate hreflang entries and ambiguous default-locale
303
+ // resolution.
304
+ const localeOwners = new Map<AvailableLanguage, string>();
305
+ for (const entry of config.locales) {
306
+ const previousPath = localeOwners.get(entry.locale);
307
+ if (previousPath !== undefined) {
308
+ ctx.addIssue({
309
+ code: 'custom',
310
+ path: ['locales'],
311
+ message:
312
+ `Locale '${entry.locale}' is declared twice (paths '${previousPath}' ` +
313
+ `and '${entry.path}'). Each locale must appear at most once.`,
314
+ });
315
+ continue;
316
+ }
317
+ localeOwners.set(entry.locale, entry.path);
318
+ }
319
+ });
320
+ export type WildoMarketingWebsiteConfig = z.infer<typeof WildoMarketingWebsiteConfigSchema>;
321
+ export type WildoMarketingWebsiteConfigInput = z.input<typeof WildoMarketingWebsiteConfigSchema>;
322
+ /** @wildo_source:part:end engine.saas-website.config */
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @wildo-package @wildo-ai/saas-website (Node-only config-loader entry)
3
+ *
4
+ * Dedicated subpath for the per-service `wildo.website.config.ts`
5
+ * loader (`saas-website-slice.md` Step 7.5 — mirrors the
6
+ * `saas-technical-doc.md` Step 7.5 ship). Consumers:
7
+ *
8
+ * - `examples/wonder-todos/website/astro.config.ts` — wires the
9
+ * loader into the Astro build's `i18n` config.
10
+ * - Future companion subprocesses that introspect the marketing
11
+ * surface (locale coverage audits, SEO-link sanity checks).
12
+ *
13
+ * @wildo-boundary
14
+ * This entry is on the **Node-only** side of the bundle boundary.
15
+ * It re-exports `loadWebsiteConfig` (which transitively pulls
16
+ * `loadConfig` from `@wildo-ai/platform-config-lib` — jiti-backed,
17
+ * uses `node:fs` / `node:path` / `node:crypto`) and is therefore
18
+ * carved into its own package subpath so it cannot leak into the
19
+ * browser-side React-island bundle.
20
+ *
21
+ * Enforced by `engine/saas-website/src/__tests__/bundle-isolation.test.ts`:
22
+ * - This file IS in `ENTRY_FILES` (so the closure walker covers it).
23
+ * - `@wildo-ai/platform-config-lib` is in `ALLOWED_BARE_SPECIFIERS`
24
+ * AND `SSR_ONLY_BARE_SPECIFIERS`, which together prove the
25
+ * specifier reaches the closure ONLY through this entry — never
26
+ * through `index.ts`, `companion-exports.ts`, or `astro-island.ts`.
27
+ *
28
+ * Re-exporting `loadWebsiteConfig` from `index.ts` would (a) drag
29
+ * `@wildo-ai/platform-config-lib`'s heavy graph into the marketing-
30
+ * site browser bundle and (b) trip the SSR-tier negative test
31
+ * above — both fail-loud signals that this subpath separation
32
+ * exists for a reason.
33
+ */
34
+
35
+ export * from './config/load-website-config';
@@ -0,0 +1,184 @@
1
+ import React from 'react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { render } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+ import { BUILTIN_PROVIDER_CAPABILITY } from '@wildo-ai/saas-models/public-runtime';
6
+
7
+ import { WebsitePageLayout } from '../layouts/WebsitePageLayout';
8
+ import {
9
+ WebsiteRuntimeContextProvider,
10
+ type WebsiteRuntimeContextValue,
11
+ } from '../contexts/WebsiteRuntimeContext';
12
+ import { useWebsitePage } from '../contexts/useWebsitePage';
13
+ import { WebsiteLabelKeySchema } from '../../schemas/label-keys/website-label-key.schemas';
14
+ import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
15
+
16
+ const RuntimeHeader = (): React.ReactElement => <span data-testid="runtime-header">RH</span>;
17
+ const RuntimeFooter = (): React.ReactElement => <span data-testid="runtime-footer">RF</span>;
18
+
19
+ const SKIP_KEY = WebsiteLabelKeySchema.parse('website.chrome.layout.skip-to-main-content');
20
+
21
+ const RUNTIME: WebsiteRuntimeContextValue = {
22
+ locale: AvailableLanguage.EN,
23
+ defaultLocale: AvailableLanguage.EN,
24
+ labelPack: { 'website.chrome.layout.skip-to-main-content': 'Skip to main content' },
25
+ designTokens: {
26
+ colors: {
27
+ background: '#fff', foreground: '#000', primary: '#000',
28
+ primaryForeground: '#fff', accent: '#000', accentForeground: '#fff',
29
+ surface: '#fff', surfaceForeground: '#000', border: '#ccc',
30
+ muted: '#eee', mutedForeground: '#666',
31
+ },
32
+ radii: { none: '0', sm: '2px', md: '4px', lg: '8px', xl: '12px', full: '9999px' },
33
+ },
34
+ navigation: {
35
+ headerComponent: RuntimeHeader,
36
+ footerComponent: RuntimeFooter,
37
+ },
38
+ skipToMainContentLabelKey: SKIP_KEY,
39
+ };
40
+
41
+ const RUNTIME_WITH_FRONTEND_PROVIDERS: WebsiteRuntimeContextValue = {
42
+ ...RUNTIME,
43
+ frontendProviderRegistry: {
44
+ modules: new Map(),
45
+ getProviderByRef() {
46
+ return undefined;
47
+ },
48
+ getProvidersByProviderCapability() {
49
+ return [];
50
+ },
51
+ findProviderByProviderCapability(providerCapability) {
52
+ switch (providerCapability) {
53
+ case BUILTIN_PROVIDER_CAPABILITY.FRONTEND_PRODUCT_ANALYTICS:
54
+ return {
55
+ metadata: { ref: 'posthog' },
56
+ providerCapabilities: [BUILTIN_PROVIDER_CAPABILITY.FRONTEND_PRODUCT_ANALYTICS],
57
+ protocols: ['FRONTEND_SDK'],
58
+ publicConfig: { integrationKind: 'product-analytics' },
59
+ };
60
+ case BUILTIN_PROVIDER_CAPABILITY.FRONTEND_ERROR_MONITORING:
61
+ return {
62
+ metadata: { ref: 'sentry' },
63
+ providerCapabilities: [BUILTIN_PROVIDER_CAPABILITY.FRONTEND_ERROR_MONITORING],
64
+ protocols: ['FRONTEND_SDK'],
65
+ publicConfig: { integrationKind: 'browser-error-monitoring' },
66
+ };
67
+ case BUILTIN_PROVIDER_CAPABILITY.FRONTEND_CAPTCHA:
68
+ return {
69
+ metadata: { ref: 'recaptcha' },
70
+ providerCapabilities: [BUILTIN_PROVIDER_CAPABILITY.FRONTEND_CAPTCHA],
71
+ protocols: ['FRONTEND_SDK'],
72
+ publicConfig: { integrationKind: 'captcha' },
73
+ };
74
+ default:
75
+ return undefined;
76
+ }
77
+ },
78
+ },
79
+ };
80
+
81
+ const wrap = (children: React.ReactNode) => (
82
+ <WebsiteRuntimeContextProvider value={RUNTIME}>{children}</WebsiteRuntimeContextProvider>
83
+ );
84
+
85
+ function PageRefProbe(): React.ReactElement {
86
+ const { pageRef } = useWebsitePage();
87
+ return <span data-testid="page-ref">{pageRef as unknown as string}</span>;
88
+ }
89
+
90
+ describe('WebsitePageLayout', () => {
91
+ it('provides the page ref to descendants and stamps the data attribute', () => {
92
+ const { getByTestId, container } = render(
93
+ wrap(
94
+ <WebsitePageLayout pageRef="landing">
95
+ <PageRefProbe />
96
+ </WebsitePageLayout>,
97
+ ),
98
+ );
99
+ expect(getByTestId('page-ref')).toHaveTextContent('landing');
100
+ expect(container.querySelector('[data-website-page-ref="landing"]')).not.toBeNull();
101
+ });
102
+
103
+ it('renders runtime navigation header + footer by default', () => {
104
+ const { getByTestId } = render(
105
+ wrap(
106
+ <WebsitePageLayout pageRef="landing">
107
+ <span>body</span>
108
+ </WebsitePageLayout>,
109
+ ),
110
+ );
111
+ expect(getByTestId('runtime-header')).toBeInTheDocument();
112
+ expect(getByTestId('runtime-footer')).toBeInTheDocument();
113
+ });
114
+
115
+ it('honors per-page header/footer overrides', () => {
116
+ const { getByTestId, queryByTestId } = render(
117
+ wrap(
118
+ <WebsitePageLayout
119
+ pageRef="landing"
120
+ header={<div data-testid="custom-header">CH</div>}
121
+ footer={<div data-testid="custom-footer">CF</div>}
122
+ >
123
+ <span>body</span>
124
+ </WebsitePageLayout>,
125
+ ),
126
+ );
127
+ expect(getByTestId('custom-header')).toBeInTheDocument();
128
+ expect(getByTestId('custom-footer')).toBeInTheDocument();
129
+ expect(queryByTestId('runtime-header')).toBeNull();
130
+ expect(queryByTestId('runtime-footer')).toBeNull();
131
+ });
132
+
133
+ it('suppresses chrome when header/footer are explicitly null', () => {
134
+ const { container } = render(
135
+ wrap(
136
+ <WebsitePageLayout pageRef="landing" header={null} footer={null}>
137
+ <span>body</span>
138
+ </WebsitePageLayout>,
139
+ ),
140
+ );
141
+ expect(container.querySelector('header')).toBeNull();
142
+ expect(container.querySelector('footer')).toBeNull();
143
+ });
144
+
145
+ it('uses semantic <main> for the body wrapper', () => {
146
+ const { container } = render(
147
+ wrap(
148
+ <WebsitePageLayout pageRef="landing">
149
+ <span data-testid="body">body</span>
150
+ </WebsitePageLayout>,
151
+ ),
152
+ );
153
+ const main = container.querySelector('main');
154
+ expect(main).not.toBeNull();
155
+ expect(main?.querySelector('[data-testid="body"]')).not.toBeNull();
156
+ });
157
+
158
+ it('stamps active website provider refs on the layout root when available', () => {
159
+ const { container } = render(
160
+ <WebsiteRuntimeContextProvider value={RUNTIME_WITH_FRONTEND_PROVIDERS}>
161
+ <WebsitePageLayout pageRef="landing">
162
+ <span>body</span>
163
+ </WebsitePageLayout>
164
+ </WebsiteRuntimeContextProvider>,
165
+ );
166
+ const layoutRoot = container.querySelector('[data-website-page-ref="landing"]');
167
+ expect(layoutRoot).not.toBeNull();
168
+ expect(layoutRoot).toHaveAttribute('data-website-product-analytics-provider-ref', 'posthog');
169
+ expect(layoutRoot).toHaveAttribute('data-website-error-monitoring-provider-ref', 'sentry');
170
+ expect(layoutRoot).toHaveAttribute('data-website-captcha-provider-ref', 'recaptcha');
171
+ });
172
+
173
+ it('rejects malformed pageRef at parse time', () => {
174
+ expect(() =>
175
+ render(
176
+ wrap(
177
+ <WebsitePageLayout pageRef="INVALID UPPERCASE">
178
+ <span />
179
+ </WebsitePageLayout>,
180
+ ),
181
+ ),
182
+ ).toThrowError(/WebsitePageRef/);
183
+ });
184
+ });
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { render } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ import { WebsiteSection } from '../layouts/WebsiteSection';
7
+ import { useWebsiteSection } from '../contexts/useWebsiteSection';
8
+ import { WebsiteSectionCategory } from '../../schemas/sections/website-section-category.shared';
9
+
10
+ function SectionRefProbe(): React.ReactElement {
11
+ const { sectionRef, category } = useWebsiteSection();
12
+ return (
13
+ <span data-testid="probe">
14
+ {(sectionRef as unknown as string)}|{category}
15
+ </span>
16
+ );
17
+ }
18
+
19
+ describe('WebsiteSection', () => {
20
+ it('renders a semantic <section> with data-section-ref + data-section-category', () => {
21
+ const { container } = render(
22
+ <WebsiteSection sectionRef="hero" category={WebsiteSectionCategory.HERO}>
23
+ <span>body</span>
24
+ </WebsiteSection>,
25
+ );
26
+ const section = container.querySelector('section');
27
+ expect(section).not.toBeNull();
28
+ expect(section?.getAttribute('data-section-ref')).toBe('hero');
29
+ expect(section?.getAttribute('data-section-category')).toBe('hero');
30
+ });
31
+
32
+ it('provides the section context to descendants', () => {
33
+ const { getByTestId } = render(
34
+ <WebsiteSection sectionRef="pricing" category={WebsiteSectionCategory.PRICING}>
35
+ <SectionRefProbe />
36
+ </WebsiteSection>,
37
+ );
38
+ expect(getByTestId('probe')).toHaveTextContent('pricing|pricing');
39
+ });
40
+
41
+ it('forwards id, className, aria-label, aria-labelledby', () => {
42
+ const { container } = render(
43
+ <WebsiteSection
44
+ sectionRef="cta"
45
+ category={WebsiteSectionCategory.CTA}
46
+ id="primary-cta"
47
+ className="my-section"
48
+ ariaLabel="Primary call to action"
49
+ ariaLabelledBy="cta-heading"
50
+ >
51
+ <span>body</span>
52
+ </WebsiteSection>,
53
+ );
54
+ const section = container.querySelector('section');
55
+ expect(section?.id).toBe('primary-cta');
56
+ expect(section?.className).toBe('my-section');
57
+ expect(section?.getAttribute('aria-label')).toBe('Primary call to action');
58
+ expect(section?.getAttribute('aria-labelledby')).toBe('cta-heading');
59
+ });
60
+
61
+ it('omits aria-* attributes when not provided', () => {
62
+ const { container } = render(
63
+ <WebsiteSection sectionRef="hero" category={WebsiteSectionCategory.HERO}>
64
+ <span>body</span>
65
+ </WebsiteSection>,
66
+ );
67
+ const section = container.querySelector('section');
68
+ expect(section?.hasAttribute('aria-label')).toBe(false);
69
+ expect(section?.hasAttribute('aria-labelledby')).toBe(false);
70
+ expect(section?.hasAttribute('id')).toBe(false);
71
+ });
72
+
73
+ it('rejects malformed sectionRef at parse time', () => {
74
+ expect(() =>
75
+ render(
76
+ <WebsiteSection
77
+ sectionRef="UPPER_BAD"
78
+ category={WebsiteSectionCategory.HERO}
79
+ >
80
+ <span />
81
+ </WebsiteSection>,
82
+ ),
83
+ ).toThrowError(/WebsiteSectionRef/);
84
+ });
85
+ });