@wildo-ai/saas-website 1.1.4 → 1.1.6

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 (265) hide show
  1. package/dist/esm/astro/blog-post-bridge.d.ts +23 -20
  2. package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
  3. package/dist/esm/astro/blog-post-bridge.js +13 -6
  4. package/dist/esm/astro/blog-post-bridge.js.map +1 -1
  5. package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -1
  6. package/dist/esm/astro/bridge-runtime.d.ts +55 -26
  7. package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
  8. package/dist/esm/astro/bridge-runtime.js +21 -7
  9. package/dist/esm/astro/bridge-runtime.js.map +1 -1
  10. package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -1
  11. package/dist/esm/astro/headers-renderer.d.ts.map +1 -1
  12. package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -1
  13. package/dist/esm/astro/label-pack-loader.d.ts +4 -4
  14. package/dist/esm/astro/label-pack-loader.d.ts.map +1 -1
  15. package/dist/esm/astro/label-pack-loader.js +14 -4
  16. package/dist/esm/astro/label-pack-loader.js.map +1 -1
  17. package/dist/esm/astro/llms-txt-server.d.ts.map +1 -1
  18. package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -1
  19. package/dist/esm/astro/robots-renderer.d.ts +9 -8
  20. package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
  21. package/dist/esm/astro/robots-renderer.js +15 -15
  22. package/dist/esm/astro/robots-renderer.js.map +1 -1
  23. package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -1
  24. package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -1
  25. package/dist/esm/astro/website-page-head.renderer.d.ts +1 -1
  26. package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
  27. package/dist/esm/astro/website-page-head.renderer.js +2 -1
  28. package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
  29. package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -1
  30. package/dist/esm/astro/website-site-context.d.ts +5 -2
  31. package/dist/esm/astro/website-site-context.d.ts.map +1 -1
  32. package/dist/esm/astro/website-site-context.js.map +1 -1
  33. package/dist/esm/astro-island.d.ts.map +1 -1
  34. package/dist/esm/astro.d.ts.map +1 -1
  35. package/dist/esm/companion-exports.d.ts +8 -15
  36. package/dist/esm/companion-exports.d.ts.map +1 -1
  37. package/dist/esm/companion-exports.js +8 -15
  38. package/dist/esm/companion-exports.js.map +1 -1
  39. package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -1
  40. package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -1
  41. package/dist/esm/config/define-website-config.d.ts +3 -2
  42. package/dist/esm/config/define-website-config.d.ts.map +1 -1
  43. package/dist/esm/config/define-website-config.js +10 -10
  44. package/dist/esm/config/define-website-config.js.map +1 -1
  45. package/dist/esm/config/index.d.ts.map +1 -1
  46. package/dist/esm/config/load-website-config.d.ts +6 -0
  47. package/dist/esm/config/load-website-config.d.ts.map +1 -1
  48. package/dist/esm/config/load-website-config.js +20 -10
  49. package/dist/esm/config/load-website-config.js.map +1 -1
  50. package/dist/esm/config/load-website-pricing-catalog.d.ts +13 -0
  51. package/dist/esm/config/load-website-pricing-catalog.d.ts.map +1 -0
  52. package/dist/esm/config/load-website-pricing-catalog.js +34 -0
  53. package/dist/esm/config/load-website-pricing-catalog.js.map +1 -0
  54. package/dist/esm/config/load-website-site-context.d.ts +53 -0
  55. package/dist/esm/config/load-website-site-context.d.ts.map +1 -0
  56. package/dist/esm/config/load-website-site-context.js +98 -0
  57. package/dist/esm/config/load-website-site-context.js.map +1 -0
  58. package/dist/esm/config/wildo-website-config.schemas.d.ts +10 -5
  59. package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
  60. package/dist/esm/config/wildo-website-config.schemas.js +10 -5
  61. package/dist/esm/config/wildo-website-config.schemas.js.map +1 -1
  62. package/dist/esm/config-loader.d.ts +2 -0
  63. package/dist/esm/config-loader.d.ts.map +1 -1
  64. package/dist/esm/config-loader.js +2 -0
  65. package/dist/esm/config-loader.js.map +1 -1
  66. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +26 -6
  67. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
  68. package/dist/esm/core/anonymous-session/InboundContactForm.js +51 -33
  69. package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -1
  70. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +14 -3
  71. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
  72. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +28 -13
  73. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
  74. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +24 -1
  75. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
  76. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +46 -3
  77. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
  78. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts +36 -0
  79. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts.map +1 -0
  80. package/dist/esm/core/anonymous-session/website-submission-challenge.js +69 -0
  81. package/dist/esm/core/anonymous-session/website-submission-challenge.js.map +1 -0
  82. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts +6 -0
  83. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -0
  84. package/dist/esm/core/consent/WebsiteConsentBanner.js +129 -0
  85. package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -0
  86. package/dist/esm/core/consent/WebsiteConsentContext.d.ts +85 -0
  87. package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -0
  88. package/dist/esm/core/consent/WebsiteConsentContext.js +89 -0
  89. package/dist/esm/core/consent/WebsiteConsentContext.js.map +1 -0
  90. package/dist/esm/core/consent/WebsiteConsentGate.d.ts +27 -0
  91. package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -0
  92. package/dist/esm/core/consent/WebsiteConsentGate.js +9 -0
  93. package/dist/esm/core/consent/WebsiteConsentGate.js.map +1 -0
  94. package/dist/esm/core/consent/useWebsiteConsent.d.ts +10 -0
  95. package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -0
  96. package/dist/esm/core/consent/useWebsiteConsent.js +17 -0
  97. package/dist/esm/core/consent/useWebsiteConsent.js.map +1 -0
  98. package/dist/esm/core/consent/website-consent-storage.d.ts +22 -0
  99. package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -0
  100. package/dist/esm/core/consent/website-consent-storage.js +102 -0
  101. package/dist/esm/core/consent/website-consent-storage.js.map +1 -0
  102. package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
  103. package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -1
  104. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +33 -6
  105. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
  106. package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
  107. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -1
  108. package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -1
  109. package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -1
  110. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +18 -0
  111. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
  112. package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -2
  113. package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -1
  114. package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
  115. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts +57 -0
  116. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts.map +1 -0
  117. package/dist/esm/core/external-providers/WebsiteProviderComponent.js +56 -0
  118. package/dist/esm/core/external-providers/WebsiteProviderComponent.js.map +1 -0
  119. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +1 -2
  120. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
  121. package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -1
  122. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts +63 -0
  123. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts.map +1 -0
  124. package/dist/esm/core/external-providers/provider-component-registry.website.js +68 -0
  125. package/dist/esm/core/external-providers/provider-component-registry.website.js.map +1 -0
  126. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts +36 -0
  127. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts.map +1 -0
  128. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js +59 -0
  129. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js.map +1 -0
  130. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +43 -10
  131. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
  132. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +92 -29
  133. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
  134. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts +11 -0
  135. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts.map +1 -0
  136. package/dist/esm/core/external-providers/website-provider-sdk-activations.js +14 -0
  137. package/dist/esm/core/external-providers/website-provider-sdk-activations.js.map +1 -0
  138. package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
  139. package/dist/esm/core/factories/define-website-section.d.ts +3 -3
  140. package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
  141. package/dist/esm/core/factories/define-website-section.js +3 -3
  142. package/dist/esm/core/factories/define-website-section.js.map +1 -1
  143. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -1
  144. package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -1
  145. package/dist/esm/core/layouts/WebsitePageLayout.d.ts +21 -1
  146. package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
  147. package/dist/esm/core/layouts/WebsitePageLayout.js +39 -8
  148. package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
  149. package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -1
  150. package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -1
  151. package/dist/esm/index.d.ts +36 -65
  152. package/dist/esm/index.d.ts.map +1 -1
  153. package/dist/esm/index.js +36 -65
  154. package/dist/esm/index.js.map +1 -1
  155. package/dist/esm/mdx/BlogPost.d.ts.map +1 -1
  156. package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -1
  157. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -1
  158. package/dist/esm/mdx/index.d.ts.map +1 -1
  159. package/dist/esm/mdx.d.ts.map +1 -1
  160. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -1
  161. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +35 -6
  162. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
  163. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +39 -5
  164. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -1
  165. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts +61 -0
  166. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -0
  167. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js +73 -0
  168. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js.map +1 -0
  169. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
  170. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts +32 -0
  171. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts.map +1 -0
  172. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js +36 -0
  173. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js.map +1 -0
  174. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -1
  175. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +6 -5
  176. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
  177. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +8 -7
  178. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -1
  179. package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -1
  180. package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -1
  181. package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
  182. package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -1
  183. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +14 -7
  184. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
  185. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +14 -7
  186. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -1
  187. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts +57 -0
  188. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts.map +1 -0
  189. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js +76 -0
  190. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js.map +1 -0
  191. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +23 -0
  192. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -1
  193. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +20 -2
  194. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -1
  195. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +7 -5
  196. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -1
  197. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -1
  198. package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -1
  199. package/dist/tsconfig.build.tsbuildinfo +1 -1
  200. package/package.json +6 -29
  201. package/src/__tests__/bundle-isolation.test.ts +19 -22
  202. package/src/astro/__tests__/blog-post-bridge.test.tsx +58 -1
  203. package/src/astro/__tests__/bridge-runtime.test.tsx +44 -1
  204. package/src/astro/__tests__/label-pack-loader.test.ts +29 -1
  205. package/src/astro/blog-post-bridge.tsx +36 -26
  206. package/src/astro/bridge-runtime.tsx +79 -33
  207. package/src/astro/label-pack-loader.ts +12 -4
  208. package/src/astro/robots-renderer.ts +15 -15
  209. package/src/astro/website-page-head.renderer.ts +2 -2
  210. package/src/astro/website-site-context.ts +5 -2
  211. package/src/companion-exports.ts +8 -15
  212. package/src/config/__tests__/website-build-origins.test.ts +52 -0
  213. package/src/config/define-website-config.ts +10 -10
  214. package/src/config/load-website-config.ts +27 -15
  215. package/src/config/load-website-pricing-catalog.ts +52 -0
  216. package/src/config/load-website-site-context.ts +145 -0
  217. package/src/config/wildo-website-config.schemas.ts +10 -5
  218. package/src/config-loader.ts +2 -0
  219. package/src/core/__tests__/WebsitePageLayout.test.tsx +1 -1
  220. package/src/core/__tests__/contexts.test.tsx +20 -1
  221. package/src/core/__tests__/website-consent.test.tsx +312 -0
  222. package/src/core/anonymous-session/InboundContactForm.tsx +100 -45
  223. package/src/core/anonymous-session/__tests__/InboundContactForm.labels.test.tsx +108 -0
  224. package/src/core/anonymous-session/__tests__/InboundContactForm.submission-challenge.test.tsx +139 -0
  225. package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +11 -4
  226. package/src/core/anonymous-session/inbound-contact-form.schema.ts +30 -13
  227. package/src/core/anonymous-session/website-anonymous-session-client.ts +52 -3
  228. package/src/core/anonymous-session/website-submission-challenge.ts +99 -0
  229. package/src/core/consent/WebsiteConsentBanner.tsx +223 -0
  230. package/src/core/consent/WebsiteConsentContext.tsx +164 -0
  231. package/src/core/consent/WebsiteConsentGate.tsx +34 -0
  232. package/src/core/consent/useWebsiteConsent.ts +18 -0
  233. package/src/core/consent/website-consent-storage.ts +116 -0
  234. package/src/core/contexts/WebsiteRuntimeContext.tsx +33 -6
  235. package/src/core/contexts/useWebsiteRuntime.ts +33 -2
  236. package/src/core/external-providers/WebsiteProviderComponent.tsx +153 -0
  237. package/src/core/external-providers/__tests__/frontend-provider-registry.website.test.ts +4 -0
  238. package/src/core/external-providers/__tests__/website-provider-component.test.tsx +238 -0
  239. package/src/core/external-providers/__tests__/website-provider-sdks.test.tsx +72 -0
  240. package/src/core/external-providers/frontend-provider-registry.website.ts +1 -2
  241. package/src/core/external-providers/provider-component-registry.website.ts +115 -0
  242. package/src/core/external-providers/useWebsiteProviderComponent.ts +76 -0
  243. package/src/core/external-providers/useWebsiteProviderScripts.ts +114 -40
  244. package/src/core/external-providers/website-provider-sdk-activations.ts +23 -0
  245. package/src/core/factories/define-website-section.ts +3 -3
  246. package/src/core/layouts/WebsitePageLayout.tsx +75 -7
  247. package/src/index.ts +38 -65
  248. package/src/schemas/__tests__/design-token-css-property.test.ts +72 -0
  249. package/src/schemas/__tests__/pricing-offer-structured-data.test.ts +122 -0
  250. package/src/schemas/__tests__/structured-data-reconciliation.test.ts +35 -0
  251. package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +45 -5
  252. package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +83 -0
  253. package/src/schemas/label-keys/website-consent-label-keys.schemas.ts +78 -0
  254. package/src/schemas/label-keys/website-provider-component-label-keys.schemas.ts +40 -0
  255. package/src/schemas/manifests/website-root-config.shared.schemas.ts +10 -9
  256. package/src/schemas/sections/website-section-definition.shared.schemas.ts +14 -7
  257. package/src/schemas/structured-data/pricing-offer-structured-data.shared.ts +144 -0
  258. package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +46 -1
  259. package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +7 -5
  260. package/dist/esm/core/external-providers/provider-scripts.website.d.ts +0 -68
  261. package/dist/esm/core/external-providers/provider-scripts.website.d.ts.map +0 -1
  262. package/dist/esm/core/external-providers/provider-scripts.website.js +0 -99
  263. package/dist/esm/core/external-providers/provider-scripts.website.js.map +0 -1
  264. package/src/core/external-providers/__tests__/provider-scripts.website.test.ts +0 -146
  265. package/src/core/external-providers/provider-scripts.website.ts +0 -126
