@wildo-ai/saas-website 1.1.5 → 1.1.7

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 (225) hide show
  1. package/dist/esm/astro/blog-post-bridge.d.ts +9 -0
  2. package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
  3. package/dist/esm/astro/blog-post-bridge.js +10 -5
  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 +25 -6
  7. package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
  8. package/dist/esm/astro/bridge-runtime.js +10 -6
  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 +9 -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 +18 -9
  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.map +1 -1
  59. package/dist/esm/config-loader.d.ts +2 -0
  60. package/dist/esm/config-loader.d.ts.map +1 -1
  61. package/dist/esm/config-loader.js +2 -0
  62. package/dist/esm/config-loader.js.map +1 -1
  63. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +26 -6
  64. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
  65. package/dist/esm/core/anonymous-session/InboundContactForm.js +51 -33
  66. package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -1
  67. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +11 -0
  68. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
  69. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +15 -4
  70. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
  71. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +14 -2
  72. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
  73. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +34 -4
  74. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
  75. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts +36 -0
  76. package/dist/esm/core/anonymous-session/website-submission-challenge.d.ts.map +1 -0
  77. package/dist/esm/core/anonymous-session/website-submission-challenge.js +69 -0
  78. package/dist/esm/core/anonymous-session/website-submission-challenge.js.map +1 -0
  79. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -1
  80. package/dist/esm/core/consent/WebsiteConsentBanner.js +11 -10
  81. package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -1
  82. package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -1
  83. package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -1
  84. package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -1
  85. package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -1
  86. package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
  87. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +25 -5
  88. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
  89. package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
  90. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -1
  91. package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -1
  92. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +18 -0
  93. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
  94. package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -2
  95. package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -1
  96. package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
  97. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts +57 -0
  98. package/dist/esm/core/external-providers/WebsiteProviderComponent.d.ts.map +1 -0
  99. package/dist/esm/core/external-providers/WebsiteProviderComponent.js +56 -0
  100. package/dist/esm/core/external-providers/WebsiteProviderComponent.js.map +1 -0
  101. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
  102. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts +63 -0
  103. package/dist/esm/core/external-providers/provider-component-registry.website.d.ts.map +1 -0
  104. package/dist/esm/core/external-providers/provider-component-registry.website.js +68 -0
  105. package/dist/esm/core/external-providers/provider-component-registry.website.js.map +1 -0
  106. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts +36 -0
  107. package/dist/esm/core/external-providers/useWebsiteProviderComponent.d.ts.map +1 -0
  108. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js +59 -0
  109. package/dist/esm/core/external-providers/useWebsiteProviderComponent.js.map +1 -0
  110. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +15 -5
  111. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
  112. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +39 -24
  113. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
  114. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts +11 -0
  115. package/dist/esm/core/external-providers/website-provider-sdk-activations.d.ts.map +1 -0
  116. package/dist/esm/core/external-providers/website-provider-sdk-activations.js +14 -0
  117. package/dist/esm/core/external-providers/website-provider-sdk-activations.js.map +1 -0
  118. package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
  119. package/dist/esm/core/factories/define-website-section.d.ts +3 -3
  120. package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
  121. package/dist/esm/core/factories/define-website-section.js +3 -3
  122. package/dist/esm/core/factories/define-website-section.js.map +1 -1
  123. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -1
  124. package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -1
  125. package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
  126. package/dist/esm/core/layouts/WebsitePageLayout.js +10 -7
  127. package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
  128. package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -1
  129. package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -1
  130. package/dist/esm/index.d.ts +30 -65
  131. package/dist/esm/index.d.ts.map +1 -1
  132. package/dist/esm/index.js +30 -65
  133. package/dist/esm/index.js.map +1 -1
  134. package/dist/esm/mdx/BlogPost.d.ts.map +1 -1
  135. package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -1
  136. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -1
  137. package/dist/esm/mdx/index.d.ts.map +1 -1
  138. package/dist/esm/mdx.d.ts.map +1 -1
  139. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -1
  140. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +35 -6
  141. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
  142. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +39 -5
  143. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -1
  144. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -1
  145. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
  146. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts +32 -0
  147. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.d.ts.map +1 -0
  148. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js +36 -0
  149. package/dist/esm/schemas/label-keys/website-provider-component-label-keys.schemas.js.map +1 -0
  150. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -1
  151. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +9 -8
  152. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
  153. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +8 -7
  154. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -1
  155. package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -1
  156. package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -1
  157. package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
  158. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +14 -7
  159. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
  160. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +14 -7
  161. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -1
  162. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts +57 -0
  163. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.d.ts.map +1 -0
  164. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js +76 -0
  165. package/dist/esm/schemas/structured-data/pricing-offer-structured-data.shared.js.map +1 -0
  166. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +23 -0
  167. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -1
  168. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +20 -2
  169. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -1
  170. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +7 -5
  171. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -1
  172. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -1
  173. package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -1
  174. package/dist/tsconfig.build.tsbuildinfo +1 -1
  175. package/package.json +5 -5
  176. package/src/__tests__/bundle-isolation.test.ts +19 -22
  177. package/src/astro/__tests__/blog-post-bridge.test.tsx +18 -2
  178. package/src/astro/__tests__/bridge-runtime.test.tsx +21 -3
  179. package/src/astro/__tests__/label-pack-loader.test.ts +7 -4
  180. package/src/astro/blog-post-bridge.tsx +19 -5
  181. package/src/astro/bridge-runtime.tsx +35 -11
  182. package/src/astro/label-pack-loader.ts +8 -4
  183. package/src/astro/robots-renderer.ts +15 -15
  184. package/src/astro/website-page-head.renderer.ts +2 -2
  185. package/src/astro/website-site-context.ts +5 -2
  186. package/src/companion-exports.ts +8 -15
  187. package/src/config/__tests__/website-build-origins.test.ts +52 -0
  188. package/src/config/define-website-config.ts +10 -10
  189. package/src/config/load-website-config.ts +25 -14
  190. package/src/config/load-website-pricing-catalog.ts +52 -0
  191. package/src/config/load-website-site-context.ts +145 -0
  192. package/src/config-loader.ts +2 -0
  193. package/src/core/__tests__/contexts.test.tsx +20 -1
  194. package/src/core/__tests__/website-consent.test.tsx +2 -1
  195. package/src/core/anonymous-session/InboundContactForm.tsx +100 -45
  196. package/src/core/anonymous-session/__tests__/InboundContactForm.labels.test.tsx +108 -0
  197. package/src/core/anonymous-session/__tests__/InboundContactForm.submission-challenge.test.tsx +139 -0
  198. package/src/core/anonymous-session/inbound-contact-form.schema.ts +16 -4
  199. package/src/core/anonymous-session/website-anonymous-session-client.ts +39 -4
  200. package/src/core/anonymous-session/website-submission-challenge.ts +99 -0
  201. package/src/core/consent/WebsiteConsentBanner.tsx +11 -10
  202. package/src/core/contexts/WebsiteRuntimeContext.tsx +25 -5
  203. package/src/core/contexts/useWebsiteRuntime.ts +33 -2
  204. package/src/core/external-providers/WebsiteProviderComponent.tsx +153 -0
  205. package/src/core/external-providers/__tests__/frontend-provider-registry.website.test.ts +4 -0
  206. package/src/core/external-providers/__tests__/website-provider-component.test.tsx +238 -0
  207. package/src/core/external-providers/__tests__/website-provider-sdks.test.tsx +72 -0
  208. package/src/core/external-providers/provider-component-registry.website.ts +115 -0
  209. package/src/core/external-providers/useWebsiteProviderComponent.ts +76 -0
  210. package/src/core/external-providers/useWebsiteProviderScripts.ts +56 -37
  211. package/src/core/external-providers/website-provider-sdk-activations.ts +23 -0
  212. package/src/core/factories/define-website-section.ts +3 -3
  213. package/src/core/layouts/WebsitePageLayout.tsx +11 -6
  214. package/src/index.ts +31 -65
  215. package/src/schemas/__tests__/design-token-css-property.test.ts +72 -0
  216. package/src/schemas/__tests__/pricing-offer-structured-data.test.ts +122 -0
  217. package/src/schemas/__tests__/structured-data-reconciliation.test.ts +35 -0
  218. package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +45 -5
  219. package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +12 -0
  220. package/src/schemas/label-keys/website-provider-component-label-keys.schemas.ts +40 -0
  221. package/src/schemas/manifests/website-root-config.shared.schemas.ts +10 -9
  222. package/src/schemas/sections/website-section-definition.shared.schemas.ts +14 -7
  223. package/src/schemas/structured-data/pricing-offer-structured-data.shared.ts +144 -0
  224. package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +46 -1
  225. package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +7 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wildo-ai/saas-website",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "SaaS marketing/landing website engine — schemas, helpers, layout/primitive placeholders, Astro/MDX adapters, and SEO/AEO/GEO builder library. Consumers author all business sections themselves via the defineWebsiteSection factory.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Wildo AI",
