@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,157 @@
1
+ /**
2
+ * InboundContactForm — the marketing site's real lead-capture form (React island).
3
+ *
4
+ * Self-contained + bundle-safe: uses only `react`, the website-local input schema, and
5
+ * a `WebsiteAnonymousSessionClient` instance (constructed by the section/bridge wiring
6
+ * from the site's API-origin config). On submit it validates client-side, then POSTs
7
+ * `/inbound-contacts` through the anonymous-session client — the FIRST submission mints
8
+ * the anonymous session (init header) and the backend stamps the lead's `anonymousUserId`
9
+ * so it can later transpose to the visitor's account on signup/login.
10
+ *
11
+ * Labels are injected (optional props with English fallbacks) rather than read from the
12
+ * website runtime context, so the component stays framework-light and unit-testable; the
13
+ * section wrapper passes i18n-resolved strings.
14
+ */
15
+
16
+ import React, { useCallback, useMemo, useState } from "react";
17
+ import { InboundContactFormSchema, buildInboundContactCreateBody, type InboundContactFormValues } from "./inbound-contact-form.schema";
18
+ import type { WebsiteAnonymousSessionClient } from "./website-anonymous-session-client";
19
+
20
+ export interface InboundContactFormLabels {
21
+ emailLabel: string;
22
+ firstNameLabel: string;
23
+ lastNameLabel: string;
24
+ phoneLabel: string;
25
+ /** OPTIONAL marketing opt-in checkbox. */
26
+ marketingConsentLabel: string;
27
+ submitLabel: string;
28
+ submittingLabel: string;
29
+ successMessage: string;
30
+ errorMessage: string;
31
+ }
32
+
33
+ const DEFAULT_LABELS: InboundContactFormLabels = {
34
+ emailLabel: "Email",
35
+ firstNameLabel: "First name",
36
+ lastNameLabel: "Last name",
37
+ phoneLabel: "Phone",
38
+ marketingConsentLabel: "Keep me updated about news and offers",
39
+ submitLabel: "Get in touch",
40
+ submittingLabel: "Sending…",
41
+ successMessage: "Thanks — we'll be in touch shortly.",
42
+ errorMessage: "Something went wrong. Please try again.",
43
+ };
44
+
45
+ type SubmitState = { status: "idle" } | { status: "submitting" } | { status: "success" } | { status: "error"; message: string };
46
+
47
+ export interface InboundContactFormProps {
48
+ /** The anonymous-session client (constructed by the section wiring from site config). */
49
+ client: WebsiteAnonymousSessionClient;
50
+ /** Optional i18n-resolved labels; English fallbacks are used per-field when omitted. */
51
+ labels?: Partial<InboundContactFormLabels>;
52
+ /** Optional callback fired after a successful submission (e.g. analytics). */
53
+ onSubmitted?: () => void;
54
+ }
55
+
56
+ const EMPTY_VALUES: InboundContactFormValues = {
57
+ email: "",
58
+ firstName: "",
59
+ lastName: "",
60
+ phone: "",
61
+ marketingConsent: false,
62
+ };
63
+
64
+ export function InboundContactForm({ client, labels, onSubmitted }: InboundContactFormProps): React.ReactElement {
65
+ const resolvedLabels = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
66
+ const [values, setValues] = useState<InboundContactFormValues>(EMPTY_VALUES);
67
+ const [fieldErrors, setFieldErrors] = useState<Partial<Record<keyof InboundContactFormValues, string>>>({});
68
+ const [submit, setSubmit] = useState<SubmitState>({ status: "idle" });
69
+
70
+ const setField = useCallback(<K extends keyof InboundContactFormValues>(key: K, value: InboundContactFormValues[K]) => {
71
+ setValues((prev) => ({ ...prev, [key]: value }));
72
+ }, []);
73
+
74
+ const handleSubmit = useCallback(
75
+ async (event: React.FormEvent<HTMLFormElement>) => {
76
+ event.preventDefault();
77
+ setFieldErrors({});
78
+
79
+ const parsed = InboundContactFormSchema.safeParse(values);
80
+ if (!parsed.success) {
81
+ const nextErrors: Partial<Record<keyof InboundContactFormValues, string>> = {};
82
+ for (const issue of parsed.error.issues) {
83
+ const key = issue.path[0] as keyof InboundContactFormValues | undefined;
84
+ if (key && !nextErrors[key]) nextErrors[key] = issue.message;
85
+ }
86
+ setFieldErrors(nextErrors);
87
+ return;
88
+ }
89
+
90
+ setSubmit({ status: "submitting" });
91
+ try {
92
+ await client.postAnonymous("/inbound-contacts", buildInboundContactCreateBody(parsed.data));
93
+ setSubmit({ status: "success" });
94
+ setValues(EMPTY_VALUES);
95
+ onSubmitted?.();
96
+ } catch {
97
+ // The backend-only `emailUserMatch` triage signal is never returned here, so a
98
+ // failure exposes no account-existence information — just a generic retry.
99
+ setSubmit({ status: "error", message: resolvedLabels.errorMessage });
100
+ }
101
+ },
102
+ [client, values, onSubmitted, resolvedLabels.errorMessage],
103
+ );
104
+
105
+ if (submit.status === "success") {
106
+ return (
107
+ <div role="status" aria-live="polite">
108
+ {resolvedLabels.successMessage}
109
+ </div>
110
+ );
111
+ }
112
+
113
+ const isSubmitting = submit.status === "submitting";
114
+
115
+ return (
116
+ <form onSubmit={handleSubmit} noValidate>
117
+ <label>
118
+ {resolvedLabels.emailLabel}
119
+ <input
120
+ type="email"
121
+ name="email"
122
+ required
123
+ value={values.email}
124
+ onChange={(e) => setField("email", e.target.value)}
125
+ aria-invalid={fieldErrors.email ? true : undefined}
126
+ />
127
+ {fieldErrors.email ? <span role="alert">{fieldErrors.email}</span> : null}
128
+ </label>
129
+
130
+ <label>
131
+ {resolvedLabels.firstNameLabel}
132
+ <input type="text" name="firstName" value={values.firstName ?? ""} onChange={(e) => setField("firstName", e.target.value)} />
133
+ </label>
134
+
135
+ <label>
136
+ {resolvedLabels.lastNameLabel}
137
+ <input type="text" name="lastName" value={values.lastName ?? ""} onChange={(e) => setField("lastName", e.target.value)} />
138
+ </label>
139
+
140
+ <label>
141
+ {resolvedLabels.phoneLabel}
142
+ <input type="tel" name="phone" value={values.phone ?? ""} onChange={(e) => setField("phone", e.target.value)} />
143
+ </label>
144
+
145
+ <label>
146
+ <input type="checkbox" name="marketingConsent" checked={values.marketingConsent} onChange={(e) => setField("marketingConsent", e.target.checked)} />
147
+ {resolvedLabels.marketingConsentLabel}
148
+ </label>
149
+
150
+ {submit.status === "error" ? <div role="alert">{submit.message}</div> : null}
151
+
152
+ <button type="submit" disabled={isSubmitting}>
153
+ {isSubmitting ? resolvedLabels.submittingLabel : resolvedLabels.submitLabel}
154
+ </button>
155
+ </form>
156
+ );
157
+ }
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { InboundContactFormSchema, buildInboundContactCreateBody, type InboundContactFormValues } from "../inbound-contact-form.schema";
4
+
5
+ /**
6
+ * The inbound-contact form is the anonymous lead-capture surface. Its
7
+ * The form may collect contact fields and an optional marketing preference. It must
8
+ * not manufacture notice, contract or consent evidence without an exact governed
9
+ * document release and the runtime receipt boundary.
10
+ */
11
+ describe("InboundContactFormSchema — contact and marketing choices", () => {
12
+ const minimum = { email: "Lead@Example.com" };
13
+
14
+ it("accepts email as the minimum and defaults the optional marketing choice", () => {
15
+ expect(InboundContactFormSchema.parse(minimum)).toEqual({ email: "Lead@Example.com", marketingConsent: false });
16
+ });
17
+
18
+ it("rejects a malformed email", () => {
19
+ expect(InboundContactFormSchema.safeParse({ ...minimum, email: "not-an-email" }).success).toBe(false);
20
+ });
21
+ });
22
+
23
+ describe("buildInboundContactCreateBody — no synthetic legal receipt", () => {
24
+ const values: InboundContactFormValues = {
25
+ email: "Lead@Example.com",
26
+ marketingConsent: false,
27
+ };
28
+
29
+ it("emits no embedded legal-document approval", () => {
30
+ const serialized = JSON.stringify(buildInboundContactCreateBody(values));
31
+ expect(serialized).not.toContain("Approval");
32
+ expect(serialized).not.toContain("acceptedAt");
33
+ });
34
+
35
+ it("lower-cases + trims the email", () => {
36
+ expect(buildInboundContactCreateBody(values).email).toBe("lead@example.com");
37
+ });
38
+
39
+ it("maps the OPTIONAL marketing opt-in to the dataProcessingConsent tier", () => {
40
+ const tier = (v: InboundContactFormValues): string => (buildInboundContactCreateBody(v).privacy as { dataProcessingConsent: string }).dataProcessingConsent;
41
+ expect(tier({ ...values, marketingConsent: true })).toBe("marketing");
42
+ expect(tier({ ...values, marketingConsent: false })).toBe("basic");
43
+ });
44
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Inbound-contact form input schema (website-local, bundle-safe).
3
+ *
4
+ * Deliberately a SEPARATE plain-Zod schema, NOT `InboundContactSchema` from
5
+ * `@wildo-ai/saas-models`: that schema is decorator-bound (`.isPrimaryKey()`,
6
+ * `.excludeFromCreate()`, `.isBackendOnly()`, …), so importing it would boot the
7
+ * decorator framework into the browser bundle — forbidden by bundle discipline. The
8
+ * website only needs to validate the handful of client-authored CREATE fields; the
9
+ * backend is the authority on the full contract (it re-validates + injects the
10
+ * server-set fields: `_id`, `anonymousUserId`, `userId`, `emailUserMatch`, timestamps).
11
+ *
12
+ * Mirrors the non-excluded, non-backend-only CREATE inputs of `InboundContact`
13
+ * (`email` required; `firstName` / `lastName` / `phone` optional), plus TWO distinct
14
+ * privacy signal: an OPTIONAL marketing opt-in (mapped to the
15
+ * `privacy.dataProcessingConsent` tier). A governed notice, contract or consent
16
+ * interaction must use the separate exact-release receipt contract; this DTO cannot
17
+ * manufacture legal evidence from a checkbox and client timestamp.
18
+ * `relatedUserId` is intentionally absent — it is `.excludeFromCreate()` on the
19
+ * resource (sales/admin-only, never client-authored).
20
+ */
21
+
22
+ import { z } from "zod";
23
+
24
+ /**
25
+ * The wire payload POSTed to `/api/v1/inbound-contacts`. `privacy` matches the
26
+ * resource's `InboundContactPrivacySchema` shape (a nested consent object) so the
27
+ * backend accepts it verbatim.
28
+ */
29
+ export const InboundContactFormSchema = z.object({
30
+ // Zod 4: top-level `z.email()` (not the deprecated `z.string().email()`), matching
31
+ // the resource schema. An empty string fails it, so it also covers "required".
32
+ email: z.email("Enter a valid email address"),
33
+ firstName: z.string().trim().max(200).optional(),
34
+ lastName: z.string().trim().max(200).optional(),
35
+ phone: z.string().trim().max(64).optional(),
36
+ /**
37
+ * OPTIONAL marketing opt-in — maps into the resource's `privacy.dataProcessingConsent`
38
+ * tier ('marketing' vs the BASIC default). Independent of the required agreement above.
39
+ */
40
+ marketingConsent: z.boolean().default(false),
41
+ });
42
+
43
+ export type InboundContactFormValues = z.infer<typeof InboundContactFormSchema>;
44
+
45
+ /**
46
+ * Build the exact JSON body the backend CREATE expects from validated form values.
47
+ * Keeps the `privacy` nesting + the consent enum mapping in ONE place so the form
48
+ * component stays declarative.
49
+ */
50
+ export function buildInboundContactCreateBody(values: InboundContactFormValues): Record<string, unknown> {
51
+ return {
52
+ email: values.email.trim().toLowerCase(),
53
+ ...(values.firstName ? { firstName: values.firstName } : {}),
54
+ ...(values.lastName ? { lastName: values.lastName } : {}),
55
+ ...(values.phone ? { phone: values.phone } : {}),
56
+ privacy: {
57
+ // OPTIONAL marketing tier: 'marketing' when the visitor opted in, else the
58
+ // resource's BASIC default.
59
+ dataProcessingConsent: values.marketingConsent ? "marketing" : "basic",
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * WebsiteAnonymousSessionClient
3
+ *
4
+ * A self-contained, browser-only anonymous-session HTTP client for the marketing
5
+ * website. It MIRRORS the SaaS app's `AnonymousSessionContext` protocol WITHOUT
6
+ * importing `@wildo-ai/saas-frontend-lib` — the website ships into a browser bundle
7
+ * that must not pull the decorator framework / heavy frontend graph (bundle
8
+ * discipline). It uses ONLY browser globals (`fetch` / `crypto` / `localStorage`) and
9
+ * the bundle-safe `@wildo-ai/saas-models/public-runtime` atoms. No axios (axios is the
10
+ * SSR tier), no React — a React island context can wrap this class.
11
+ *
12
+ * Protocol (W7.3a):
13
+ * 1. The first anonymous request sends `x-anonymous-session-init: <clientUuid>` — the
14
+ * backend mints an `AnonymousUser` (uuidRef = clientUuid) and returns a signed JWT
15
+ * in the `x-anonymous-session-token` RESPONSE header.
16
+ * 2. Subsequent requests send `x-anonymous-session: <jwt>`.
17
+ * Every request also sends `X-Frontend-Service-Name` (required by the anon backend
18
+ * to accept the request and resolve the frontend identity).
19
+ *
20
+ * CORS (critical): the website is a DIFFERENT origin from the backend API, so the
21
+ * `x-anonymous-session-token` response header is readable by `fetch` ONLY if the
22
+ * backend lists it in `Access-Control-Expose-Headers` (non-safelisted response headers
23
+ * are hidden from cross-origin `fetch`). Without that, the captured token is `null`,
24
+ * the session never advances past init, and every request re-mints a session.
25
+ *
26
+ * Cross-origin continuity: the anonymous session created here (uuidRef) is what the
27
+ * SaaS app transposes on signup/login. It is carried across the apex↔app boundary by
28
+ * the backend-set parent-domain `__wildo_anon_session` cookie — hence `credentials:
29
+ * 'include'`, so that cookie rides same-site requests to the API.
30
+ */
31
+
32
+ import { WildoHeaderKeys, MAIN_API_BASE_PATH } from '@wildo-ai/saas-models/public-runtime';
33
+
34
+ /**
35
+ * localStorage keys. The website is self-contained (mirror-don't-import), so it pins
36
+ * the stable literal values rather than importing the decorator-adjacent shared
37
+ * contract. Cross-origin continuity flows through the backend cookie, NOT this
38
+ * localStorage, so these keys need not match the app's byte-for-byte.
39
+ */
40
+ const STORAGE_KEYS = {
41
+ anonymousUserId: 'anonymousUserId',
42
+ anonymousSessionToken: 'anonymousSessionToken',
43
+ } as const;
44
+
45
+ export interface WebsiteAnonymousSessionClientOptions {
46
+ /** Backend API origin WITHOUT a trailing slash, e.g. `https://api.example.com`. */
47
+ apiOrigin: string;
48
+ /** `X-Frontend-Service-Name` value identifying this marketing site to the backend. */
49
+ frontendServiceName: string;
50
+ }
51
+
52
+ /** Thrown when an anonymous request returns a non-2xx status. */
53
+ export class WebsiteAnonymousRequestError extends Error {
54
+ constructor(
55
+ public readonly status: number,
56
+ public readonly bodyText: string,
57
+ ) {
58
+ super(`Anonymous request failed with status ${status}`);
59
+ this.name = 'WebsiteAnonymousRequestError';
60
+ }
61
+ }
62
+
63
+ export class WebsiteAnonymousSessionClient {
64
+ private anonymousUserId: string | null = null;
65
+ private sessionToken: string | null = null;
66
+ private readonly apiBaseUrl: string;
67
+ private readonly frontendServiceName: string;
68
+
69
+ constructor(options: WebsiteAnonymousSessionClientOptions) {
70
+ this.apiBaseUrl = `${options.apiOrigin.replace(/\/+$/, '')}${MAIN_API_BASE_PATH}`;
71
+ this.frontendServiceName = options.frontendServiceName;
72
+ this.restoreFromStorage();
73
+ }
74
+
75
+ /** The anonymous session's relational identity (client uuidRef), or `null` if not started. */
76
+ getAnonymousUserId(): string | null {
77
+ return this.anonymousUserId;
78
+ }
79
+
80
+ /**
81
+ * Return the anonymous session's relational identity (client uuidRef),
82
+ * STARTING the session first if it has not begun yet. Unlike
83
+ * `getAnonymousUserId()` (which may return `null`), this always yields
84
+ * a stable uuidRef.
85
+ *
86
+ * Used by NESTED anonymous routes that must embed the id in the URL
87
+ * BEFORE the first request — e.g. draft-notes POSTs to
88
+ * `/user-anonymous-users/<uuidRef>/draft-notes`. The id generated here
89
+ * is the SAME one the next `postAnonymous(...)` sends as
90
+ * `x-anonymous-session-init`, so the URL scope and the minted session
91
+ * refer to one anonymous user (no split identity).
92
+ */
93
+ getOrStartAnonymousUserId(): string {
94
+ this.ensureSessionStarted();
95
+ // ensureSessionStarted() guarantees anonymousUserId is non-null.
96
+ return this.anonymousUserId as string;
97
+ }
98
+
99
+ /** `true` once an init request has succeeded and a session token is held. */
100
+ isSessionActive(): boolean {
101
+ return this.anonymousUserId !== null && this.sessionToken !== null;
102
+ }
103
+
104
+ /**
105
+ * POST `body` to an anonymous endpoint. `path` is relative to `/api/v1`
106
+ * (e.g. `/inbound-contacts`, or a nested `/anonymous-users/<uuidRef>/draft-notes`).
107
+ * Lazily starts the session on the first call, attaches the anon-session +
108
+ * service-name headers, captures any returned session token, and returns the parsed
109
+ * JSON response. Throws `WebsiteAnonymousRequestError` on a non-2xx status.
110
+ */
111
+ async postAnonymous<TResponse = unknown>(path: string, body: unknown): Promise<TResponse> {
112
+ this.ensureSessionStarted();
113
+
114
+ const response = await fetch(`${this.apiBaseUrl}${path}`, {
115
+ method: 'POST',
116
+ headers: this.buildHeaders(),
117
+ body: JSON.stringify(body),
118
+ // Carry the parent-domain `__wildo_anon_session` cookie to the API so the
119
+ // session is continuous across the apex/app/api origins.
120
+ credentials: 'include',
121
+ });
122
+
123
+ this.captureSessionToken(response);
124
+
125
+ if (!response.ok) {
126
+ throw new WebsiteAnonymousRequestError(response.status, await safeReadText(response));
127
+ }
128
+ return (await response.json()) as TResponse;
129
+ }
130
+
131
+ private ensureSessionStarted(): void {
132
+ if (!this.anonymousUserId) {
133
+ this.anonymousUserId = crypto.randomUUID();
134
+ this.sessionToken = null;
135
+ this.persist();
136
+ }
137
+ }
138
+
139
+ private buildHeaders(): Record<string, string> {
140
+ const headers: Record<string, string> = {
141
+ 'Content-Type': 'application/json',
142
+ [WildoHeaderKeys.FRONTEND_SERVICE_NAME]: this.frontendServiceName,
143
+ };
144
+ // Once we hold the JWT, restore the session; before that, the raw UUID inits it.
145
+ if (this.sessionToken) {
146
+ headers[WildoHeaderKeys.ANONYMOUS_SESSION] = this.sessionToken;
147
+ } else if (this.anonymousUserId) {
148
+ headers[WildoHeaderKeys.ANONYMOUS_SESSION_INIT] = this.anonymousUserId;
149
+ }
150
+ return headers;
151
+ }
152
+
153
+ private captureSessionToken(response: Response): void {
154
+ // Requires the backend to CORS-expose this header (see the class-level CORS note).
155
+ const token = response.headers.get(WildoHeaderKeys.ANONYMOUS_SESSION_TOKEN);
156
+ if (token && token !== this.sessionToken) {
157
+ this.sessionToken = token;
158
+ this.persist();
159
+ }
160
+ }
161
+
162
+ private restoreFromStorage(): void {
163
+ try {
164
+ const userId = localStorage.getItem(STORAGE_KEYS.anonymousUserId);
165
+ const token = localStorage.getItem(STORAGE_KEYS.anonymousSessionToken);
166
+ if (userId) this.anonymousUserId = userId;
167
+ if (token) this.sessionToken = token;
168
+ } catch {
169
+ // localStorage unavailable (SSR build / privacy mode) — the session stays
170
+ // in-memory for this page load only.
171
+ }
172
+ }
173
+
174
+ private persist(): void {
175
+ try {
176
+ if (this.anonymousUserId) {
177
+ localStorage.setItem(STORAGE_KEYS.anonymousUserId, this.anonymousUserId);
178
+ }
179
+ if (this.sessionToken) {
180
+ localStorage.setItem(STORAGE_KEYS.anonymousSessionToken, this.sessionToken);
181
+ }
182
+ } catch {
183
+ // Best-effort persistence; a failure only means the session won't survive reload.
184
+ }
185
+ }
186
+ }
187
+
188
+ async function safeReadText(response: Response): Promise<string> {
189
+ try {
190
+ return await response.text();
191
+ } catch {
192
+ return '';
193
+ }
194
+ }
@@ -0,0 +1,52 @@
1
+ import React, { createContext, type ReactNode } from 'react';
2
+
3
+ import type { WebsitePageRef } from '../../schemas/refs/page-ref.schemas';
4
+
5
+ /**
6
+ * @wildo_source:part:start saas.website.page.context facet:layer:core facet:family:website
7
+ *
8
+ * `WebsitePageContext` — identity of the page currently rendering.
9
+ * Provided by `<WebsitePageLayout pageRef="…">` exactly once per
10
+ * page render and consumed by `useWebsiteLabel(leaf)` to compose the
11
+ * full label key (`website.{pageRef}.{sectionRef}.{leaf}`).
12
+ *
13
+ * **Why an isolated context (not merged into runtime)**: the runtime
14
+ * context is set at the host-adapter boundary and is constant per
15
+ * build; the page context is set per page route. Splitting them
16
+ * lets a single host adapter render multiple pages (preview tooling,
17
+ * MDX-driven blog index aggregating post excerpts, etc.) without
18
+ * lying about which page each subtree belongs to.
19
+ *
20
+ * **Why a separate file from the hook**: same Fast-Refresh rationale
21
+ * as `WebsiteRuntimeContext` — split provider/context/hook to keep
22
+ * HMR boundaries small.
23
+ */
24
+ export interface WebsitePageContextValue {
25
+ /**
26
+ * The validated `WebsitePageRef` of the page currently rendering.
27
+ * MUST match a `WebsitePageManifest.pageRef` registered in the
28
+ * site's `WebsiteRootConfig`.
29
+ */
30
+ pageRef: WebsitePageRef;
31
+ }
32
+
33
+ export const WebsitePageContext: React.Context<WebsitePageContextValue | null> =
34
+ createContext<WebsitePageContextValue | null>(null);
35
+
36
+ export interface WebsitePageContextProviderProps {
37
+ value: WebsitePageContextValue;
38
+ children: ReactNode;
39
+ }
40
+
41
+ export function WebsitePageContextProvider({
42
+ value,
43
+ children,
44
+ }: WebsitePageContextProviderProps): ReactNode {
45
+ return (
46
+ <WebsitePageContext.Provider value={value}>
47
+ {children}
48
+ </WebsitePageContext.Provider>
49
+ );
50
+ }
51
+ WebsitePageContextProvider.displayName = 'WebsitePageContextProvider';
52
+ /** @wildo_source:part:end saas.website.page.context */