@@ -4,6 +4,8 @@ import { resolve } from 'node:path';
4
4
  import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
5
5
 
6
6
  import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
7
+ import { WEBSITE_CONSENT_LABEL_KEYS } from '../schemas/label-keys/website-consent-label-keys.schemas';
8
+ import { WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS } from '../schemas/label-keys/website-provider-component-label-keys.schemas';
7
9
  import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
8
10
  import type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';
9
11
  import {
@@ -20,10 +22,10 @@ import type { WebsiteSiteContext } from './website-site-context';
20
22
  *
21
23
  * `label-pack-loader.ts` — synchronous, fs-backed loader for
22
24
  * marketing-site label packs. Consumed by:
23
- * - the Astro adapter (`WebsitePageBridge.astro` reads the active
24
- * locale's pack at request time during SSR / pre-render),
25
- * - the `wildo website audit` CLI (Phase 7) to validate every
26
- * locale pack against every manifest at CI time,
25
+ * - each page's `.astro` frontmatter, through `loadWebsitePageRuntime`,
26
+ * which reads the active locale's pack during SSR / pre-render,
27
+ * - `wildo website audit`, which validates every locale pack against
28
+ * every manifest and section through `loadAndValidateAllLabelPacks`,
27
29
  * - any build-time hook that wants to fail fast on a missing or
28
30
  * malformed pack.
29
31
  *
@@ -207,6 +209,12 @@ function composeEffectiveChromeLabelKeys(
207
209
  ): readonly WebsiteLabelKey[] {
208
210
  const set = new Set<WebsiteLabelKey>(rootConfig.chromeLabelKeys);
209
211
  set.add(rootConfig.skipToMainContentLabelKey);
212
+ // The visitor consent panel is framework-owned in the same way (#534): required in every pack, so a site
213
+ // that turns on analytics can never ship it with an unlabelled way to refuse.
214
+ for (const key of WEBSITE_CONSENT_LABEL_KEYS) set.add(key);
215
+ // So is the provider-component slot's refusal wording (#1703): a site placing an address lookup must not
216
+ // ship its consent-required or unavailable state as a raw key.
217
+ for (const key of WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS) set.add(key);
210
218
  return Array.from(set);
211
219
  }
212
220
 
@@ -34,10 +34,12 @@ import type { WebsiteSiteContext } from './website-site-context';
34
34
  * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default
35
35
  * direction) — each named bot gets its own `User-agent: <bot>`
36
36
  * + `Disallow: /` block.
37
- * 3. Catch-all `User-agent: *` block carrying the site-wide
38
- * `policy` (`Disallow: /` if `policy === 'noindex'` OR if
39
- * `allowedCrawlers` is set, otherwise nothing) and any
40
- * `disallow[]` path-prefix entries.
37
+ * 3. Catch-all `User-agent: *` block: `Disallow: /` only when
38
+ * `allowedCrawlers` is set (the named bots are then the only ones
39
+ * permitted), otherwise the allow-everything `Disallow:` plus any
40
+ * `disallow[]` path-prefix entries. `policy === 'noindex'` adds
41
+ * nothing here — it is expressed by the page's robots meta tag,
42
+ * which a crawler can only read if it may fetch the page (#494).
41
43
  * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when
42
44
  * `seo.sitemap?.enabled !== false` (defaults to `true`).
43
45
  * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of
@@ -57,10 +59,9 @@ import type { WebsiteSiteContext } from './website-site-context';
57
59
  *
58
60
  * The `Sitemap:` directive is independent from any `User-agent`
59
61
  * block — it is a top-level signal to any crawler that the site
60
- * publishes a sitemap. Even when the catch-all policy is
61
- * `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line
62
- * stays useful: a crawler that ignores `noindex` (Bing was famous
63
- * for this) at least has a single canonical URL list to consult,
62
+ * publishes a sitemap. Even when the catch-all is `Disallow: /`
63
+ * (`allowedCrawlers` set), the `Sitemap:` line stays useful: the
64
+ * permitted crawlers have a single canonical URL list to consult,
64
65
  * and human readers of `robots.txt` see the publication contract
65
66
  * intact.
66
67
  *
@@ -120,13 +121,12 @@ export function renderRobotsTxt(siteContext: WebsiteSiteContext): string {
120
121
  }
121
122
 
122
123
  /**
123
- * Block 3 — catch-all `User-agent: *`. Two policy-driven shapes:
124
- * - `noindex` → `Disallow: /` (the site-wide closure).
125
- * - `index` (default) → `Disallow:` is the spec-canonical "allow
126
- * everything" line; combined with any per-path `Disallow: <path>`
127
- * entries from `disallow[]`. When `allowedCrawlers` is set, the
128
- * catch-all also closes the rest of the crawler universe with
129
- * `Disallow: /` so the named bots are the ONLY ones permitted.
124
+ * Block 3 — catch-all `User-agent: *`. `Disallow:` is the
125
+ * spec-canonical "allow everything" line, combined with any per-path
126
+ * `Disallow: <path>` entries from `disallow[]`. When `allowedCrawlers`
127
+ * is set, the catch-all instead closes the rest of the crawler universe
128
+ * with `Disallow: /` so the named bots are the ONLY ones permitted. The
129
+ * `policy` is not expressed here at all — see below.
130
130
  */
131
131
  const catchAllLines: string[] = ['User-agent: *'];
132
132
  /*
@@ -1,7 +1,7 @@
1
1
  import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
2
 
3
3
  import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';
4
- import type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
4
+ import { websiteDesignTokenCssProperty, type WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
5
5
  import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
6
6
  import type { LabelPack } from '../schemas/validators/label-pack.validator';
7
7
 
@@ -277,7 +277,7 @@ export function renderWebsiteDesignTokensCss(tokens: WebsiteDesignTokens): strin
277
277
  if (groupValue !== null && typeof groupValue === 'object' && !Array.isArray(groupValue)) {
278
278
  for (const [tokenName, tokenValue] of Object.entries(groupValue) as [string, unknown][]) {
279
279
  if (typeof tokenValue === 'string') {
280
- lines.push(` --website-${groupName}-${tokenName}: ${tokenValue};`);
280
+ lines.push(` ${websiteDesignTokenCssProperty(groupName, tokenName)}: ${tokenValue};`);
281
281
  }
282
282
  }
283
283
  } else if (typeof groupValue === 'string') {
@@ -10,7 +10,8 @@ import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtim
10
10
  * `WebsiteRootConfig` (page registry, design tokens, navigation chrome,
11
11
  * site-level SEO branding) with the per-service `WildoMarketingWebsiteConfig`
12
12
  * loaded from `wildo.website.config.ts` (locales, default locale,
13
- * public origin, future `appOrigin` cross-origin handoff target).
13
+ * public origin, and the `appOrigin` / `apiOrigin` the site reaches the
14
+ * product through — settled for THIS build by `loadWebsiteSiteContext`).
14
15
  *
15
16
  * # Why a dedicated aggregator type (vs. threading both raw configs)
16
17
  *
@@ -84,7 +85,9 @@ export interface WebsiteSiteContext {
84
85
  * The per-service `WildoMarketingWebsiteConfig` loaded from
85
86
  * `wildo.website.config.ts`. Single source of truth for the
86
87
  * marketing-site ROUTING + ORIGIN surface (`defaultLocale`,
87
- * `locales`, `publicOrigin`, future `appOrigin`).
88
+ * `locales`, `publicOrigin`, `appOrigin`, `apiOrigin`). The two origins
89
+ * are the values for THIS build (override, local dev, authored — see
90
+ * `resolveWebsiteBuildOrigins`), which is why every page reads them here.
88
91
  */
89
92
  readonly websiteConfig: WildoMarketingWebsiteConfig;
90
93
  /**
@@ -8,22 +8,15 @@
8
8
  * - anything Astro-build-time that cannot pull React into the
9
9
  * evaluation graph
10
10
  *
11
- * Per `saas-website-slice.md` §"Note on `companion-exports.ts` scope
12
- * expansion": the SEO builder library lives here too (it's pure
13
- * string + JSON-LD generation, React-free by design).
14
- *
15
11
  * @wildo-boundary
16
- * This file MUST re-export ONLY from `schemas/**` and `seo/**`.
17
- * It MUST NOT import or re-export anything from:
18
- * - `core/**` (React runtime contexts/hooks)
19
- * - `astro/**` (Astro-only adapter)
20
- * - `mdx/**` (MDX-only adapter)
21
- * - `integration/**` (React-bearing integration code)
22
- * It MUST NOT import React, Astro, or `@mdx-js/*`.
23
- *
24
- * Enforced by review until Phase 8 boundary lint design (see plan).
25
- *
26
- * Phase 1 status: schemas exposed; SEO builders land in Phase 2.5.
12
+ * This file re-exports ONLY from `schemas/**`. It MUST NOT import or
13
+ * re-export anything from `core/**` (React contexts/hooks), `astro/**`
14
+ * (Astro/Node adapter), `mdx/**` or `config/**`, and MUST NOT import
15
+ * React, Astro or `@mdx-js/*`. The SEO and structured-data renderers
16
+ * are React-free string builders too, but they live on `/astro` beside
17
+ * the build-time helpers that call them, and are reached from there.
18
+ * Enforced by `src/__tests__/bundle-isolation.test.ts` and
19
+ * `schemas/__tests__/companion-exports-react-free.test.ts`.
27
20
  */
28
21
 
29
22
  export * from './schemas/refs/page-ref.schemas';
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
4
+
5
+ import { resolveWebsiteBuildOrigins } from '../load-website-site-context';
6
+ import type { WildoMarketingWebsiteConfig } from '../wildo-website-config.schemas';
7
+
8
+ const AUTHORED: WildoMarketingWebsiteConfig = {
9
+ publicOrigin: 'https://example.com',
10
+ appOrigin: 'https://app.example.com',
11
+ apiOrigin: 'https://api.example.com',
12
+ defaultLocale: AvailableLanguage.EN,
13
+ locales: [{ locale: AvailableLanguage.EN, path: 'en' }],
14
+ } as WildoMarketingWebsiteConfig;
15
+
16
+ const LOCAL = { app: 'http://localhost:4272', api: 'http://localhost:4271' };
17
+
18
+ describe('resolveWebsiteBuildOrigins', () => {
19
+ it('under astro dev, links into the locally running product and API', () => {
20
+ const resolved = resolveWebsiteBuildOrigins({ websiteConfig: AUTHORED, isDevelopment: true, localOrigins: LOCAL, overrides: {} });
21
+ expect(resolved.appOrigin).toBe('http://localhost:4272');
22
+ expect(resolved.apiOrigin).toBe('http://localhost:4271');
23
+ expect(resolved.publicOrigin).toBe('https://example.com');
24
+ });
25
+
26
+ it('a production build keeps the authored origins, never a localhost one', () => {
27
+ const resolved = resolveWebsiteBuildOrigins({ websiteConfig: AUTHORED, isDevelopment: false, localOrigins: LOCAL, overrides: {} });
28
+ expect(resolved.appOrigin).toBe('https://app.example.com');
29
+ expect(resolved.apiOrigin).toBe('https://api.example.com');
30
+ });
31
+
32
+ it('an explicit per-environment override wins in both modes', () => {
33
+ const overrides = { appOrigin: 'https://app.staging.example.com', apiOrigin: 'https://api.staging.example.com' };
34
+ for (const isDevelopment of [true, false]) {
35
+ const resolved = resolveWebsiteBuildOrigins({ websiteConfig: AUTHORED, isDevelopment, localOrigins: LOCAL, overrides });
36
+ expect(resolved.appOrigin).toBe(overrides.appOrigin);
37
+ expect(resolved.apiOrigin).toBe(overrides.apiOrigin);
38
+ }
39
+ });
40
+
41
+ it('falls back to the authored origin when the app declares no local port', () => {
42
+ const resolved = resolveWebsiteBuildOrigins({ websiteConfig: AUTHORED, isDevelopment: true, localOrigins: {}, overrides: {} });
43
+ expect(resolved.appOrigin).toBe('https://app.example.com');
44
+ expect(resolved.apiOrigin).toBe('https://api.example.com');
45
+ });
46
+
47
+ it('leaves apiOrigin absent when nothing supplies one, so a capture form can refuse loudly', () => {
48
+ const { apiOrigin: _omitted, ...withoutApi } = AUTHORED;
49
+ const resolved = resolveWebsiteBuildOrigins({ websiteConfig: withoutApi as WildoMarketingWebsiteConfig, isDevelopment: false, localOrigins: LOCAL, overrides: {} });
50
+ expect('apiOrigin' in resolved).toBe(false);
51
+ });
52
+ });
@@ -13,8 +13,9 @@
13
13
  *
14
14
  * The warner has ZERO branches today because every OPTIONAL field that
15
15
  * exists is already wired (`csp?` now flows through the website-local
16
- * CSP sync step) and the only unwired field left is `appOrigin`, which
17
- * is REQUIRED. After the seam-collapse (`saas-website-seam-collapse.md`
16
+ * CSP sync step), and every REQUIRED field has a runtime consumer too —
17
+ * `appOrigin` reaches every CTA through `WebsiteRuntimeContext.appOrigin`,
18
+ * settled per build by `loadWebsiteSiteContext`. After the seam-collapse (`saas-website-seam-collapse.md`
18
19
  * — SHIPPED), the routing/origin fields (`publicOrigin`,
19
20
  * `defaultLocale`, `locales`) are consumed directly by every Astro
20
21
  * engine helper via `WebsiteSiteContext`, so they are no longer
@@ -99,14 +100,13 @@ export function defineWebsiteConfig(
99
100
  * but has no runtime consumer yet.
100
101
  *
101
102
  * Branches today: ZERO. The one OPTIONAL field that exists (`csp?`) is
102
- * fully wired through the website-local CSP sync/build path, while the
103
- * only field that still lacks a runtime consumer (`appOrigin`) is
104
- * REQUIRED. After the seam-collapse (`saas-website-seam-collapse.md` —
105
- * SHIPPED) the routing/origin fields are consumed directly by every
106
- * Astro engine helper via `WebsiteSiteContext`. Required-but-unwired
107
- * fields cannot reasonably fire on every parse — that's a
108
- * deferred-wiring surface tracked in the schema header, not an
109
- * `@unimplemented` warner concern.
103
+ * fully wired through the website-local CSP sync/build path. After the
104
+ * seam-collapse (`saas-website-seam-collapse.md` — SHIPPED) the
105
+ * routing/origin fields are consumed directly by every Astro engine
106
+ * helper via `WebsiteSiteContext`, and `appOrigin` / `apiOrigin` are
107
+ * settled per build by `loadWebsiteSiteContext` and read by every page.
108
+ * A required field could never fire this warner anyway — it would fire
109
+ * on every parse.
110
110
  *
111
111
  * The function exists as a structural mirror of
112
112
  * `defineTechnicalDocConfig.emitUnimplementedRuntimeWarnings` so
@@ -37,6 +37,7 @@ import { AppFrontendType, loadConfig } from '@wildo-ai/platform-config-lib';
37
37
  import type { WildoSaasConfig } from '@wildo-ai/platform-config-lib';
38
38
  import type { WildoMarketingWebsiteConfig } from './wildo-website-config.schemas';
39
39
 
40
+ import { describeThrownValue } from '@wildo-ai/saas-models/public-runtime';
40
41
  /**
41
42
  * Conventional file name for the per-service marketing-website
42
43
  * configuration (Step 7.5 refactor — `saas-website-slice.md` /
@@ -125,6 +126,30 @@ export interface LoadWebsiteConfigArgs {
125
126
  saasConfig: WildoSaasConfig;
126
127
  }
127
128
 
129
+ /**
130
+ * The application's one `STATIC_WEBSITE` service, as `[serviceKey, service]`.
131
+ * Throws when none is declared: the service folder is what owns
132
+ * `wildo.website.config.ts`, so a website build without one has nothing to load.
133
+ */
134
+ export function resolveStaticWebsiteServiceEntry(
135
+ saasConfig: WildoSaasConfig,
136
+ ): [string, WildoSaasConfig['services'][string]] {
137
+ const websiteServiceEntry = Object.entries(saasConfig.services).find(
138
+ ([, service]) => service.frontendType === AppFrontendType.STATIC_WEBSITE,
139
+ );
140
+ if (!websiteServiceEntry) {
141
+ throw new Error(
142
+ '[wildo-website-config] No frontend service with `frontendType: STATIC_WEBSITE` is declared in '
143
+ + 'wildo.saas.config.ts. The marketing-website Astro build / loader requires an authored '
144
+ + 'STATIC_WEBSITE service because that service folder owns the per-service '
145
+ + '`wildo.website.config.ts` (publicOrigin, appOrigin, defaultLocale, locales). '
146
+ + 'Add a STATIC_WEBSITE service in wildo.saas.config.ts and ship the per-service '
147
+ + 'config file under it.',
148
+ );
149
+ }
150
+ return websiteServiceEntry;
151
+ }
152
+
128
153
  /**
129
154
  * Discover and load the per-service marketing-website config.
130
155
  *
@@ -168,20 +193,7 @@ export async function loadWebsiteConfig(
168
193
  args: LoadWebsiteConfigArgs,
169
194
  ): Promise<WildoMarketingWebsiteConfig> {
170
195
  const { saasRoot, saasConfig } = args;
171
-
172
- const websiteServiceEntry = Object.entries(saasConfig.services).find(
173
- ([, service]) => service.frontendType === AppFrontendType.STATIC_WEBSITE,
174
- );
175
- if (!websiteServiceEntry) {
176
- throw new Error(
177
- '[wildo-website-config] No frontend service with `frontendType: STATIC_WEBSITE` is declared in '
178
- + 'wildo.saas.config.ts. The marketing-website Astro build / loader requires an authored '
179
- + 'STATIC_WEBSITE service because that service folder owns the per-service '
180
- + '`wildo.website.config.ts` (publicOrigin, appOrigin, defaultLocale, locales). '
181
- + 'Add a STATIC_WEBSITE service in wildo.saas.config.ts and ship the per-service '
182
- + 'config file under it.',
183
- );
184
- }
196
+ const websiteServiceEntry = resolveStaticWebsiteServiceEntry(saasConfig);
185
197
 
186
198
  const [websiteServiceKey, websiteService] = websiteServiceEntry;
187
199
  const websiteConfigPath = resolvePath(
@@ -213,7 +225,7 @@ export async function loadWebsiteConfig(
213
225
  try {
214
226
  loaded = await loadConfig<WildoMarketingWebsiteConfig>(websiteConfigPath);
215
227
  } catch (e) {
216
- const cause = e instanceof Error ? e.message : String(e);
228
+ const cause = describeThrownValue(e);
217
229
  throw new Error(
218
230
  `[wildo-website-config] Failed to load per-service marketing-website config at `
219
231
  + `${websiteConfigPath}. The file MUST do `
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @wildo_source:part:start saas.website.config.pricing-catalog-loader facet:layer:astro facet:family:website facet:audience:app-developer
3
+ *
4
+ * `loadWebsitePricingCatalog` — the application's public pricing catalogue, for a website's pricing
5
+ * page (#1835). Call it in the page's `.astro` frontmatter, pass the result to the bridge island as
6
+ * `pricingCatalog`, and read it in the section with `useWebsitePricingCatalog()`; build the page's
7
+ * `AggregateOffer` from the same value with `buildWebsitePricingOfferStructuredData`.
8
+ *
9
+ * It walks up to `wildo.saas.config.ts` the way `loadWebsiteSiteContext` does, and reads the billing
10
+ * catalogue the backend sells from — the `productDefinitions` of the application's shared modules —
11
+ * through `loadApplicationPublicPricingCatalog`, from SOURCE, so a website image that builds only the
12
+ * website still sees the current catalogue.
13
+ *
14
+ * Separate from `loadWebsiteSiteContext` on purpose: loading the shared graph costs seconds, and only a
15
+ * pricing page needs it. Every other page's build stays as fast as it was.
16
+ *
17
+ * **Node-only.** Lives on the `/config-loader` subpath; it must never reach a React-island bundle.
18
+ */
19
+ import {
20
+ loadApplicationPublicPricingCatalog,
21
+ loadConfig,
22
+ resolveWorkspaceScope,
23
+ type WildoSaasConfig,
24
+ } from '@wildo-ai/platform-config-lib';
25
+ import type { PublicPricingCatalog } from '@wildo-ai/saas-models/public-runtime';
26
+
27
+ export interface LoadWebsitePricingCatalogInput {
28
+ /** Where to start the walk up to `wildo.saas.config.ts`. Defaults to the working directory. */
29
+ readonly cwd?: string | undefined;
30
+ /**
31
+ * Keep only plans this scope may subscribe to (a `ResourcePrimaryScope` wire value, e.g.
32
+ * `'organizations'`). Omit when the catalogue sells to one audience.
33
+ */
34
+ readonly targetScope?: string | undefined;
35
+ }
36
+
37
+ export async function loadWebsitePricingCatalog(input: LoadWebsitePricingCatalogInput = {}): Promise<PublicPricingCatalog> {
38
+ const workspaceScope = resolveWorkspaceScope(input.cwd);
39
+ if (workspaceScope.saasRoot === null || workspaceScope.saasConfigPath === null) {
40
+ throw new Error(
41
+ `[saas-website] No wildo.saas.config.ts found walking up from ${input.cwd ?? process.cwd()}. `
42
+ + 'Run astro from inside the application workspace.',
43
+ );
44
+ }
45
+ const saasConfig = await loadConfig<WildoSaasConfig>(workspaceScope.saasConfigPath);
46
+ return loadApplicationPublicPricingCatalog({
47
+ saasRoot: workspaceScope.saasRoot,
48
+ saasConfig,
49
+ ...(input.targetScope === undefined ? {} : { targetScope: input.targetScope }),
50
+ });
51
+ }
52
+ /** @wildo_source:part:end saas.website.config.pricing-catalog-loader */
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @wildo_source:part:start saas.website.config.site-context-loader facet:layer:astro facet:family:website facet:audience:app-developer
3
+ *
4
+ * `loadWebsiteSiteContext` — the ONE way a website workspace builds its
5
+ * `WebsiteSiteContext`. Called once, from the workspace's
6
+ * `src/website-site-context.ts`:
7
+ *
8
+ * ```ts
9
+ * import { loadWebsiteSiteContext } from '@wildo-ai/saas-website/config-loader';
10
+ * import { WEBSITE_ROOT_CONFIG } from './website-root.config';
11
+ *
12
+ * export const WEBSITE_SITE_CONTEXT = await loadWebsiteSiteContext({
13
+ * rootConfig: WEBSITE_ROOT_CONFIG,
14
+ * isDevelopment: import.meta.env?.DEV === true,
15
+ * });
16
+ * ```
17
+ *
18
+ * It walks up from the working directory to `wildo.saas.config.ts`, finds the
19
+ * application's single `STATIC_WEBSITE` service, loads that service's
20
+ * `wildo.website.config.ts` and its materialized provider block, and settles the
21
+ * two origins the site reaches the product through (see
22
+ * {@link resolveWebsiteBuildOrigins}).
23
+ *
24
+ * **Why the engine owns this.** Every website workspace used to carry its own
25
+ * copy of this lookup — the scaffolded ones and the hand-written ones alike —
26
+ * and each copy hardcoded its local API port while the app origin had no local
27
+ * form at all. The result: under `astro dev`, every "Start free" CTA pointed at a
28
+ * production-shaped host that resolves to nothing. One loader means one rule.
29
+ *
30
+ * **Why it walks up from the working directory, never from `import.meta.url`.**
31
+ * Astro's prerender pass bundles the calling module into `dist/.prerender/chunks/…`,
32
+ * where a `new URL('../..', import.meta.url)` anchor resolves to `dist/` rather than
33
+ * the application root — every prerendered route then failed with "Config file not
34
+ * found: …/website/dist/wildo.saas.config.ts", while `astro dev` hid it by serving
35
+ * from `src/`. Every `astro dev` / `astro build` runs with the website package (or
36
+ * an ancestor) as its working directory, which is inside the application either way.
37
+ *
38
+ * **Node-only.** Lives on the `/config-loader` subpath with the other loaders; it
39
+ * must never reach a React-island bundle.
40
+ */
41
+ import { resolve as resolvePath } from 'node:path';
42
+
43
+ import {
44
+ loadConfig,
45
+ loadMaterializedFrontendProviders,
46
+ resolveFrontendServicesArtifactPath,
47
+ resolveLocalhostApplicationOrigins,
48
+ resolveWorkspaceScope,
49
+ type LocalhostApplicationOrigins,
50
+ type WildoSaasConfig,
51
+ } from '@wildo-ai/platform-config-lib';
52
+
53
+ import { composeWebsiteSiteContext, type WebsiteSiteContext } from '../astro/website-site-context';
54
+ import type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';
55
+ import { loadWebsiteConfig, resolveStaticWebsiteServiceEntry } from './load-website-config';
56
+ import type { WildoMarketingWebsiteConfig } from './wildo-website-config.schemas';
57
+
58
+ /** Explicit per-build origin overrides — what a deploy build passes as build arguments. */
59
+ export interface WebsiteOriginOverrides {
60
+ /** `PUBLIC_APP_ORIGIN`: this environment's product origin, derived by the deploy workflow. */
61
+ readonly appOrigin?: string | undefined;
62
+ /** `PUBLIC_API_ORIGIN`: this environment's API origin. */
63
+ readonly apiOrigin?: string | undefined;
64
+ }
65
+
66
+ export interface ResolveWebsiteBuildOriginsInput {
67
+ /** The authored `wildo.website.config.ts` — the production default. */
68
+ readonly websiteConfig: WildoMarketingWebsiteConfig;
69
+ /** `true` under `astro dev`. */
70
+ readonly isDevelopment: boolean;
71
+ /** Where the product and API run locally, from the app's own declared ports. */
72
+ readonly localOrigins: LocalhostApplicationOrigins;
73
+ /** Explicit overrides, which win over everything. */
74
+ readonly overrides: WebsiteOriginOverrides;
75
+ }
76
+
77
+ /**
78
+ * The `websiteConfig` this BUILD serves, with `appOrigin` and `apiOrigin` settled.
79
+ *
80
+ * Precedence, per origin:
81
+ *
82
+ * 1. an explicit override (`PUBLIC_APP_ORIGIN` / `PUBLIC_API_ORIGIN`) — the deploy
83
+ * workflow derives these per environment, so a staging build links into staging;
84
+ * 2. under `astro dev`, the locally running product / API, from the ports the
85
+ * application declares — so a CTA reaches the process `wildo local dev` started;
86
+ * 3. the value authored in `wildo.website.config.ts`.
87
+ *
88
+ * The result replaces the two fields in `websiteConfig` rather than living beside
89
+ * it, because every page already forwards `websiteConfig.appOrigin` /
90
+ * `websiteConfig.apiOrigin` into its island: a second field would be a second
91
+ * source that pages could read instead. `websiteConfig` in a site context is
92
+ * therefore "the config as it applies to this build", which the config file was
93
+ * already half-way to being — it reads `PUBLIC_APP_ORIGIN` itself.
94
+ */
95
+ export function resolveWebsiteBuildOrigins(input: ResolveWebsiteBuildOriginsInput): WildoMarketingWebsiteConfig {
96
+ const { websiteConfig, isDevelopment, localOrigins, overrides } = input;
97
+ const settle = (override: string | undefined, local: string | undefined, authored: string | undefined): string | undefined =>
98
+ override || (isDevelopment ? local : undefined) || authored;
99
+ const appOrigin = settle(overrides.appOrigin, localOrigins.app, websiteConfig.appOrigin) ?? websiteConfig.appOrigin;
100
+ const apiOrigin = settle(overrides.apiOrigin, localOrigins.api, websiteConfig.apiOrigin);
101
+ return { ...websiteConfig, appOrigin, ...(apiOrigin === undefined ? {} : { apiOrigin }) };
102
+ }
103
+
104
+ export interface LoadWebsiteSiteContextInput {
105
+ /** The workspace's `WEBSITE_ROOT_CONFIG`. */
106
+ readonly rootConfig: WebsiteRootConfig;
107
+ /** Pass `import.meta.env?.DEV === true` (optional chaining: `astro.config.ts` evaluates the caller before Vite's env exists). */
108
+ readonly isDevelopment: boolean;
109
+ /** Where to start the walk up to `wildo.saas.config.ts`. Defaults to the working directory. */
110
+ readonly cwd?: string | undefined;
111
+ /** Defaults to `PUBLIC_APP_ORIGIN` / `PUBLIC_API_ORIGIN` from the process environment. */
112
+ readonly overrides?: WebsiteOriginOverrides | undefined;
113
+ }
114
+
115
+ export async function loadWebsiteSiteContext(input: LoadWebsiteSiteContextInput): Promise<WebsiteSiteContext> {
116
+ const workspaceScope = resolveWorkspaceScope(input.cwd);
117
+ if (workspaceScope.saasRoot === null || workspaceScope.saasConfigPath === null) {
118
+ throw new Error(
119
+ `[saas-website] No wildo.saas.config.ts found walking up from ${input.cwd ?? process.cwd()}. `
120
+ + 'Run astro from inside the application workspace.',
121
+ );
122
+ }
123
+ const saasRoot = workspaceScope.saasRoot;
124
+ const saasConfig = await loadConfig<WildoSaasConfig>(workspaceScope.saasConfigPath);
125
+ const [websiteServiceKey] = resolveStaticWebsiteServiceEntry(saasConfig);
126
+ const websiteConfig = await loadWebsiteConfig({ saasRoot, saasConfig });
127
+
128
+ return composeWebsiteSiteContext({
129
+ rootConfig: input.rootConfig,
130
+ websiteConfig: resolveWebsiteBuildOrigins({
131
+ websiteConfig,
132
+ isDevelopment: input.isDevelopment,
133
+ localOrigins: resolveLocalhostApplicationOrigins(saasConfig.services),
134
+ overrides: input.overrides ?? {
135
+ appOrigin: process.env.PUBLIC_APP_ORIGIN,
136
+ apiOrigin: process.env.PUBLIC_API_ORIGIN,
137
+ },
138
+ }),
139
+ frontendProviders: loadMaterializedFrontendProviders({
140
+ artifactPath: resolveFrontendServicesArtifactPath(resolvePath(saasRoot)),
141
+ serviceKey: websiteServiceKey,
142
+ }),
143
+ });
144
+ }
145
+ /** @wildo_source:part:end saas.website.config.site-context-loader */
@@ -195,11 +195,16 @@ export const WildoMarketingWebsiteConfigSchema = z
195
195
  * Origin of the SaaS application the marketing site links into
196
196
  * (signup / login / pricing → checkout).
197
197
  *
198
- * **Today**: NO runtime consumer. The cross-origin handoff
199
- * surface that will read it does not exist yet. Authored
200
- * eagerly so the link-surface PR is a single framework-owned move
201
- * — until then the field is exercised only by the schema
202
- * parser.
198
+ * Read at runtime since #493: each `.astro` page forwards it to its
199
+ * bridge island, and the CTA reads it off `WebsiteRuntimeContext`.
200
+ *
201
+ * Per environment: a deployment's app lives at a different host in each
202
+ * environment, and a static site bakes the value in at build time, so the
203
+ * generated deploy workflow passes the environment's app origin as
204
+ * `PUBLIC_APP_ORIGIN` — derived from the infra config's `serviceHosts` and
205
+ * `publicDomain`, not authored a second time. An application's
206
+ * `wildo.website.config.ts` reads that first and keeps its authored value
207
+ * as the default for a build that passes none.
203
208
  *
204
209
  * Distinct from the general `WildoSaasConfig.services.<name>`
205
210
  * URL surface because the marketing site is a separate Astro
@@ -33,3 +33,5 @@
33
33
  */
34
34
 
35
35
  export * from './config/load-website-config';
36
+ export * from './config/load-website-site-context';
37
+ export * from './config/load-website-pricing-catalog';
@@ -5,7 +5,7 @@ import '@testing-library/jest-dom';
5
5
  import { BUILTIN_PROVIDER_CAPABILITY, defineProviderMetadata, ProviderOriginKind, ProviderTier } from '@wildo-ai/saas-models/public-runtime';
6
6
 
7
7
  import { WebsitePageLayout } from '../layouts/WebsitePageLayout';
8
- import { PROVIDER_SCRIPT_MARKER_ATTRIBUTE } from '../external-providers/provider-scripts.website';
8
+ import { PROVIDER_SCRIPT_MARKER_ATTRIBUTE } from '@wildo-ai/external-connectors-public/website';
9
9
  import { createFrontendWebsiteProviderRegistry } from '../external-providers/frontend-provider-registry.website';
10
10
  import {
11
11
  WebsiteRuntimeContextProvider,
@@ -7,7 +7,7 @@ import {
7
7
  WebsiteRuntimeContextProvider,
8
8
  type WebsiteRuntimeContextValue,
9
9
  } from '../contexts/WebsiteRuntimeContext';
10
- import { useWebsiteRuntime } from '../contexts/useWebsiteRuntime';
10
+ import { useWebsitePricingCatalog, useWebsiteRuntime } from '../contexts/useWebsiteRuntime';
11
11
  import { WebsitePageContextProvider } from '../contexts/WebsitePageContext';
12
12
  import { useWebsitePage } from '../contexts/useWebsitePage';
13
13
  import { WebsiteSectionContextProvider } from '../contexts/WebsiteSectionContext';
@@ -76,6 +76,25 @@ describe('WebsiteRuntimeContext', () => {
76
76
  });
77
77
  });
78
78
 
79
+ describe('useWebsitePricingCatalog', () => {
80
+ const wrapperWith = (value: WebsiteRuntimeContextValue) => ({ children }: { children: React.ReactNode }) => (
81
+ <WebsiteRuntimeContextProvider value={value}>{children}</WebsiteRuntimeContextProvider>
82
+ );
83
+
84
+ it('returns the catalogue the page passed to its island', () => {
85
+ const catalog = { plans: [] };
86
+ const { result } = renderHook(() => useWebsitePricingCatalog(), { wrapper: wrapperWith({ ...RUNTIME_FIXTURE, pricingCatalog: catalog }) });
87
+ expect(result.current).toBe(catalog);
88
+ });
89
+
90
+ it('fails loudly when the page passed none — a pricing section must never render "no plans" for a missing prop', () => {
91
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
92
+ expect(() => renderHook(() => useWebsitePricingCatalog(), { wrapper: wrapperWith(RUNTIME_FIXTURE) })).toThrowError(
93
+ /did not pass a pricing catalogue/,
94
+ );
95
+ });
96
+ });
97
+
79
98
  describe('WebsitePageContext', () => {
80
99
  it('provides the page ref through the provider component', () => {
81
100
  const pageRef = WebsitePageRefSchema.parse('landing');