@@ -65,10 +65,10 @@
65
65
  "test:watch": "vitest"
66
66
  },
67
67
  "dependencies": {
68
- "@wildo-ai/external-connectors-public": "1.1.5",
69
- "@wildo-ai/platform-config-lib": "1.1.5",
70
- "@wildo-ai/presets-components-models": "1.1.5",
71
- "@wildo-ai/saas-models": "1.1.5",
68
+ "@wildo-ai/external-connectors-public": "1.1.7",
69
+ "@wildo-ai/platform-config-lib": "1.1.7",
70
+ "@wildo-ai/presets-components-models": "1.1.7",
71
+ "@wildo-ai/saas-models": "1.1.7",
72
72
  "yaml": "^2.3.4"
73
73
  },
74
74
  "devDependencies": {
@@ -279,29 +279,14 @@ const ALLOWED_BARE_SPECIFIERS: ReadonlySet<string> = new Set([
279
279
  * `FORBIDDEN_SPECIFIERS`.
280
280
  */
281
281
  '@wildo-ai/zod-decorators',
282
- /**
283
- * SSR / Astro-middleware tier. These four specifiers support server-side code paths
284
- * the marketing engine genuinely needs:
285
- * - `axios` — outbound HTTP from Astro middleware (proxy to
286
- * the SaaS backend, contact-form forwarding, webhook fan-out).
287
- * - `jsonwebtoken` — verify cross-frontend handoff JWTs in
288
- * middleware before letting a request reach a logged-in surface.
289
- * - `socket.io-client` — real-time push (e.g. live preview / status
290
- * events from the SaaS app to the marketing site).
291
- * - `reflect-metadata` — decorator metadata runtime; required
292
- * by any future build-time consumer of `@wildo-ai/zod-decorators`
293
- * metadata (the import IS side-effecting — it mutates
294
- * `globalThis.Reflect`; that side effect is acceptable in an
295
- * SSR context but would still be objectionable in a React
296
- * island bundle).
297
- * Allowing them here is a deliberate trade-off: this test no
298
- * longer guarantees a hard browser-bundle size budget on its own.
299
- * Application import discipline keeps the React-island side lean.
282
+ /*
283
+ * `axios`, `jsonwebtoken`, `socket.io-client` and `reflect-metadata` were allowed here from
284
+ * 2026-04-19 for an SSR middleware tier (backend proxying, handoff-JWT verification, live push)
285
+ * that was never built. Removed 2026-09-25: no source in this package imports any of them, and an
286
+ * allow-list wider than the code is a door nothing guards — the next import of one would pass
287
+ * this test unexamined. The tier that does exist (contact forms, anonymous sessions, consent
288
+ * recording) uses `fetch`. Re-add a specifier in the change that first imports it.
300
289
  */
301
- 'axios',
302
- 'jsonwebtoken',
303
- 'socket.io-client',
304
- 'reflect-metadata',
305
290
  /**
306
291
  * SSR / Astro-build-time tier. The
307
292
  * `astro.ts` sub-entry exposes the label-pack loader, which
@@ -498,6 +483,18 @@ describe('saas-website — bundle-isolation boundary (D12)', () => {
498
483
  }
499
484
  });
500
485
 
486
+ it('walks the provider-component loader (#1703), so everything it imports is held to this boundary', () => {
487
+ // The loader reaches vendor code only through a provider module's DYNAMIC `import()`; these files
488
+ // must stay inside the walk so a static vendor import added to them is caught here.
489
+ for (const file of [
490
+ 'core/external-providers/provider-component-registry.website.ts',
491
+ 'core/external-providers/useWebsiteProviderComponent.ts',
492
+ 'core/external-providers/WebsiteProviderComponent.tsx',
493
+ ]) {
494
+ expect(visited.has(resolve(SRC_ROOT, file)), `Closure did not visit ${file}`).toBe(true);
495
+ }
496
+ });
497
+
501
498
  it.each([...FORBIDDEN_SPECIFIERS])(
502
499
  'no file in the saas-website closure imports the forbidden specifier "%s"',
503
500
  (forbidden) => {
@@ -5,7 +5,14 @@ import { useMDXComponents } from '@mdx-js/react';
5
5
  import React, { type ComponentType } from 'react';
6
6
  import { describe, expect, it } from 'vitest';
7
7
 
8
- import { BUILTIN_PROVIDER_CAPABILITY, defineProviderMetadata, ProviderOriginKind, ProviderTier } from '@wildo-ai/saas-models/public-runtime';
8
+ import {
9
+ BUILTIN_PROVIDER_CAPABILITY,
10
+ defineFrontendPublicConfigSchema,
11
+ defineProviderMetadata,
12
+ FrontendPublicConfigValueKind,
13
+ ProviderOriginKind,
14
+ ProviderTier,
15
+ } from '@wildo-ai/saas-models/public-runtime';
9
16
  import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
10
17
  import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
11
18
 
@@ -57,6 +64,15 @@ const SKIP_LABEL_PACK = {
57
64
  // Typed as the real block rather than `as const`: the prop takes `FrontendProvidersBlock`,
58
65
  // whose `providerCapabilities` / `protocols` are mutable `string[]`, so a `readonly` tuple
59
66
  // fixture is not assignable to the surface under test.
67
+ /**
68
+ * The key list every real frontend provider module declares (#1705): hydration withholds the code of a module
69
+ * without one, so a fixture module without it would never reach the closure this test observes.
70
+ */
71
+ const PUBLIC_CONFIG_SCHEMA = defineFrontendPublicConfigSchema({
72
+ integrationKind: { kind: FrontendPublicConfigValueKind.STRING, description: 'Which integration.' },
73
+ websiteRuntime: { kind: FrontendPublicConfigValueKind.BOOLEAN, description: 'Serves the website runtime.' },
74
+ });
75
+
60
76
  const FRONTEND_PROVIDERS: FrontendProvidersBlock = {
61
77
  providers: {
62
78
  sentry: {
@@ -104,7 +120,7 @@ describe('createWebsiteBlogPostBridgeIsland', () => {
104
120
  return <span>provider closure</span>;
105
121
  }
106
122
  const entry = { ...FRONTEND_PROVIDERS.providers.sentry, sdk: { packageName: '@test/sdk' } };
107
- const modules = { sentry: { ...entry, sdk: { descriptor: entry.sdk, activate } } } as GeneratedWebsiteProviderModules;
123
+ const modules = { sentry: { ...entry, publicConfigSchema: PUBLIC_CONFIG_SCHEMA, sdk: { descriptor: entry.sdk, activate } } } as GeneratedWebsiteProviderModules;
108
124
  const Island = createWebsiteBlogPostBridgeIsland({
109
125
  slugToMdxBody: { demo: ProviderProbe },
110
126
  navigation: buildNavigation(),
@@ -4,7 +4,14 @@ import { render, screen } from '@testing-library/react';
4
4
  import React from 'react';
5
5
  import { describe, expect, it } from 'vitest';
6
6
 
7
- import { BUILTIN_PROVIDER_CAPABILITY, defineProviderMetadata, ProviderOriginKind, ProviderTier } from '@wildo-ai/saas-models/public-runtime';
7
+ import {
8
+ BUILTIN_PROVIDER_CAPABILITY,
9
+ defineFrontendPublicConfigSchema,
10
+ defineProviderMetadata,
11
+ FrontendPublicConfigValueKind,
12
+ ProviderOriginKind,
13
+ ProviderTier,
14
+ } from '@wildo-ai/saas-models/public-runtime';
8
15
  import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
9
16
  import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
10
17
 
@@ -73,6 +80,15 @@ const SKIP_LABEL_PACK = { 'website.chrome.layout.skip-to-main-content': 'Skip to
73
80
  // Typed as the real block rather than `as const`: the prop takes `FrontendProvidersBlock`,
74
81
  // whose `providerCapabilities` / `protocols` are mutable `string[]`, so a `readonly` tuple
75
82
  // fixture is not assignable to the surface under test.
83
+ /**
84
+ * The key list every real frontend provider module declares (#1705): hydration withholds the code of a module
85
+ * without one, so a fixture module without it would never reach the closure this test observes.
86
+ */
87
+ const PUBLIC_CONFIG_SCHEMA = defineFrontendPublicConfigSchema({
88
+ integrationKind: { kind: FrontendPublicConfigValueKind.STRING, description: 'Which integration.' },
89
+ websiteRuntime: { kind: FrontendPublicConfigValueKind.BOOLEAN, description: 'Serves the website runtime.' },
90
+ });
91
+
76
92
  const FRONTEND_PROVIDERS: FrontendProvidersBlock = {
77
93
  providers: {
78
94
  posthog: {
@@ -94,7 +110,9 @@ function RuntimeProbe(): React.ReactNode {
94
110
 
95
111
  describe('createWebsitePageBridgeIsland', () => {
96
112
  it('keeps executable provider code in the bridge closure while props survive JSON serialization', () => {
97
- const activate = async () => ({ handle: 'test' });
113
+ // PostHog rests on analytics consent, so like the real module it declares `stoppable` and returns the
114
+ // `deactivate` that promise requires (#1704); without it hydration withholds the code this test observes.
115
+ const activate = async () => ({ handle: 'test', deactivate: () => undefined });
98
116
  let observed: unknown;
99
117
  function ProviderProbe() {
100
118
  const registry = useWebsiteRuntime().frontendProviderRegistry;
@@ -102,7 +120,7 @@ describe('createWebsitePageBridgeIsland', () => {
102
120
  return <span>provider closure</span>;
103
121
  }
104
122
  const entry = { ...FRONTEND_PROVIDERS.providers.posthog, sdk: { packageName: '@test/sdk' } };
105
- const modules = { posthog: { ...entry, sdk: { descriptor: entry.sdk, activate } } } as GeneratedWebsiteProviderModules;
123
+ const modules = { posthog: { ...entry, publicConfigSchema: PUBLIC_CONFIG_SCHEMA, sdk: { descriptor: entry.sdk, activate, stoppable: true } } } as GeneratedWebsiteProviderModules;
106
124
  const Island = createWebsitePageBridgeIsland({
107
125
  PageComponent: ProviderProbe, pageRef: PAGE_REF,
108
126
  navigation: buildNavigation(),
@@ -1,4 +1,5 @@
1
1
  import { WEBSITE_CONSENT_LABEL_KEYS } from '../../schemas/label-keys/website-consent-label-keys.schemas';
2
+ import { WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS } from '../../schemas/label-keys/website-provider-component-label-keys.schemas';
2
3
  import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
4
  import { tmpdir } from 'node:os';
4
5
  import { resolve } from 'node:path';
@@ -62,7 +63,9 @@ const buildManifest = () =>
62
63
 
63
64
  const SKIP_KEY_STR = 'website.chrome.layout.skip-to-main-content';
64
65
  // Framework-owned consent-panel keys (#534): required in every pack, like the skip link.
65
- const CONSENT_LABELS: Record<string, string> = Object.fromEntries(WEBSITE_CONSENT_LABEL_KEYS.map((key) => [key as unknown as string, 'X']));
66
+ // The provider-component slot's refusal wording (#1703) is framework-owned the same way.
67
+ const FRAMEWORK_OWNED_KEYS = [...WEBSITE_CONSENT_LABEL_KEYS, ...WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS];
68
+ const CONSENT_LABELS: Record<string, string> = Object.fromEntries(FRAMEWORK_OWNED_KEYS.map((key) => [key as unknown as string, 'X']));
66
69
 
67
70
  const buildRootConfig = (): WebsiteRootConfig =>
68
71
  WebsiteRootConfigSchema.parse({
@@ -210,7 +213,7 @@ describe('label-pack-loader', () => {
210
213
  writeFileSync(resolve(tmpDir, 'fr.json'), JSON.stringify({}));
211
214
  expect(() =>
212
215
  assertLabelPacksValid(tmpDir, buildSiteContext(), buildExpectedKeys()),
213
- ).toThrow(new RegExp(`Locale "fr".*missing ${4 + WEBSITE_CONSENT_LABEL_KEYS.length} required key\\(s\\)`, 's'));
216
+ ).toThrow(new RegExp(`Locale "fr".*missing ${4 + FRAMEWORK_OWNED_KEYS.length} required key\\(s\\)`, 's'));
214
217
  });
215
218
 
216
219
  it('annotates each missing key with its declaring page / section origin', () => {
@@ -294,7 +297,7 @@ describe('label-pack-loader', () => {
294
297
  warnSpy.mockRestore();
295
298
  });
296
299
 
297
- it('requires every framework-owned consent-panel key in every locale pack (#534)', () => {
300
+ it('requires every framework-owned consent-panel (#534) and provider-component (#1703) key in every locale pack', () => {
298
301
  const withoutConsent = JSON.stringify({
299
302
  'website.landing.hero.headline': 'X',
300
303
  'website.landing.meta.title': 'X',
@@ -311,7 +314,7 @@ describe('label-pack-loader', () => {
311
314
  }
312
315
  expect(captured).toMatch(/Locale "en":/);
313
316
  expect(captured).not.toMatch(/Locale "fr":/);
314
- for (const key of WEBSITE_CONSENT_LABEL_KEYS) expect(captured).toContain(key as unknown as string);
317
+ for (const key of FRAMEWORK_OWNED_KEYS) expect(captured).toContain(key as unknown as string);
315
318
  });
316
319
 
317
320
  it('treats WebsiteRootConfig.skipToMainContentLabelKey as required (Phase 6 third audit fix M-α)', () => {
@@ -7,7 +7,8 @@ import {
7
7
  WebsiteRuntimeContextProvider,
8
8
  type WebsiteRuntimeContextValue,
9
9
  } from '../core/contexts/WebsiteRuntimeContext';
10
- import { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';
10
+ import { useWebsiteProviderRegistries } from '../core/external-providers/provider-component-registry.website';
11
+ import type { ProviderComponentLoaderContribution } from '@wildo-ai/external-connectors-public/website';
11
12
  import type { GeneratedWebsiteProviderModules } from '../core/external-providers/frontend-provider-registry.website';
12
13
  import { BlogPost, BLOG_POST_PAGE_REF } from '../mdx/BlogPost';
13
14
  import type { WebsiteMdxComponentsMap } from '../mdx/website-mdx-provider';
@@ -228,6 +229,14 @@ export interface CreateWebsiteBlogPostBridgeIslandDeps {
228
229
  * Config sync emits a stable map, including an empty map when no providers are declared.
229
230
  */
230
231
  frontendProviderModules?: GeneratedWebsiteProviderModules;
232
+ /**
233
+ * Loaders for provider components an ENGINE provider module declares but cannot import (#1703) —
234
+ * the website's facet-library door, the equivalent of the application's
235
+ * `contributeProviderComponentLoaders`. A provider PACKAGE that ships its own component needs
236
+ * nothing here: its module's `components[capability].load` is used. A DEPS entry because a loader
237
+ * is a function, and island props are serialized.
238
+ */
239
+ providerComponentLoaders?: ReadonlyArray<ProviderComponentLoaderContribution>;
231
240
  /**
232
241
  * Per-slug map of compiled MDX bodies (each entry is a React
233
242
  * component — the `.default` export of the MDX module). Typically
@@ -383,6 +392,13 @@ export function createWebsiteBlogPostBridgeIsland(
383
392
  );
384
393
  }
385
394
 
395
+ // After the missing-slug throw on purpose: that throw ends the render, so it never changes the
396
+ // hook order of a render that completes, and the build-time error stays reachable.
397
+ const { frontendProviderRegistry, providerComponentRegistry } = useWebsiteProviderRegistries({
398
+ frontendProviders,
399
+ frontendProviderModules: deps.frontendProviderModules,
400
+ providerComponentLoaders: deps.providerComponentLoaders,
401
+ });
386
402
  const runtimeValue: WebsiteRuntimeContextValue = {
387
403
  locale,
388
404
  defaultLocale,
@@ -392,10 +408,8 @@ export function createWebsiteBlogPostBridgeIsland(
392
408
  skipToMainContentLabelKey,
393
409
  appOrigin,
394
410
  ...(deps.consentBannerComponent !== undefined ? { consentBannerComponent: deps.consentBannerComponent } : {}),
395
- frontendProviderRegistry: createFrontendWebsiteProviderRegistry({
396
- entries: frontendProviders,
397
- modules: deps.frontendProviderModules,
398
- }),
411
+ frontendProviderRegistry,
412
+ providerComponentRegistry,
399
413
  };
400
414
  return (
401
415
  <WebsiteRuntimeContextProvider value={runtimeValue}>
@@ -1,14 +1,15 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { ComponentType, FunctionComponent, ReactNode } from 'react';
3
3
 
4
- import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';
4
+ import type { FrontendProvidersBlock, PublicPricingCatalog } from '@wildo-ai/saas-models/public-runtime';
5
5
  import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
6
6
 
7
7
  import {
8
8
  WebsiteRuntimeContextProvider,
9
9
  type WebsiteRuntimeContextValue,
10
10
  } from '../core/contexts/WebsiteRuntimeContext';
11
- import { createFrontendWebsiteProviderRegistry } from '../core/external-providers/frontend-provider-registry.website';
11
+ import { useWebsiteProviderRegistries } from '../core/external-providers/provider-component-registry.website';
12
+ import type { ProviderComponentLoaderContribution } from '@wildo-ai/external-connectors-public/website';
12
13
  import type { GeneratedWebsiteProviderModules } from '../core/external-providers/frontend-provider-registry.website';
13
14
  import { WebsitePageLayout } from '../core/layouts/WebsitePageLayout';
14
15
  import type { WebsiteConsentRecorder } from '../core/consent/WebsiteConsentContext';
@@ -72,22 +73,22 @@ import type { LabelPack } from '../schemas/validators/label-pack.validator';
72
73
  * } from '@wildo-ai/saas-website/astro';
73
74
  * import { LandingBridgeIsland } from '../pages-impl/landing.bridge';
74
75
  * import { LANDING_PAGE_MANIFEST } from '../pages-impl/landing.page';
75
- * import { WEBSITE_ROOT_CONFIG } from '../website-root.config';
76
+ * import { WEBSITE_SITE_CONTEXT } from '../website-site-context';
76
77
  *
77
78
  * const runtime = loadWebsitePageRuntime({
78
79
  * manifest: LANDING_PAGE_MANIFEST,
79
- * rootConfig: WEBSITE_ROOT_CONFIG,
80
+ * siteContext: WEBSITE_SITE_CONTEXT,
80
81
  * labelPacksDir: './src/i18n',
81
82
  * astroLocale: Astro.currentLocale,
82
83
  * });
83
84
  * const headHtml = renderWebsitePageHeadHtml({
84
85
  * manifest: LANDING_PAGE_MANIFEST,
85
- * rootConfig: WEBSITE_ROOT_CONFIG,
86
+ * siteContext: WEBSITE_SITE_CONTEXT,
86
87
  * locale: runtime.locale,
87
88
  * labelPack: runtime.labelPack,
88
89
  * });
89
90
  * const designTokensCss = renderWebsiteDesignTokensCss(
90
- * WEBSITE_ROOT_CONFIG.designTokens,
91
+ * WEBSITE_SITE_CONTEXT.rootConfig.designTokens,
91
92
  * );
92
93
  * ---
93
94
  * <!doctype html>
@@ -100,8 +101,11 @@ import type { LabelPack } from '../schemas/validators/label-pack.validator';
100
101
  * <LandingBridgeIsland
101
102
  * client:idle
102
103
  * locale={runtime.locale}
104
+ * defaultLocale={runtime.defaultLocale}
105
+ * appOrigin={WEBSITE_SITE_CONTEXT.websiteConfig.appOrigin}
106
+ * apiOrigin={WEBSITE_SITE_CONTEXT.websiteConfig.apiOrigin}
103
107
  * labelPack={runtime.labelPack}
104
- * designTokens={WEBSITE_ROOT_CONFIG.designTokens}
108
+ * designTokens={WEBSITE_SITE_CONTEXT.rootConfig.designTokens}
105
109
  * />
106
110
  * </body>
107
111
  * </html>
@@ -152,6 +156,14 @@ export interface CreateWebsitePageBridgeIslandDeps {
152
156
  * Config sync emits a stable map, including an empty map when no providers are declared.
153
157
  */
154
158
  frontendProviderModules?: GeneratedWebsiteProviderModules;
159
+ /**
160
+ * Loaders for provider components an ENGINE provider module declares but cannot import (#1703) —
161
+ * the website's facet-library door, the equivalent of the application's
162
+ * `contributeProviderComponentLoaders`. A provider PACKAGE that ships its own component needs
163
+ * nothing here: its module's `components[capability].load` is used. A DEPS entry because a loader
164
+ * is a function, and island props are serialized.
165
+ */
166
+ providerComponentLoaders?: ReadonlyArray<ProviderComponentLoaderContribution>;
155
167
  /**
156
168
  * The page's React component. Typically the same value that was
157
169
  * passed to `defineWebsitePageManifest({ pageComponent })` — the
@@ -270,6 +282,13 @@ export interface WebsitePageBridgeIslandProps {
270
282
  * the consumer's job and the whole point of removing the fallback.
271
283
  */
272
284
  appOrigin?: string;
285
+ /**
286
+ * The application's public pricing catalogue (#1835), forwarded into
287
+ * `WebsiteRuntimeContext.pricingCatalog` for `useWebsitePricingCatalog()`. Plain JSON-safe data
288
+ * (amounts travel as decimal strings, never `bigint`), loaded in the `.astro` frontmatter with
289
+ * `loadWebsitePricingCatalog()`. Passed only by pages with a pricing section.
290
+ */
291
+ pricingCatalog?: PublicPricingCatalog;
273
292
  }
274
293
 
275
294
  /**
@@ -293,6 +312,7 @@ export function createWebsitePageBridgeIsland(
293
312
  frontendProviders,
294
313
  apiOrigin,
295
314
  appOrigin,
315
+ pricingCatalog,
296
316
  }: WebsitePageBridgeIslandProps): ReactNode {
297
317
  /*
298
318
  * Both must be present: the factory is build-time wiring and `apiOrigin` is per-environment, and
@@ -305,6 +325,11 @@ export function createWebsitePageBridgeIsland(
305
325
  : undefined),
306
326
  [apiOrigin],
307
327
  );
328
+ const { frontendProviderRegistry, providerComponentRegistry } = useWebsiteProviderRegistries({
329
+ frontendProviders,
330
+ frontendProviderModules: deps.frontendProviderModules,
331
+ providerComponentLoaders: deps.providerComponentLoaders,
332
+ });
308
333
  const runtimeValue: WebsiteRuntimeContextValue = {
309
334
  locale,
310
335
  defaultLocale,
@@ -312,13 +337,12 @@ export function createWebsitePageBridgeIsland(
312
337
  designTokens,
313
338
  navigation,
314
339
  skipToMainContentLabelKey,
315
- frontendProviderRegistry: createFrontendWebsiteProviderRegistry({
316
- entries: frontendProviders,
317
- modules: deps.frontendProviderModules,
318
- }),
340
+ frontendProviderRegistry,
341
+ providerComponentRegistry,
319
342
  ...(deps.consentBannerComponent !== undefined ? { consentBannerComponent: deps.consentBannerComponent } : {}),
320
343
  apiOrigin,
321
344
  appOrigin,
345
+ ...(pricingCatalog !== undefined ? { pricingCatalog } : {}),
322
346
  };
323
347
  return (
324
348
  <WebsiteRuntimeContextProvider value={runtimeValue}>
@@ -5,6 +5,7 @@ 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
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';
8
9
  import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
9
10
  import type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';
10
11
  import {
@@ -21,10 +22,10 @@ import type { WebsiteSiteContext } from './website-site-context';
21
22
  *
22
23
  * `label-pack-loader.ts` — synchronous, fs-backed loader for
23
24
  * marketing-site label packs. Consumed by:
24
- * - the Astro adapter (`WebsitePageBridge.astro` reads the active
25
- * locale's pack at request time during SSR / pre-render),
26
- * - the `wildo website audit` CLI (Phase 7) to validate every
27
- * 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`,
28
29
  * - any build-time hook that wants to fail fast on a missing or
29
30
  * malformed pack.
30
31
  *
@@ -211,6 +212,9 @@ function composeEffectiveChromeLabelKeys(
211
212
  // The visitor consent panel is framework-owned in the same way (#534): required in every pack, so a site
212
213
  // that turns on analytics can never ship it with an unlabelled way to refuse.
213
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);
214
218
  return Array.from(set);
215
219
  }
216
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