@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,193 @@
1
+ import React, { type CSSProperties, type ReactNode } from 'react';
2
+
3
+ import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
4
+
5
+ import { useWebsiteRuntime } from '../../core/contexts/useWebsiteRuntime';
6
+ import { buildLocalizedRoutePath } from '../../core/routing/internal-routing.utils';
7
+
8
+ import { WebsiteLink } from './WebsiteLink';
9
+
10
+ /**
11
+ * @wildo_source:part:start saas.website.primitives.internal-link facet:layer:components facet:family:website
12
+ *
13
+ * `WebsiteInternalLink` — L1 marketing-site link primitive for
14
+ * INTERNAL routes (any path served by the same Astro build).
15
+ *
16
+ * **The architectural problem this solves** (Phase 6 third audit fix
17
+ * H-β): the marketing site mounts the default locale at the
18
+ * un-prefixed path (`/`, `/pricing`) and every other locale under
19
+ * `/<locale>/…` (`/fr/`, `/fr/pricing`). A naive `<a href="/pricing">`
20
+ * authored in the chrome therefore works on EN pages and SILENTLY
21
+ * BREAKS on FR pages — the visitor falls out of their locale on every
22
+ * navigation. The dist-HTML audit confirmed the regression.
23
+ *
24
+ * **Why a dedicated primitive** (parallel to `WebsiteLink`, not a flag
25
+ * on it):
26
+ * 1. The two primitives have different authoring contracts — an
27
+ * external link's `href` is a fully-qualified URL the author
28
+ * already commits to; an internal link's `href` is a raw
29
+ * `routePath` (`'/pricing'`) that the framework prefixes. Mixing
30
+ * both shapes behind one `external?` prop forces the author to
31
+ * think about a routing rule on every link they place; mixing
32
+ * them behind one component invites the wrong shape on the wrong
33
+ * branch (an internal href passed with `external` set, or vice
34
+ * versa) without a type-system signal.
35
+ * 2. Internal links REQUIRE the runtime context; external links do
36
+ * not. Forcing every external-link consumer to wrap in
37
+ * `<WebsiteRuntimeContextProvider>` (or pay the
38
+ * throw-on-missing-context cost) is wrong. Two primitives keep
39
+ * the dependency surface honest.
40
+ * 3. The framework can layer additional internal-link concerns
41
+ * later (active-route highlighting, prefetch directives,
42
+ * router-aware navigation when the consumer overrides
43
+ * `WebsiteLink`) without complicating the external-link
44
+ * contract.
45
+ *
46
+ * **Why `routePath` is a string and NOT `WebsitePageRef`**:
47
+ * - Many internal targets are NOT pages (anchor links to a section
48
+ * within the current page: `'#features'`; same-locale subroutes
49
+ * served by the consumer's Astro build but not registered as
50
+ * `WebsitePageManifest` entries: `'/blog/launch'`). A `pageRef`
51
+ * prop would force every link target to be a registered manifest,
52
+ * which is too restrictive for chrome.
53
+ * - The consumer's chrome typically links to a small fixed set of
54
+ * `routePath` values they author once. The boilerplate cost of
55
+ * looking up the matching manifest by ref is non-trivial; the
56
+ * payoff (typo protection on `'/pricing'`) is small.
57
+ * - We DO validate at runtime that `routePath` starts with `/` and
58
+ * does not double-prefix (a path already starting with `/<locale>/`
59
+ * would otherwise become `/fr/fr/pricing` on a FR page) — see the
60
+ * in-render guard below.
61
+ *
62
+ * **Anchor-only links** (`href="#features"`) are passed through
63
+ * verbatim: they navigate within the current page, no locale prefix
64
+ * applies. Same for `mailto:` / `tel:` — recognized by colon presence
65
+ * and forwarded as-is.
66
+ *
67
+ * **Boundary**: this component MUST stay in the React-island bundle
68
+ * tier (`components/low-level/`). It depends on `useWebsiteRuntime`
69
+ * (which throws when called outside a `<WebsiteRuntimeContextProvider>`)
70
+ * — the throw is the contract that prevents the chrome from being
71
+ * rendered outside a bridged page accidentally.
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * // In HeaderComponent.tsx — the framework computes the right URL
76
+ * // automatically (`/pricing` on EN pages, `/fr/pricing` on FR
77
+ * // pages):
78
+ * <WebsiteInternalLink routePath="/pricing">
79
+ * <WebsiteText variant={WebsiteTextVariant.BODY}>Pricing</WebsiteText>
80
+ * </WebsiteInternalLink>
81
+ * ```
82
+ */
83
+ export interface WebsiteInternalLinkProps {
84
+ /**
85
+ * The locale-NEUTRAL route path. Must match a value the consumer's
86
+ * Astro build serves (typically a `WebsitePageManifest.routePath`,
87
+ * but anchor-only `#section` and same-page subroutes are also
88
+ * accepted). MUST start with `/` (or `#` for in-page anchors, or
89
+ * include `:` for `mailto:` / `tel:`).
90
+ */
91
+ routePath: string;
92
+ className?: string;
93
+ style?: CSSProperties;
94
+ id?: string;
95
+ /**
96
+ * Optional `aria-label` for icon-only links (where the link text
97
+ * is a glyph or sr-only).
98
+ */
99
+ ariaLabel?: string;
100
+ children: ReactNode;
101
+ }
102
+
103
+ function isAnchorOrUrlScheme(routePath: string): boolean {
104
+ return routePath.startsWith('#') || routePath.includes(':');
105
+ }
106
+
107
+ /**
108
+ * Pre-computed set of every known locale code (`'en'`, `'fr'`,
109
+ * `'en-GB'`, …). Built ONCE at module load from `AvailableLanguage`
110
+ * so the per-render double-prefix guard below is an O(1) Set.has
111
+ * check rather than an Object.values() walk on every link render.
112
+ */
113
+ const ALL_LOCALE_CODES: ReadonlySet<string> = new Set(
114
+ Object.values(AvailableLanguage) as readonly string[],
115
+ );
116
+
117
+ /**
118
+ * Defensive in-render guard: rejects route paths whose first segment
119
+ * matches a known locale code. The marketing-site convention is that
120
+ * the consumer authors locale-NEUTRAL paths and the framework
121
+ * prefixes them; an already-prefixed path would either render as a
122
+ * `/<locale>/<locale>/pricing` double-prefix on non-default-locale
123
+ * pages or silently work on default-locale pages and break on other
124
+ * locales — both are silent regressions. Throwing here makes the
125
+ * misuse loud at first render.
126
+ *
127
+ * Returns the offending locale segment if the path is misused, or
128
+ * `null` if the path is well-formed.
129
+ */
130
+ function detectAccidentalLocalePrefix(routePath: string): string | null {
131
+ const slashIdx = routePath.indexOf('/', 1);
132
+ const firstSegment =
133
+ slashIdx === -1 ? routePath.slice(1) : routePath.slice(1, slashIdx);
134
+ if (firstSegment === '') return null;
135
+ return ALL_LOCALE_CODES.has(firstSegment) ? firstSegment : null;
136
+ }
137
+
138
+ export function WebsiteInternalLink({
139
+ routePath,
140
+ className,
141
+ style,
142
+ id,
143
+ ariaLabel,
144
+ children,
145
+ }: WebsiteInternalLinkProps): ReactNode {
146
+ const runtime = useWebsiteRuntime();
147
+
148
+ let href: string;
149
+ if (isAnchorOrUrlScheme(routePath)) {
150
+ href = routePath;
151
+ } else {
152
+ if (!routePath.startsWith('/')) {
153
+ throw new Error(
154
+ `[WebsiteInternalLink] routePath="${routePath}" must start with "/" ` +
155
+ `(framework-relative path), "#" (in-page anchor) or include ":" ` +
156
+ `(URL scheme like mailto:/tel:). Use <WebsiteLink href={...} external={true}> ` +
157
+ `for fully-qualified external URLs.`,
158
+ );
159
+ }
160
+ const accidentalPrefix = detectAccidentalLocalePrefix(routePath);
161
+ if (accidentalPrefix !== null) {
162
+ throw new Error(
163
+ `[WebsiteInternalLink] routePath="${routePath}" begins with the ` +
164
+ `locale segment "/${accidentalPrefix}" — the framework prefixes ` +
165
+ `the active locale automatically, so passing a pre-prefixed path ` +
166
+ `would render as "/${accidentalPrefix}${routePath}" on non-default ` +
167
+ `locale pages (double-prefix) or silently break locale routing on ` +
168
+ `default-locale pages. Pass the locale-NEUTRAL path instead ` +
169
+ `(eg. "/pricing", not "/${accidentalPrefix}/pricing").`,
170
+ );
171
+ }
172
+ href = buildLocalizedRoutePath({
173
+ locale: runtime.locale,
174
+ defaultLocale: runtime.defaultLocale,
175
+ routePath,
176
+ });
177
+ }
178
+
179
+ return (
180
+ <WebsiteLink
181
+ href={href}
182
+ external={false}
183
+ className={className}
184
+ style={style}
185
+ id={id}
186
+ ariaLabel={ariaLabel}
187
+ >
188
+ {children}
189
+ </WebsiteLink>
190
+ );
191
+ }
192
+ WebsiteInternalLink.displayName = 'WebsiteInternalLink';
193
+ /** @wildo_source:part:end saas.website.primitives.internal-link */
@@ -0,0 +1,71 @@
1
+ import React, { type CSSProperties, type ReactNode } from 'react';
2
+
3
+ /**
4
+ * @wildo_source:part:start saas.website.primitives.link facet:layer:components facet:family:website
5
+ *
6
+ * `WebsiteLink` — L1 marketing-site link primitive.
7
+ *
8
+ * **Status (Phase 2 / D18)**: minimal placeholder. Renders a plain
9
+ * `<a>` with `data-website-primitive="link"` and forwards href +
10
+ * standard link attributes. Consumers override at the L1 boundary
11
+ * to wire in their preferred client-side router (Astro `<a>` does
12
+ * full navigation by default; consumers using View Transitions or
13
+ * a router-aware link wrap that here).
14
+ *
15
+ * **Why `external` is an explicit prop** (vs. inferred from the
16
+ * href shape): inference is unreliable (relative paths from a CMS
17
+ * may resolve to external; protocol-relative URLs are external but
18
+ * don't start with `http`). An explicit prop forces the section
19
+ * author to decide. When `external === true`, the primitive applies
20
+ * `rel="noopener noreferrer"` and `target="_blank"` automatically
21
+ * — security best-practice baked in.
22
+ *
23
+ * **Why no `variant`**: link visual variants (button-like CTA,
24
+ * inline-text link, icon-only) are too tightly coupled to the
25
+ * design system to be useful as a placeholder vocabulary. The
26
+ * override implementation defines its own variants; the
27
+ * placeholder ships only the security-correct attribute defaults.
28
+ */
29
+ export interface WebsiteLinkProps {
30
+ href: string;
31
+ /**
32
+ * Marks the link as external. Adds `target="_blank"` and
33
+ * `rel="noopener noreferrer"`. Defaults to `false`.
34
+ */
35
+ external?: boolean;
36
+ className?: string;
37
+ style?: CSSProperties;
38
+ id?: string;
39
+ /**
40
+ * Optional `aria-label` for icon-only links (where the link text
41
+ * is a glyph or sr-only).
42
+ */
43
+ ariaLabel?: string;
44
+ children: ReactNode;
45
+ }
46
+
47
+ export function WebsiteLink({
48
+ href,
49
+ external = false,
50
+ className,
51
+ style,
52
+ id,
53
+ ariaLabel,
54
+ children,
55
+ }: WebsiteLinkProps): ReactNode {
56
+ const attrs: Record<string, unknown> = {
57
+ 'data-website-primitive': 'link',
58
+ href,
59
+ };
60
+ if (external) {
61
+ attrs.target = '_blank';
62
+ attrs.rel = 'noopener noreferrer';
63
+ }
64
+ if (className !== undefined) attrs.className = className;
65
+ if (style !== undefined) attrs.style = style;
66
+ if (id !== undefined) attrs.id = id;
67
+ if (ariaLabel !== undefined) attrs['aria-label'] = ariaLabel;
68
+ return <a {...attrs}>{children}</a>;
69
+ }
70
+ WebsiteLink.displayName = 'WebsiteLink';
71
+ /** @wildo_source:part:end saas.website.primitives.link */
@@ -0,0 +1,75 @@
1
+ import React, { type CSSProperties, type ReactNode } from 'react';
2
+
3
+ /**
4
+ * @wildo_source:part:start saas.website.primitives.text facet:layer:components facet:family:website
5
+ *
6
+ * `WebsiteText` — L1 marketing-site body-text primitive.
7
+ *
8
+ * Same Phase 2 / D18 status as `WebsiteHeading` — minimal,
9
+ * self-contained placeholder; consumers override at the L1 boundary
10
+ * via barrel re-export. The skill-driven authoring path will require
11
+ * an override before shipping a public marketing site.
12
+ *
13
+ * **Why `variant` (lead vs. body vs. caption) is shipped here**:
14
+ * marketing-site body copy has a distinct typographic scale from
15
+ * in-app body copy (looser line-height for scannability, larger
16
+ * `lead` paragraphs under headlines, smaller `caption` under
17
+ * pricing tier features). The variant slot exists at the
18
+ * placeholder level so section authors call the same prop the
19
+ * override implementation honors.
20
+ *
21
+ * **Why default tag is `<p>`**: the overwhelming majority of
22
+ * marketing copy is a paragraph. The `as` prop allows escaping to
23
+ * `<span>` (inline copy inside a heading), `<strong>` (emphatic
24
+ * inline), or `<em>` (italicized inline) without compromising the
25
+ * paragraph default.
26
+ */
27
+ export enum WebsiteTextVariant {
28
+ /**
29
+ * Lead paragraph immediately under a section headline (slightly
30
+ * larger / less dense than body).
31
+ */
32
+ LEAD = 'lead',
33
+ /**
34
+ * Standard body copy. The default.
35
+ */
36
+ BODY = 'body',
37
+ /**
38
+ * Small / supporting copy (footnotes, captions, fine print).
39
+ */
40
+ CAPTION = 'caption',
41
+ }
42
+
43
+ export type WebsiteTextElement = 'p' | 'span' | 'strong' | 'em' | 'div';
44
+
45
+ export interface WebsiteTextProps {
46
+ /** Visual variant. Defaults to `BODY`. */
47
+ variant?: WebsiteTextVariant;
48
+ /** Rendered tag. Defaults to `'p'`. */
49
+ as?: WebsiteTextElement;
50
+ className?: string;
51
+ style?: CSSProperties;
52
+ id?: string;
53
+ children: ReactNode;
54
+ }
55
+
56
+ export function WebsiteText({
57
+ variant = WebsiteTextVariant.BODY,
58
+ as = 'p',
59
+ className,
60
+ style,
61
+ id,
62
+ children,
63
+ }: WebsiteTextProps): ReactNode {
64
+ const Tag = as;
65
+ const attrs: Record<string, unknown> = {
66
+ 'data-website-primitive': 'text',
67
+ 'data-website-text-variant': variant,
68
+ };
69
+ if (className !== undefined) attrs.className = className;
70
+ if (style !== undefined) attrs.style = style;
71
+ if (id !== undefined) attrs.id = id;
72
+ return <Tag {...attrs}>{children}</Tag>;
73
+ }
74
+ WebsiteText.displayName = 'WebsiteText';
75
+ /** @wildo_source:part:end saas.website.primitives.text */
@@ -0,0 +1,169 @@
1
+ import React from 'react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { render } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
7
+
8
+ import { WebsiteRuntimeContextProvider } from '../../../core/contexts/WebsiteRuntimeContext';
9
+ import { WebsiteLabelKeySchema } from '../../../schemas/label-keys/website-label-key.schemas';
10
+
11
+ import { WebsiteInternalButton } from '../WebsiteInternalButton';
12
+ import { WebsiteButtonSize, WebsiteButtonVariant } from '../WebsiteButton';
13
+
14
+ const SKIP_KEY = WebsiteLabelKeySchema.parse('website.chrome.layout.skip-to-main-content');
15
+
16
+ function renderWithRuntime(
17
+ ui: React.ReactNode,
18
+ opts: { locale: AvailableLanguage; defaultLocale: AvailableLanguage },
19
+ ): ReturnType<typeof render> {
20
+ return render(
21
+ <WebsiteRuntimeContextProvider
22
+ value={{
23
+ locale: opts.locale,
24
+ defaultLocale: opts.defaultLocale,
25
+ labelPack: {},
26
+ designTokens: {
27
+ colors: {
28
+ background: '#fff',
29
+ foreground: '#111',
30
+ primary: '#0070f3',
31
+ primaryForeground: '#fff',
32
+ accent: '#ff5e5b',
33
+ accentForeground: '#fff',
34
+ surface: '#fafafa',
35
+ surfaceForeground: '#111',
36
+ border: '#e5e5e5',
37
+ muted: '#f5f5f5',
38
+ mutedForeground: '#737373',
39
+ },
40
+ radii: {
41
+ none: '0',
42
+ sm: '0.125rem',
43
+ md: '0.375rem',
44
+ lg: '0.75rem',
45
+ xl: '1.5rem',
46
+ full: '9999px',
47
+ },
48
+ },
49
+ navigation: {
50
+ headerComponent: () => null,
51
+ footerComponent: () => null,
52
+ },
53
+ skipToMainContentLabelKey: SKIP_KEY,
54
+ }}
55
+ >
56
+ {ui}
57
+ </WebsiteRuntimeContextProvider>,
58
+ );
59
+ }
60
+
61
+ describe('WebsiteInternalButton', () => {
62
+ describe('default-locale page (un-prefixed routing)', () => {
63
+ it('renders the routePath verbatim when locale === defaultLocale', () => {
64
+ const { container } = renderWithRuntime(
65
+ <WebsiteInternalButton routePath="/contact">Contact</WebsiteInternalButton>,
66
+ { locale: AvailableLanguage.EN, defaultLocale: AvailableLanguage.EN },
67
+ );
68
+ expect(container.querySelector('a')?.getAttribute('href')).toBe('/contact');
69
+ });
70
+ });
71
+
72
+ describe('non-default-locale page (prefixed routing)', () => {
73
+ it('prefixes /<locale> on FR pages', () => {
74
+ const { container } = renderWithRuntime(
75
+ <WebsiteInternalButton routePath="/contact">Contactez-nous</WebsiteInternalButton>,
76
+ { locale: AvailableLanguage.FR, defaultLocale: AvailableLanguage.EN },
77
+ );
78
+ expect(container.querySelector('a')?.getAttribute('href')).toBe('/fr/contact');
79
+ });
80
+
81
+ it('collapses root to /<locale> (NOT /<locale>/)', () => {
82
+ const { container } = renderWithRuntime(
83
+ <WebsiteInternalButton routePath="/">Home</WebsiteInternalButton>,
84
+ { locale: AvailableLanguage.FR, defaultLocale: AvailableLanguage.EN },
85
+ );
86
+ expect(container.querySelector('a')?.getAttribute('href')).toBe('/fr');
87
+ });
88
+ });
89
+
90
+ describe('misuse guards', () => {
91
+ it('throws when routePath does not start with /', () => {
92
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
93
+ expect(() =>
94
+ renderWithRuntime(
95
+ <WebsiteInternalButton routePath="contact">Contact</WebsiteInternalButton>,
96
+ { locale: AvailableLanguage.EN, defaultLocale: AvailableLanguage.EN },
97
+ ),
98
+ ).toThrow(/must start with/i);
99
+ consoleErrorSpy.mockRestore();
100
+ });
101
+
102
+ it('rejects anchor-only routePaths (use WebsiteButton instead)', () => {
103
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
104
+ expect(() =>
105
+ renderWithRuntime(
106
+ <WebsiteInternalButton routePath="#features">Features</WebsiteInternalButton>,
107
+ { locale: AvailableLanguage.EN, defaultLocale: AvailableLanguage.EN },
108
+ ),
109
+ ).toThrow(/must start with "\/"/i);
110
+ consoleErrorSpy.mockRestore();
111
+ });
112
+
113
+ it('rejects URL schemes (mailto: / tel:)', () => {
114
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
115
+ expect(() =>
116
+ renderWithRuntime(
117
+ <WebsiteInternalButton routePath="mailto:hi@wildo.ai">Email</WebsiteInternalButton>,
118
+ { locale: AvailableLanguage.EN, defaultLocale: AvailableLanguage.EN },
119
+ ),
120
+ ).toThrow(/must start with "\/"/i);
121
+ consoleErrorSpy.mockRestore();
122
+ });
123
+
124
+ it('throws when used outside <WebsiteRuntimeContextProvider>', () => {
125
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
126
+ expect(() =>
127
+ render(<WebsiteInternalButton routePath="/contact">Contact</WebsiteInternalButton>),
128
+ ).toThrow(/WebsiteRuntimeContextProvider/);
129
+ consoleErrorSpy.mockRestore();
130
+ });
131
+ });
132
+
133
+ describe('attribute / variant forwarding', () => {
134
+ it('forwards variant + size to the underlying WebsiteButton', () => {
135
+ const { container } = renderWithRuntime(
136
+ <WebsiteInternalButton
137
+ routePath="/contact"
138
+ variant={WebsiteButtonVariant.SECONDARY}
139
+ size={WebsiteButtonSize.LG}
140
+ >
141
+ Contact
142
+ </WebsiteInternalButton>,
143
+ { locale: AvailableLanguage.EN, defaultLocale: AvailableLanguage.EN },
144
+ );
145
+ const span = container.querySelector('[data-website-primitive="button"]');
146
+ expect(span?.getAttribute('data-website-button-variant')).toBe('secondary');
147
+ expect(span?.getAttribute('data-website-button-size')).toBe('lg');
148
+ expect(span?.getAttribute('data-website-button-mode')).toBe('link');
149
+ });
150
+
151
+ it('forwards className, id, ariaLabel to the rendered anchor', () => {
152
+ const { container } = renderWithRuntime(
153
+ <WebsiteInternalButton
154
+ routePath="/contact"
155
+ className="wt-cta"
156
+ id="hero-cta"
157
+ ariaLabel="Open contact page"
158
+ >
159
+ Contact
160
+ </WebsiteInternalButton>,
161
+ { locale: AvailableLanguage.EN, defaultLocale: AvailableLanguage.EN },
162
+ );
163
+ const a = container.querySelector('a');
164
+ expect(a?.getAttribute('class')).toBe('wt-cta');
165
+ expect(a?.getAttribute('id')).toBe('hero-cta');
166
+ expect(a?.getAttribute('aria-label')).toBe('Open contact page');
167
+ });
168
+ });
169
+ });