@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
@@ -1,15 +1,17 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
+ import { WEBSITE_CONSENT_LABEL_KEYS } from '../schemas/label-keys/website-consent-label-keys.schemas.js';
4
+ import { WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS } from '../schemas/label-keys/website-provider-component-label-keys.schemas.js';
3
5
  import { validateLabelPackAgainstManifest, } from '../schemas/validators/label-pack.validator.js';
4
6
  /**
5
7
  * @wildo_source:part:start saas.website.astro.label-loader facet:layer:astro facet:family:website
6
8
  *
7
9
  * `label-pack-loader.ts` — synchronous, fs-backed loader for
8
10
  * marketing-site label packs. Consumed by:
9
- * - the Astro adapter (`WebsitePageBridge.astro` reads the active
10
- * locale's pack at request time during SSR / pre-render),
11
- * - the `wildo website audit` CLI (Phase 7) to validate every
12
- * locale pack against every manifest at CI time,
11
+ * - each page's `.astro` frontmatter, through `loadWebsitePageRuntime`,
12
+ * which reads the active locale's pack during SSR / pre-render,
13
+ * - `wildo website audit`, which validates every locale pack against
14
+ * every manifest and section through `loadAndValidateAllLabelPacks`,
13
15
  * - any build-time hook that wants to fail fast on a missing or
14
16
  * malformed pack.
15
17
  *
@@ -158,6 +160,14 @@ export function loadAndValidateAllLabelPacks(packsDir, siteContext, expectedKeys
158
160
  function composeEffectiveChromeLabelKeys(rootConfig) {
159
161
  const set = new Set(rootConfig.chromeLabelKeys);
160
162
  set.add(rootConfig.skipToMainContentLabelKey);
163
+ // The visitor consent panel is framework-owned in the same way (#534): required in every pack, so a site
164
+ // that turns on analytics can never ship it with an unlabelled way to refuse.
165
+ for (const key of WEBSITE_CONSENT_LABEL_KEYS)
166
+ set.add(key);
167
+ // So is the provider-component slot's refusal wording (#1703): a site placing an address lookup must not
168
+ // ship its consent-required or unavailable state as a raw key.
169
+ for (const key of WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS)
170
+ set.add(key);
161
171
  return Array.from(set);
162
172
  }
163
173
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"label-pack-loader.js","sourceRoot":"","sources":["../../../../src/astro/label-pack-loader.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\nimport type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport {\n validateLabelPackAgainstManifest,\n type ExpectedLabelKeysBySectionRef,\n type LabelPack,\n type LabelPackValidationResult,\n} from '../schemas/validators/label-pack.validator';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.label-loader facet:layer:astro facet:family:website\n *\n * `label-pack-loader.ts` — synchronous, fs-backed loader for\n * marketing-site label packs. Consumed by:\n * - the Astro adapter (`WebsitePageBridge.astro` reads the active\n * locale's pack at request time during SSR / pre-render),\n * - the `wildo website audit` CLI (Phase 7) to validate every\n * locale pack against every manifest at CI time,\n * - any build-time hook that wants to fail fast on a missing or\n * malformed pack.\n *\n * **Convention** (matches the slice plan + the validator's contract):\n *\n * - One file per locale: `<packsDir>/<locale>.json`\n * - JSON object: flat key-value map, `Record<string, string>`\n * - Keys are fully-composed `WebsiteLabelKey`s (`website.<page>.<section>.<slot>`)\n * - Values are the localized strings\n *\n * **Why synchronous I/O**:\n * - The Astro frontmatter executes at build / SSR time in a Node\n * process (no event loop pressure to relieve), and the bridge\n * wants the pack resolved before it emits any HTML — async would\n * force the consumer to `await` inside an Astro frontmatter,\n * which is supported but adds a needless yield-point on a single\n * small JSON file.\n * - The validator is pure and synchronous; making the loader async\n * would add a coloring boundary for no benefit.\n *\n * **Failure modes — all surfaced as thrown errors with actionable\n * messages**:\n * - File missing — wrapped in a `[label-pack-loader] Could not read\n * pack at <path>` error so a typo in the dir / locale is obvious.\n * - JSON parse failure — error includes the file path and the\n * parser's own message.\n * - Top-level shape is not an object, or a value is not a string —\n * error names the offending key + observed type.\n *\n * **Why three exports** (loader, multi-loader, validator-runner):\n * - `loadLabelPack(packsDir, locale)` is the atomic primitive\n * (one file → one validated `LabelPack`). Used by the bridge at\n * request time.\n * - `loadAllLabelPacks(packsDir, siteContext)` is the build-time\n * batch — every locale pack at once. Used by the CLI auditor\n * and by the build-start hook.\n * - `validateAllLabelPacks(packsDir, siteContext, expectedKeysBySectionRef)`\n * is the highest-level entry point: load every pack, run the\n * validator on each, and either return the diagnostic results\n * or throw on the first hard error. The returned shape carries\n * per-locale diagnostics so the caller can format a CLI report.\n *\n * **`siteContext` parameter** — the consumer's composed\n * `WebsiteSiteContext` (see `website-site-context.ts`). The loader\n * reads `siteContext.websiteConfig.locales` for \"which locales does\n * the site ship?\" and `siteContext.rootConfig.{pageManifests,\n * chromeLabelKeys, skipToMainContentLabelKey}` for the manifest\n * registry / chrome-key set the validator runs against.\n */\n\n/**\n * Load and validate a single locale's label pack. Synchronous —\n * suitable for Astro frontmatter execution.\n *\n * @param packsDir Absolute or cwd-relative path to the directory\n * containing per-locale `<locale>.json` files.\n * @param locale The locale to load. Must be a member of\n * `WildoMarketingWebsiteConfig.locales`.\n * @returns A flat `LabelPack` (`Record<string, string>`) ready to\n * feed into `WebsiteRuntimeContextValue.labelPack`.\n * @throws When the file is missing, malformed JSON, not an object,\n * or contains a non-string value.\n */\nexport function loadLabelPack(packsDir: string, locale: AvailableLanguage): LabelPack {\n const filePath = resolve(packsDir, `${locale}.json`);\n\n let raw: string;\n try {\n raw = readFileSync(filePath, 'utf-8');\n } catch (cause) {\n throw new Error(\n `[label-pack-loader] Could not read label pack at ${filePath} ` +\n `(locale=\"${locale}\"). ` +\n `Make sure the directory exists and contains a \"${locale}.json\" file. ` +\n `Underlying error: ${(cause as Error).message}`,\n { cause },\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (cause) {\n throw new Error(\n `[label-pack-loader] Failed to parse JSON in ${filePath} ` +\n `(locale=\"${locale}\"). Underlying parser error: ${(cause as Error).message}`,\n { cause },\n );\n }\n\n return assertLabelPackShape(parsed, filePath, locale);\n}\n\n/**\n * Batch loader — every locale declared on the site at once. The map\n * iteration order matches the declaration order of\n * `siteContext.websiteConfig.locales` so CLI reports come out in a\n * deterministic, authored sequence.\n */\nexport function loadAllLabelPacks(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n): Map<AvailableLanguage, LabelPack> {\n const packs = new Map<AvailableLanguage, LabelPack>();\n for (const entry of siteContext.websiteConfig.locales) {\n packs.set(entry.locale, loadLabelPack(packsDir, entry.locale));\n }\n return packs;\n}\n\n/**\n * Per-locale validation result — the validator's flat output extended\n * with the locale tag so a multi-locale report can flatten + group.\n */\nexport interface LocaleLabelPackValidation {\n locale: AvailableLanguage;\n result: LabelPackValidationResult;\n}\n\n/**\n * Highest-level entry point: load every locale's pack and run the\n * label-pack validator against the manifest registry.\n *\n * **Return shape decision** — this function does NOT throw on\n * `missing` keys; it returns the per-locale results so the caller\n * (the build-start hook OR the CLI auditor OR a debugging\n * subprocess) can decide how to surface the failure (process exit\n * code, formatted CLI output, JSON report, …). A thin `throwIfAny…`\n * helper sits next to it for the common \"fail the build on first\n * missing key\" path.\n */\nexport function loadAndValidateAllLabelPacks(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n): readonly LocaleLabelPackValidation[] {\n const { rootConfig } = siteContext;\n const packs = loadAllLabelPacks(packsDir, siteContext);\n const manifests: readonly WebsitePageManifest[] = rootConfig.pageManifests;\n const effectiveChromeKeys = composeEffectiveChromeLabelKeys(rootConfig);\n\n const validations: LocaleLabelPackValidation[] = [];\n for (const [locale, pack] of packs) {\n validations.push({\n locale,\n result: validateLabelPackAgainstManifest(\n manifests,\n expectedKeysBySectionRef,\n pack,\n effectiveChromeKeys,\n ),\n });\n }\n return validations;\n}\n\n/**\n * Compose the chrome-label-key set the validator should treat as\n * \"expected\" — `WebsiteRootConfig.chromeLabelKeys` (consumer-authored)\n * UNION the framework-required `skipToMainContentLabelKey`.\n *\n * Centralized here because:\n * - Both `loadAndValidateAllLabelPacks` (used at build time) and\n * `buildKeyOriginMap` (used to annotate missing-key errors) need\n * the same composed list, and forking the union into two places\n * would be a drift hazard.\n * - The framework owns the skip-link key (it is rendered by\n * `WebsitePageLayout`, not the consumer's chrome), so the\n * consumer should NOT have to remember to also list it in\n * `chromeLabelKeys`. Auto-injection here keeps the contract\n * minimal at the consumer's call site (just declare the key in\n * `WebsiteRootConfig.skipToMainContentLabelKey` once + ship the\n * translation in every locale pack) while still surfacing the key\n * through the validator's \"missing\"/\"orphaned\" diagnostics.\n * - De-duplication is via `Set` semantics so a consumer who\n * defensively lists the key in both places (no-op but harmless)\n * does not produce a duplicate-key warning downstream.\n */\nfunction composeEffectiveChromeLabelKeys(\n rootConfig: WebsiteRootConfig,\n): readonly WebsiteLabelKey[] {\n const set = new Set<WebsiteLabelKey>(rootConfig.chromeLabelKeys);\n set.add(rootConfig.skipToMainContentLabelKey);\n return Array.from(set);\n}\n\n/**\n * Convenience wrapper for the build-start hook: validates every\n * declared locale's pack and throws a SINGLE aggregated error if any\n * locale has a hard failure (`missing` keys or `unmappedSectionRefs`).\n * Orphaned keys are logged via `console.warn` (soft warning per the\n * validator's contract) but do NOT throw.\n *\n * **Why aggregate (not first-fail)**: a multi-locale marketing site\n * with `en` and `fr` packs both missing keys would, under a\n * first-fail policy, force the author to fix `en`, re-run the build,\n * THEN see `fr`'s problem. Aggregating means one build run surfaces\n * every locale's hard errors together — material UX win for the\n * authoring loop. The aggregated message keeps each locale's failure\n * in its own block so the diagnostic stays scannable.\n *\n * **Why throw vs. return**: the build-start hook lives in\n * `astro.config.mjs` (or a `vite-plugin`-style integration) and\n * benefits from the shortest possible code path — a thrown error\n * naturally aborts `astro build` with a non-zero exit code and\n * surfaces the message in the build log. The richer\n * `loadAndValidateAllLabelPacks` is what `wildo website audit` calls\n * when it wants programmatic access to the per-locale results.\n *\n * **Key-origin annotation**: every `missing` key is tagged with the\n * page (and, if section-scoped, the section) that declared it, so the\n * author can grep + open the right `defineWebsiteSection` /\n * `defineWebsitePageManifest` call site without leaving the build log.\n */\nexport function assertLabelPacksValid(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n): void {\n const { rootConfig } = siteContext;\n const validations = loadAndValidateAllLabelPacks(\n packsDir,\n siteContext,\n expectedKeysBySectionRef,\n );\n\n /**\n * Build a key → origin map ONCE per assert call (cost is bounded by\n * the total number of declared keys across the whole site, which is\n * typically small for a marketing surface). Enriches the missing-key\n * error with the page/section that committed to each key — the\n * author can grep the ref to find the site authoring call site.\n */\n const keyOrigins = buildKeyOriginMap(\n rootConfig.pageManifests,\n expectedKeysBySectionRef,\n composeEffectiveChromeLabelKeys(rootConfig),\n );\n\n const localeErrorBlocks: string[] = [];\n\n for (const { locale, result } of validations) {\n const blocks: string[] = [];\n if (result.unmappedSectionRefs.length > 0) {\n blocks.push(\n ` unmappedSectionRefs (build-pipeline bug — sections referenced by a ` +\n `manifest but missing from the expected-keys collector): ` +\n `[${(result.unmappedSectionRefs as unknown as readonly string[]).join(', ')}]. ` +\n `Add every referenced section to the array passed to ` +\n `collectExpectedLabelKeysFromSections().`,\n );\n }\n if (result.missing.length > 0) {\n const annotated = (result.missing as unknown as readonly string[]).map(\n (key) => ` \"${key}\" ← ${keyOrigins.get(key) ?? 'unknown origin'}`,\n );\n blocks.push(\n ` missing ${result.missing.length} required key(s) in ${packsDir}/${locale}.json:\\n` +\n annotated.join('\\n'),\n );\n }\n if (blocks.length > 0) {\n localeErrorBlocks.push(`Locale \"${locale}\":\\n${blocks.join('\\n')}`);\n }\n if (result.orphaned.length > 0) {\n console.warn(\n `[label-pack-loader] Locale \"${locale}\": ${result.orphaned.length} ` +\n `orphaned key(s) in ${locale}.json (declared in pack but not consumed ` +\n `by any section or page manifest):\\n ${result.orphaned.join('\\n ')}`,\n );\n }\n }\n\n if (localeErrorBlocks.length > 0) {\n throw new Error(\n `[label-pack-loader] Label pack validation failed for ` +\n `${localeErrorBlocks.length} locale(s):\\n\\n${localeErrorBlocks.join('\\n\\n')}`,\n );\n }\n}\n\n/**\n * Build a `keyString -> \"page <pageRef> meta titleKey\" | \"section <sectionRef> in page <pageRef>\"`\n * lookup so the assert message can name the call site that committed\n * to each missing key. Pure and small (cost is O(totalKeys) per build\n * — bounded by the marketing surface).\n */\nfunction buildKeyOriginMap(\n manifests: readonly WebsitePageManifest[],\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n chromeLabelKeys: readonly WebsiteLabelKey[],\n): Map<string, string> {\n const origins = new Map<string, string>();\n for (const manifest of manifests) {\n const pageRefStr = manifest.ref as unknown as string;\n const titleKey = manifest.metaLabelKeys.titleKey as unknown as string;\n const descriptionKey = manifest.metaLabelKeys.descriptionKey as unknown as string;\n if (!origins.has(titleKey)) {\n origins.set(titleKey, `page \"${pageRefStr}\" metaLabelKeys.titleKey`);\n }\n if (!origins.has(descriptionKey)) {\n origins.set(descriptionKey, `page \"${pageRefStr}\" metaLabelKeys.descriptionKey`);\n }\n for (const sectionRef of manifest.sectionRefs) {\n const sectionRefStr = sectionRef as unknown as string;\n const sectionKeys = expectedKeysBySectionRef.get(sectionRef);\n if (sectionKeys === undefined) continue;\n for (const sectionKey of sectionKeys) {\n const keyStr = sectionKey as unknown as string;\n if (!origins.has(keyStr)) {\n origins.set(keyStr, `section \"${sectionRefStr}\" in page \"${pageRefStr}\"`);\n }\n }\n }\n }\n for (const chromeKey of chromeLabelKeys) {\n const keyStr = chromeKey as unknown as string;\n if (!origins.has(keyStr)) {\n origins.set(keyStr, `WebsiteRootConfig.chromeLabelKeys`);\n }\n }\n return origins;\n}\n\n/**\n * Defensive shape guard. Surface the offending key + observed type\n * so the consumer can find the malformed entry quickly. Pure\n * validation — no transformation; the returned value is the same\n * object reference passed in (cast to `LabelPack`) so the caller\n * does not pay an unnecessary copy.\n */\nfunction assertLabelPackShape(\n parsed: unknown,\n filePath: string,\n locale: AvailableLanguage,\n): LabelPack {\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(\n `[label-pack-loader] ${filePath} (locale=\"${locale}\") must contain a ` +\n `JSON object at the top level (got ${describeJson(parsed)}).`,\n );\n }\n for (const [key, value] of Object.entries(parsed)) {\n if (typeof value !== 'string') {\n throw new Error(\n `[label-pack-loader] ${filePath} (locale=\"${locale}\"): key \"${key}\" ` +\n `must map to a string (got ${describeJson(value)}).`,\n );\n }\n }\n return parsed as LabelPack;\n}\n\nfunction describeJson(value: unknown): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n return typeof value;\n}\n/** @wildo_source:part:end saas.website.astro.label-loader */\n"]}
1
+ {"version":3,"file":"label-pack-loader.js","sourceRoot":"","sources":["../../../../src/astro/label-pack-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,OAAO,EAAE,0BAA0B,EAAE,MAAM,0DAA0D,CAAC;AACtG,OAAO,EAAE,qCAAqC,EAAE,MAAM,qEAAqE,CAAC;AAG5H,OAAO,EACL,gCAAgC,GAIjC,MAAM,4CAA4C,CAAC;AAIpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB,EAAE,MAAyB;IACvE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,GAAG,MAAM,OAAO,CAAC,CAAC;IAErD,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,oDAAoD,QAAQ,GAAG;YAC7D,YAAY,MAAM,MAAM;YACxB,kDAAkD,MAAM,eAAe;YACvE,qBAAsB,KAAe,CAAC,OAAO,EAAE,EACjD,EAAE,KAAK,EAAE,CACV,CAAC;IACJ,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,+CAA+C,QAAQ,GAAG;YACxD,YAAY,MAAM,gCAAiC,KAAe,CAAC,OAAO,EAAE,EAC9E,EAAE,KAAK,EAAE,CACV,CAAC;IACJ,CAAC;IAED,OAAO,oBAAoB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;AACxD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAgB,EAChB,WAA+B;IAE/B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgC,CAAC;IACtD,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;QACtD,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAWD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,4BAA4B,CAC1C,QAAgB,EAChB,WAA+B,EAC/B,wBAAuD;IAEvD,MAAM,EAAE,UAAU,EAAE,GAAG,WAAW,CAAC;IACnC,MAAM,KAAK,GAAG,iBAAiB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACvD,MAAM,SAAS,GAAmC,UAAU,CAAC,aAAa,CAAC;IAC3E,MAAM,mBAAmB,GAAG,+BAA+B,CAAC,UAAU,CAAC,CAAC;IAExE,MAAM,WAAW,GAAgC,EAAE,CAAC;IACpD,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC;QACnC,WAAW,CAAC,IAAI,CAAC;YACf,MAAM;YACN,MAAM,EAAE,gCAAgC,CACtC,SAAS,EACT,wBAAwB,EACxB,IAAI,EACJ,mBAAmB,CACpB;SACF,CAAC,CAAC;IACL,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,SAAS,+BAA+B,CACtC,UAA6B;IAE7B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAkB,UAAU,CAAC,eAAe,CAAC,CAAC;IACjE,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,yBAAyB,CAAC,CAAC;IAC9C,yGAAyG;IACzG,8EAA8E;IAC9E,KAAK,MAAM,GAAG,IAAI,0BAA0B;QAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3D,yGAAyG;IACzG,+DAA+D;IAC/D,KAAK,MAAM,GAAG,IAAI,qCAAqC;QAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACtE,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,qBAAqB,CACnC,QAAgB,EAChB,WAA+B,EAC/B,wBAAuD;IAEvD,MAAM,EAAE,UAAU,EAAE,GAAG,WAAW,CAAC;IACnC,MAAM,WAAW,GAAG,4BAA4B,CAC9C,QAAQ,EACR,WAAW,EACX,wBAAwB,CACzB,CAAC;IAEF;;;;;;OAMG;IACH,MAAM,UAAU,GAAG,iBAAiB,CAClC,UAAU,CAAC,aAAa,EACxB,wBAAwB,EACxB,+BAA+B,CAAC,UAAU,CAAC,CAC5C,CAAC;IAEF,MAAM,iBAAiB,GAAa,EAAE,CAAC;IAEvC,KAAK,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,MAAM,CAAC,mBAAmB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1C,MAAM,CAAC,IAAI,CACT,uEAAuE;gBACrE,0DAA0D;gBAC1D,IAAK,MAAM,CAAC,mBAAoD,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;gBAChF,sDAAsD;gBACtD,yCAAyC,CAC5C,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,SAAS,GAAI,MAAM,CAAC,OAAwC,CAAC,GAAG,CACpE,CAAC,GAAG,EAAE,EAAE,CAAC,QAAQ,GAAG,QAAQ,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,gBAAgB,EAAE,CACtE,CAAC;YACF,MAAM,CAAC,IAAI,CACT,aAAa,MAAM,CAAC,OAAO,CAAC,MAAM,uBAAuB,QAAQ,IAAI,MAAM,UAAU;gBACnF,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CACvB,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,iBAAiB,CAAC,IAAI,CAAC,WAAW,MAAM,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,CACV,+BAA+B,MAAM,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG;gBAClE,sBAAsB,MAAM,2CAA2C;gBACvE,wCAAwC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CACzE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,iBAAiB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CACb,uDAAuD;YACrD,GAAG,iBAAiB,CAAC,MAAM,kBAAkB,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAChF,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CACxB,SAAyC,EACzC,wBAAuD,EACvD,eAA2C;IAE3C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAwB,CAAC;QACrD,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,QAA6B,CAAC;QACtE,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa,CAAC,cAAmC,CAAC;QAClF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,UAAU,0BAA0B,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,SAAS,UAAU,gCAAgC,CAAC,CAAC;QACnF,CAAC;QACD,KAAK,MAAM,UAAU,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;YAC9C,MAAM,aAAa,GAAG,UAA+B,CAAC;YACtD,MAAM,WAAW,GAAG,wBAAwB,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAC7D,IAAI,WAAW,KAAK,SAAS;gBAAE,SAAS;YACxC,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;gBACrC,MAAM,MAAM,GAAG,UAA+B,CAAC;gBAC/C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;oBACzB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,aAAa,cAAc,UAAU,GAAG,CAAC,CAAC;gBAC5E,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,KAAK,MAAM,SAAS,IAAI,eAAe,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,SAA8B,CAAC;QAC9C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACzB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,mCAAmC,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,oBAAoB,CAC3B,MAAe,EACf,QAAgB,EAChB,MAAyB;IAEzB,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CACb,uBAAuB,QAAQ,aAAa,MAAM,oBAAoB;YACpE,qCAAqC,YAAY,CAAC,MAAM,CAAC,IAAI,CAChE,CAAC;IACJ,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,uBAAuB,QAAQ,aAAa,MAAM,YAAY,GAAG,IAAI;gBACnE,6BAA6B,YAAY,CAAC,KAAK,CAAC,IAAI,CACvD,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,MAAmB,CAAC;AAC7B,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACzC,OAAO,OAAO,KAAK,CAAC;AACtB,CAAC;AACD,6DAA6D","sourcesContent":["import { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';\nimport { WEBSITE_CONSENT_LABEL_KEYS } from '../schemas/label-keys/website-consent-label-keys.schemas';\nimport { WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS } from '../schemas/label-keys/website-provider-component-label-keys.schemas';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\nimport type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport {\n validateLabelPackAgainstManifest,\n type ExpectedLabelKeysBySectionRef,\n type LabelPack,\n type LabelPackValidationResult,\n} from '../schemas/validators/label-pack.validator';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.label-loader facet:layer:astro facet:family:website\n *\n * `label-pack-loader.ts` — synchronous, fs-backed loader for\n * marketing-site label packs. Consumed by:\n * - each page's `.astro` frontmatter, through `loadWebsitePageRuntime`,\n * which reads the active locale's pack during SSR / pre-render,\n * - `wildo website audit`, which validates every locale pack against\n * every manifest and section through `loadAndValidateAllLabelPacks`,\n * - any build-time hook that wants to fail fast on a missing or\n * malformed pack.\n *\n * **Convention** (matches the slice plan + the validator's contract):\n *\n * - One file per locale: `<packsDir>/<locale>.json`\n * - JSON object: flat key-value map, `Record<string, string>`\n * - Keys are fully-composed `WebsiteLabelKey`s (`website.<page>.<section>.<slot>`)\n * - Values are the localized strings\n *\n * **Why synchronous I/O**:\n * - The Astro frontmatter executes at build / SSR time in a Node\n * process (no event loop pressure to relieve), and the bridge\n * wants the pack resolved before it emits any HTML — async would\n * force the consumer to `await` inside an Astro frontmatter,\n * which is supported but adds a needless yield-point on a single\n * small JSON file.\n * - The validator is pure and synchronous; making the loader async\n * would add a coloring boundary for no benefit.\n *\n * **Failure modes — all surfaced as thrown errors with actionable\n * messages**:\n * - File missing — wrapped in a `[label-pack-loader] Could not read\n * pack at <path>` error so a typo in the dir / locale is obvious.\n * - JSON parse failure — error includes the file path and the\n * parser's own message.\n * - Top-level shape is not an object, or a value is not a string —\n * error names the offending key + observed type.\n *\n * **Why three exports** (loader, multi-loader, validator-runner):\n * - `loadLabelPack(packsDir, locale)` is the atomic primitive\n * (one file → one validated `LabelPack`). Used by the bridge at\n * request time.\n * - `loadAllLabelPacks(packsDir, siteContext)` is the build-time\n * batch — every locale pack at once. Used by the CLI auditor\n * and by the build-start hook.\n * - `validateAllLabelPacks(packsDir, siteContext, expectedKeysBySectionRef)`\n * is the highest-level entry point: load every pack, run the\n * validator on each, and either return the diagnostic results\n * or throw on the first hard error. The returned shape carries\n * per-locale diagnostics so the caller can format a CLI report.\n *\n * **`siteContext` parameter** — the consumer's composed\n * `WebsiteSiteContext` (see `website-site-context.ts`). The loader\n * reads `siteContext.websiteConfig.locales` for \"which locales does\n * the site ship?\" and `siteContext.rootConfig.{pageManifests,\n * chromeLabelKeys, skipToMainContentLabelKey}` for the manifest\n * registry / chrome-key set the validator runs against.\n */\n\n/**\n * Load and validate a single locale's label pack. Synchronous —\n * suitable for Astro frontmatter execution.\n *\n * @param packsDir Absolute or cwd-relative path to the directory\n * containing per-locale `<locale>.json` files.\n * @param locale The locale to load. Must be a member of\n * `WildoMarketingWebsiteConfig.locales`.\n * @returns A flat `LabelPack` (`Record<string, string>`) ready to\n * feed into `WebsiteRuntimeContextValue.labelPack`.\n * @throws When the file is missing, malformed JSON, not an object,\n * or contains a non-string value.\n */\nexport function loadLabelPack(packsDir: string, locale: AvailableLanguage): LabelPack {\n const filePath = resolve(packsDir, `${locale}.json`);\n\n let raw: string;\n try {\n raw = readFileSync(filePath, 'utf-8');\n } catch (cause) {\n throw new Error(\n `[label-pack-loader] Could not read label pack at ${filePath} ` +\n `(locale=\"${locale}\"). ` +\n `Make sure the directory exists and contains a \"${locale}.json\" file. ` +\n `Underlying error: ${(cause as Error).message}`,\n { cause },\n );\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (cause) {\n throw new Error(\n `[label-pack-loader] Failed to parse JSON in ${filePath} ` +\n `(locale=\"${locale}\"). Underlying parser error: ${(cause as Error).message}`,\n { cause },\n );\n }\n\n return assertLabelPackShape(parsed, filePath, locale);\n}\n\n/**\n * Batch loader — every locale declared on the site at once. The map\n * iteration order matches the declaration order of\n * `siteContext.websiteConfig.locales` so CLI reports come out in a\n * deterministic, authored sequence.\n */\nexport function loadAllLabelPacks(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n): Map<AvailableLanguage, LabelPack> {\n const packs = new Map<AvailableLanguage, LabelPack>();\n for (const entry of siteContext.websiteConfig.locales) {\n packs.set(entry.locale, loadLabelPack(packsDir, entry.locale));\n }\n return packs;\n}\n\n/**\n * Per-locale validation result — the validator's flat output extended\n * with the locale tag so a multi-locale report can flatten + group.\n */\nexport interface LocaleLabelPackValidation {\n locale: AvailableLanguage;\n result: LabelPackValidationResult;\n}\n\n/**\n * Highest-level entry point: load every locale's pack and run the\n * label-pack validator against the manifest registry.\n *\n * **Return shape decision** — this function does NOT throw on\n * `missing` keys; it returns the per-locale results so the caller\n * (the build-start hook OR the CLI auditor OR a debugging\n * subprocess) can decide how to surface the failure (process exit\n * code, formatted CLI output, JSON report, …). A thin `throwIfAny…`\n * helper sits next to it for the common \"fail the build on first\n * missing key\" path.\n */\nexport function loadAndValidateAllLabelPacks(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n): readonly LocaleLabelPackValidation[] {\n const { rootConfig } = siteContext;\n const packs = loadAllLabelPacks(packsDir, siteContext);\n const manifests: readonly WebsitePageManifest[] = rootConfig.pageManifests;\n const effectiveChromeKeys = composeEffectiveChromeLabelKeys(rootConfig);\n\n const validations: LocaleLabelPackValidation[] = [];\n for (const [locale, pack] of packs) {\n validations.push({\n locale,\n result: validateLabelPackAgainstManifest(\n manifests,\n expectedKeysBySectionRef,\n pack,\n effectiveChromeKeys,\n ),\n });\n }\n return validations;\n}\n\n/**\n * Compose the chrome-label-key set the validator should treat as\n * \"expected\" — `WebsiteRootConfig.chromeLabelKeys` (consumer-authored)\n * UNION the framework-required `skipToMainContentLabelKey`.\n *\n * Centralized here because:\n * - Both `loadAndValidateAllLabelPacks` (used at build time) and\n * `buildKeyOriginMap` (used to annotate missing-key errors) need\n * the same composed list, and forking the union into two places\n * would be a drift hazard.\n * - The framework owns the skip-link key (it is rendered by\n * `WebsitePageLayout`, not the consumer's chrome), so the\n * consumer should NOT have to remember to also list it in\n * `chromeLabelKeys`. Auto-injection here keeps the contract\n * minimal at the consumer's call site (just declare the key in\n * `WebsiteRootConfig.skipToMainContentLabelKey` once + ship the\n * translation in every locale pack) while still surfacing the key\n * through the validator's \"missing\"/\"orphaned\" diagnostics.\n * - De-duplication is via `Set` semantics so a consumer who\n * defensively lists the key in both places (no-op but harmless)\n * does not produce a duplicate-key warning downstream.\n */\nfunction composeEffectiveChromeLabelKeys(\n rootConfig: WebsiteRootConfig,\n): readonly WebsiteLabelKey[] {\n const set = new Set<WebsiteLabelKey>(rootConfig.chromeLabelKeys);\n set.add(rootConfig.skipToMainContentLabelKey);\n // The visitor consent panel is framework-owned in the same way (#534): required in every pack, so a site\n // that turns on analytics can never ship it with an unlabelled way to refuse.\n for (const key of WEBSITE_CONSENT_LABEL_KEYS) set.add(key);\n // So is the provider-component slot's refusal wording (#1703): a site placing an address lookup must not\n // ship its consent-required or unavailable state as a raw key.\n for (const key of WEBSITE_PROVIDER_COMPONENT_LABEL_KEYS) set.add(key);\n return Array.from(set);\n}\n\n/**\n * Convenience wrapper for the build-start hook: validates every\n * declared locale's pack and throws a SINGLE aggregated error if any\n * locale has a hard failure (`missing` keys or `unmappedSectionRefs`).\n * Orphaned keys are logged via `console.warn` (soft warning per the\n * validator's contract) but do NOT throw.\n *\n * **Why aggregate (not first-fail)**: a multi-locale marketing site\n * with `en` and `fr` packs both missing keys would, under a\n * first-fail policy, force the author to fix `en`, re-run the build,\n * THEN see `fr`'s problem. Aggregating means one build run surfaces\n * every locale's hard errors together — material UX win for the\n * authoring loop. The aggregated message keeps each locale's failure\n * in its own block so the diagnostic stays scannable.\n *\n * **Why throw vs. return**: the build-start hook lives in\n * `astro.config.mjs` (or a `vite-plugin`-style integration) and\n * benefits from the shortest possible code path — a thrown error\n * naturally aborts `astro build` with a non-zero exit code and\n * surfaces the message in the build log. The richer\n * `loadAndValidateAllLabelPacks` is what `wildo website audit` calls\n * when it wants programmatic access to the per-locale results.\n *\n * **Key-origin annotation**: every `missing` key is tagged with the\n * page (and, if section-scoped, the section) that declared it, so the\n * author can grep + open the right `defineWebsiteSection` /\n * `defineWebsitePageManifest` call site without leaving the build log.\n */\nexport function assertLabelPacksValid(\n packsDir: string,\n siteContext: WebsiteSiteContext,\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n): void {\n const { rootConfig } = siteContext;\n const validations = loadAndValidateAllLabelPacks(\n packsDir,\n siteContext,\n expectedKeysBySectionRef,\n );\n\n /**\n * Build a key → origin map ONCE per assert call (cost is bounded by\n * the total number of declared keys across the whole site, which is\n * typically small for a marketing surface). Enriches the missing-key\n * error with the page/section that committed to each key — the\n * author can grep the ref to find the site authoring call site.\n */\n const keyOrigins = buildKeyOriginMap(\n rootConfig.pageManifests,\n expectedKeysBySectionRef,\n composeEffectiveChromeLabelKeys(rootConfig),\n );\n\n const localeErrorBlocks: string[] = [];\n\n for (const { locale, result } of validations) {\n const blocks: string[] = [];\n if (result.unmappedSectionRefs.length > 0) {\n blocks.push(\n ` unmappedSectionRefs (build-pipeline bug — sections referenced by a ` +\n `manifest but missing from the expected-keys collector): ` +\n `[${(result.unmappedSectionRefs as unknown as readonly string[]).join(', ')}]. ` +\n `Add every referenced section to the array passed to ` +\n `collectExpectedLabelKeysFromSections().`,\n );\n }\n if (result.missing.length > 0) {\n const annotated = (result.missing as unknown as readonly string[]).map(\n (key) => ` \"${key}\" ← ${keyOrigins.get(key) ?? 'unknown origin'}`,\n );\n blocks.push(\n ` missing ${result.missing.length} required key(s) in ${packsDir}/${locale}.json:\\n` +\n annotated.join('\\n'),\n );\n }\n if (blocks.length > 0) {\n localeErrorBlocks.push(`Locale \"${locale}\":\\n${blocks.join('\\n')}`);\n }\n if (result.orphaned.length > 0) {\n console.warn(\n `[label-pack-loader] Locale \"${locale}\": ${result.orphaned.length} ` +\n `orphaned key(s) in ${locale}.json (declared in pack but not consumed ` +\n `by any section or page manifest):\\n ${result.orphaned.join('\\n ')}`,\n );\n }\n }\n\n if (localeErrorBlocks.length > 0) {\n throw new Error(\n `[label-pack-loader] Label pack validation failed for ` +\n `${localeErrorBlocks.length} locale(s):\\n\\n${localeErrorBlocks.join('\\n\\n')}`,\n );\n }\n}\n\n/**\n * Build a `keyString -> \"page <pageRef> meta titleKey\" | \"section <sectionRef> in page <pageRef>\"`\n * lookup so the assert message can name the call site that committed\n * to each missing key. Pure and small (cost is O(totalKeys) per build\n * — bounded by the marketing surface).\n */\nfunction buildKeyOriginMap(\n manifests: readonly WebsitePageManifest[],\n expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,\n chromeLabelKeys: readonly WebsiteLabelKey[],\n): Map<string, string> {\n const origins = new Map<string, string>();\n for (const manifest of manifests) {\n const pageRefStr = manifest.ref as unknown as string;\n const titleKey = manifest.metaLabelKeys.titleKey as unknown as string;\n const descriptionKey = manifest.metaLabelKeys.descriptionKey as unknown as string;\n if (!origins.has(titleKey)) {\n origins.set(titleKey, `page \"${pageRefStr}\" metaLabelKeys.titleKey`);\n }\n if (!origins.has(descriptionKey)) {\n origins.set(descriptionKey, `page \"${pageRefStr}\" metaLabelKeys.descriptionKey`);\n }\n for (const sectionRef of manifest.sectionRefs) {\n const sectionRefStr = sectionRef as unknown as string;\n const sectionKeys = expectedKeysBySectionRef.get(sectionRef);\n if (sectionKeys === undefined) continue;\n for (const sectionKey of sectionKeys) {\n const keyStr = sectionKey as unknown as string;\n if (!origins.has(keyStr)) {\n origins.set(keyStr, `section \"${sectionRefStr}\" in page \"${pageRefStr}\"`);\n }\n }\n }\n }\n for (const chromeKey of chromeLabelKeys) {\n const keyStr = chromeKey as unknown as string;\n if (!origins.has(keyStr)) {\n origins.set(keyStr, `WebsiteRootConfig.chromeLabelKeys`);\n }\n }\n return origins;\n}\n\n/**\n * Defensive shape guard. Surface the offending key + observed type\n * so the consumer can find the malformed entry quickly. Pure\n * validation — no transformation; the returned value is the same\n * object reference passed in (cast to `LabelPack`) so the caller\n * does not pay an unnecessary copy.\n */\nfunction assertLabelPackShape(\n parsed: unknown,\n filePath: string,\n locale: AvailableLanguage,\n): LabelPack {\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(\n `[label-pack-loader] ${filePath} (locale=\"${locale}\") must contain a ` +\n `JSON object at the top level (got ${describeJson(parsed)}).`,\n );\n }\n for (const [key, value] of Object.entries(parsed)) {\n if (typeof value !== 'string') {\n throw new Error(\n `[label-pack-loader] ${filePath} (locale=\"${locale}\"): key \"${key}\" ` +\n `must map to a string (got ${describeJson(value)}).`,\n );\n }\n }\n return parsed as LabelPack;\n}\n\nfunction describeJson(value: unknown): string {\n if (value === null) return 'null';\n if (Array.isArray(value)) return 'array';\n return typeof value;\n}\n/** @wildo_source:part:end saas.website.astro.label-loader */\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"llms-txt-server.d.ts","sourceRoot":"","sources":["../../../src/astro/llms-txt-server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,kBAAkB,EAC/B,OAAO,GAAE,sBAA2B,GACnC,MAAM,GAAG,IAAI,CAqBf;AACD,gEAAgE"}
1
+ {"version":3,"file":"llms-txt-server.d.ts","sourceRoot":"","sources":["../../../../src/astro/llms-txt-server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,WAAW,EAAE,kBAAkB,EAC/B,OAAO,GAAE,sBAA2B,GACnC,MAAM,GAAG,IAAI,CAqBf;AACD,gEAAgE"}
@@ -1 +1 @@
1
- {"version":3,"file":"pick-labels-for-page.d.ts","sourceRoot":"","sources":["../../../src/astro/pick-labels-for-page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AACvF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,mBAAmB,EAC7B,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,SAAS,CAkBX;AACD,qEAAqE"}
1
+ {"version":3,"file":"pick-labels-for-page.d.ts","sourceRoot":"","sources":["../../../../src/astro/pick-labels-for-page.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AACvF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,mBAAmB,EAC7B,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,SAAS,CAkBX;AACD,qEAAqE"}
@@ -29,10 +29,12 @@ import type { WebsiteSiteContext } from './website-site-context';
29
29
  * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default
30
30
  * direction) — each named bot gets its own `User-agent: <bot>`
31
31
  * + `Disallow: /` block.
32
- * 3. Catch-all `User-agent: *` block carrying the site-wide
33
- * `policy` (`Disallow: /` if `policy === 'noindex'` OR if
34
- * `allowedCrawlers` is set, otherwise nothing) and any
35
- * `disallow[]` path-prefix entries.
32
+ * 3. Catch-all `User-agent: *` block: `Disallow: /` only when
33
+ * `allowedCrawlers` is set (the named bots are then the only ones
34
+ * permitted), otherwise the allow-everything `Disallow:` plus any
35
+ * `disallow[]` path-prefix entries. `policy === 'noindex'` adds
36
+ * nothing here — it is expressed by the page's robots meta tag,
37
+ * which a crawler can only read if it may fetch the page (#494).
36
38
  * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when
37
39
  * `seo.sitemap?.enabled !== false` (defaults to `true`).
38
40
  * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of
@@ -52,10 +54,9 @@ import type { WebsiteSiteContext } from './website-site-context';
52
54
  *
53
55
  * The `Sitemap:` directive is independent from any `User-agent`
54
56
  * block — it is a top-level signal to any crawler that the site
55
- * publishes a sitemap. Even when the catch-all policy is
56
- * `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line
57
- * stays useful: a crawler that ignores `noindex` (Bing was famous
58
- * for this) at least has a single canonical URL list to consult,
57
+ * publishes a sitemap. Even when the catch-all is `Disallow: /`
58
+ * (`allowedCrawlers` set), the `Sitemap:` line stays useful: the
59
+ * permitted crawlers have a single canonical URL list to consult,
59
60
  * and human readers of `robots.txt` see the publication contract
60
61
  * intact.
61
62
  *
@@ -1 +1 @@
1
- {"version":3,"file":"robots-renderer.d.ts","sourceRoot":"","sources":["../../../src/astro/robots-renderer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,kBAAkB,GAAG,MAAM,CA+FvE;AACD,gEAAgE"}
1
+ {"version":3,"file":"robots-renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/robots-renderer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,kBAAkB,GAAG,MAAM,CA8FvE;AACD,gEAAgE"}
@@ -28,10 +28,12 @@
28
28
  * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default
29
29
  * direction) — each named bot gets its own `User-agent: <bot>`
30
30
  * + `Disallow: /` block.
31
- * 3. Catch-all `User-agent: *` block carrying the site-wide
32
- * `policy` (`Disallow: /` if `policy === 'noindex'` OR if
33
- * `allowedCrawlers` is set, otherwise nothing) and any
34
- * `disallow[]` path-prefix entries.
31
+ * 3. Catch-all `User-agent: *` block: `Disallow: /` only when
32
+ * `allowedCrawlers` is set (the named bots are then the only ones
33
+ * permitted), otherwise the allow-everything `Disallow:` plus any
34
+ * `disallow[]` path-prefix entries. `policy === 'noindex'` adds
35
+ * nothing here — it is expressed by the page's robots meta tag,
36
+ * which a crawler can only read if it may fetch the page (#494).
35
37
  * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when
36
38
  * `seo.sitemap?.enabled !== false` (defaults to `true`).
37
39
  * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of
@@ -51,10 +53,9 @@
51
53
  *
52
54
  * The `Sitemap:` directive is independent from any `User-agent`
53
55
  * block — it is a top-level signal to any crawler that the site
54
- * publishes a sitemap. Even when the catch-all policy is
55
- * `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line
56
- * stays useful: a crawler that ignores `noindex` (Bing was famous
57
- * for this) at least has a single canonical URL list to consult,
56
+ * publishes a sitemap. Even when the catch-all is `Disallow: /`
57
+ * (`allowedCrawlers` set), the `Sitemap:` line stays useful: the
58
+ * permitted crawlers have a single canonical URL list to consult,
58
59
  * and human readers of `robots.txt` see the publication contract
59
60
  * intact.
60
61
  *
@@ -106,13 +107,12 @@ export function renderRobotsTxt(siteContext) {
106
107
  blocks.push(`User-agent: ${bot}\nDisallow: /`);
107
108
  }
108
109
  /**
109
- * Block 3 — catch-all `User-agent: *`. Two policy-driven shapes:
110
- * - `noindex` → `Disallow: /` (the site-wide closure).
111
- * - `index` (default) → `Disallow:` is the spec-canonical "allow
112
- * everything" line; combined with any per-path `Disallow: <path>`
113
- * entries from `disallow[]`. When `allowedCrawlers` is set, the
114
- * catch-all also closes the rest of the crawler universe with
115
- * `Disallow: /` so the named bots are the ONLY ones permitted.
110
+ * Block 3 — catch-all `User-agent: *`. `Disallow:` is the
111
+ * spec-canonical "allow everything" line, combined with any per-path
112
+ * `Disallow: <path>` entries from `disallow[]`. When `allowedCrawlers`
113
+ * is set, the catch-all instead closes the rest of the crawler universe
114
+ * with `Disallow: /` so the named bots are the ONLY ones permitted. The
115
+ * `policy` is not expressed here at all — see below.
116
116
  */
117
117
  const catchAllLines = ['User-agent: *'];
118
118
  /*
@@ -1 +1 @@
1
- {"version":3,"file":"robots-renderer.js","sourceRoot":"","sources":["../../../src/astro/robots-renderer.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AACH,MAAM,UAAU,eAAe,CAAC,WAA+B;IAC7D,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,qKAAqK,CACtK,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAoC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC;IACtE,MAAM,MAAM,GAAwB,MAAM,EAAE,MAAM,IAAI,OAAO,CAAC;IAC9D,MAAM,eAAe,GAAG,MAAM,EAAE,eAAe,IAAI,EAAE,CAAC;IACtD,MAAM,kBAAkB,GAAG,MAAM,EAAE,kBAAkB,IAAI,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAC;IAExC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B;;;OAGG;IACH,KAAK,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,eAAe,GAAG,YAAY,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,KAAK,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC,CAAC;IACjD,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,aAAa,GAAa,CAAC,eAAe,CAAC,CAAC;IAClD;;;;;;;;;;;;OAYG;IACH,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACpC,CAAC;SAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC;;;;;WAKG;QACH,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,aAAa,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAEtC;;;;;OAKG;IACH,MAAM,cAAc,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,IAAI,IAAI,CAAC;IAC/D,IAAI,cAAc,EAAE,CAAC;QACnB;;;;WAIG;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC9D,MAAM,CAAC,IAAI,CAAC,YAAY,MAAM,oBAAoB,CAAC,CAAC;IACtD,CAAC;IAED;;;OAGG;IACH,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;AACpC,CAAC;AACD,gEAAgE","sourcesContent":["import type {\n WebsiteRobotsConfig,\n} from '../schemas/manifests/website-root-config.shared.schemas';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.robots-renderer facet:layer:astro facet:family:website\n *\n * `renderRobotsTxt(siteContext)` — pure string builder for the\n * marketing site's `robots.txt` body. The consumer wires a thin\n * `src/pages/robots.txt.ts` Astro endpoint that imports this helper\n * + `WEBSITE_SITE_CONTEXT` and returns its output as a `Response`\n * with `Content-Type: text/plain` (see `website-seo-aeo-geo` SKILL).\n *\n * # Why a pure string builder (not an Astro component)\n *\n * Same rationale as `renderWebsitePageHeadHtml`:\n * 1. Pure TypeScript ships through the `@wildo-ai/saas-website/astro`\n * subpath barrel (Astro components do not).\n * 2. Vitest-friendly — every policy permutation is asserted\n * against the returned string without an Astro test harness.\n * 3. The Astro endpoint stays a 3-line shim the consumer can\n * audit at a glance.\n *\n * # Output shape\n *\n * The emitted text follows the de-facto `robots.txt` ordering\n * (RFC 9309 §2 + Google's published guidance):\n *\n * 1. Per-bot blocks for `allowedCrawlers` (deny-by-default\n * direction) — each named bot gets its own `User-agent: <bot>`\n * + `Allow: /` block.\n * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default\n * direction) — each named bot gets its own `User-agent: <bot>`\n * + `Disallow: /` block.\n * 3. Catch-all `User-agent: *` block carrying the site-wide\n * `policy` (`Disallow: /` if `policy === 'noindex'` OR if\n * `allowedCrawlers` is set, otherwise nothing) and any\n * `disallow[]` path-prefix entries.\n * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when\n * `seo.sitemap?.enabled !== false` (defaults to `true`).\n * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of\n * whether the site has one or many sitemap files (the index\n * is always-present per the plugin's contract), so this is\n * the stable URL to advertise.\n *\n * # Mutual-exclusion invariant\n *\n * `WebsiteRobotsConfigSchema.superRefine` already guarantees\n * `allowedCrawlers` and `disallowedCrawlers` are NOT both set, so\n * step 1 and step 2 above are mutually exclusive at the input\n * layer. The string builder is defensive (handles both being unset)\n * but does NOT re-validate the invariant.\n *\n * # Why we always emit `Sitemap:` footer when `enabled !== false`\n *\n * The `Sitemap:` directive is independent from any `User-agent`\n * block — it is a top-level signal to any crawler that the site\n * publishes a sitemap. Even when the catch-all policy is\n * `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line\n * stays useful: a crawler that ignores `noindex` (Bing was famous\n * for this) at least has a single canonical URL list to consult,\n * and human readers of `robots.txt` see the publication contract\n * intact.\n *\n * # Failure modes\n *\n * - `siteContext.websiteConfig.publicOrigin` undefined → throws.\n * The marketing-site origin is the SINGLE source of truth for\n * absolute URL composition (see `WildoMarketingWebsiteConfig`\n * JSDoc); a missing origin is a configuration error, not a\n * silent fallback opportunity.\n *\n * @example\n * ```ts\n * // examples/<app>/website/src/pages/robots.txt.ts\n * import type { APIRoute } from 'astro';\n * import { renderRobotsTxt } from '@wildo-ai/saas-website/astro';\n * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';\n *\n * export const GET: APIRoute = () =>\n * new Response(renderRobotsTxt(WEBSITE_SITE_CONTEXT), {\n * headers: { 'Content-Type': 'text/plain; charset=utf-8' },\n * });\n * ```\n */\nexport function renderRobotsTxt(siteContext: WebsiteSiteContext): string {\n const { rootConfig, websiteConfig } = siteContext;\n\n if (!websiteConfig.publicOrigin) {\n throw new Error(\n 'renderRobotsTxt: WildoMarketingWebsiteConfig.publicOrigin is required to compose the absolute Sitemap: URL. Author it in your service\\'s `wildo.website.config.ts`.',\n );\n }\n\n const robots: WebsiteRobotsConfig | undefined = rootConfig.seo.robots;\n const policy: 'index' | 'noindex' = robots?.policy ?? 'index';\n const allowedCrawlers = robots?.allowedCrawlers ?? [];\n const disallowedCrawlers = robots?.disallowedCrawlers ?? [];\n const disallow = robots?.disallow ?? [];\n\n const blocks: string[] = [];\n\n /**\n * Block 1 — per-bot `Allow: /` for the deny-by-default direction.\n * Empty when `allowedCrawlers` is unset (the dominant case).\n */\n for (const bot of allowedCrawlers) {\n blocks.push(`User-agent: ${bot}\\nAllow: /`);\n }\n\n /**\n * Block 2 — per-bot `Disallow: /` for the allow-by-default\n * direction. Empty when `disallowedCrawlers` is unset (the\n * dominant case for a public marketing site).\n */\n for (const bot of disallowedCrawlers) {\n blocks.push(`User-agent: ${bot}\\nDisallow: /`);\n }\n\n /**\n * Block 3 — catch-all `User-agent: *`. Two policy-driven shapes:\n * - `noindex` → `Disallow: /` (the site-wide closure).\n * - `index` (default) → `Disallow:` is the spec-canonical \"allow\n * everything\" line; combined with any per-path `Disallow: <path>`\n * entries from `disallow[]`. When `allowedCrawlers` is set, the\n * catch-all also closes the rest of the crawler universe with\n * `Disallow: /` so the named bots are the ONLY ones permitted.\n */\n const catchAllLines: string[] = ['User-agent: *'];\n /*\n * `policy === 'noindex'` deliberately does NOT emit `Disallow: /` (#494).\n *\n * `Disallow` governs CRAWLING; the `<meta name=\"robots\" content=\"noindex\">` emitted by the page-head\n * renderer governs INDEXING, and it is the one that keeps a site out of search. Barring the crawler\n * here would mean it never fetches the page and never reads that directive — so the pair defeated\n * itself, and the site stayed indexable as a bare URL. Closure is expressed in the document; this\n * file's job for that policy is to stay out of the way.\n *\n * `allowedCrawlers` is a DIFFERENT feature and keeps its `Disallow: /`: there the catch-all closes\n * the rest of the crawler universe so the named bots are the only ones permitted, which is about\n * crawling and is exactly what `Disallow` is for.\n */\n if (allowedCrawlers.length > 0) {\n catchAllLines.push('Disallow: /');\n } else if (disallow.length === 0) {\n /**\n * Spec-canonical \"allow everything\" — `Disallow:` with no path.\n * Some crawlers treat the absence of any `Disallow:` line as\n * undefined behavior; an explicit empty Disallow is the safe\n * shape.\n */\n catchAllLines.push('Disallow:');\n }\n for (const path of disallow) {\n catchAllLines.push(`Disallow: ${path}`);\n }\n blocks.push(catchAllLines.join('\\n'));\n\n /**\n * Block 4 — `Sitemap:` footer, advertised whenever sitemap\n * emission is enabled (the marketing-site default). See file\n * preamble for why we always advertise `sitemap-index.xml` as\n * the stable URL.\n */\n const sitemapEnabled = rootConfig.seo.sitemap?.enabled ?? true;\n if (sitemapEnabled) {\n /**\n * Trim trailing slash on the origin to avoid emitting\n * `https://example.com//sitemap-index.xml`. `URL` constructor\n * is overkill for a single concat; the regex is enough.\n */\n const origin = websiteConfig.publicOrigin.replace(/\\/+$/, '');\n blocks.push(`Sitemap: ${origin}/sitemap-index.xml`);\n }\n\n /**\n * Spec-conformant trailing newline. Some crawler parsers tolerate\n * its absence; emitting it is universally safe.\n */\n return `${blocks.join('\\n\\n')}\\n`;\n}\n/** @wildo_source:part:end saas.website.astro.robots-renderer */\n"]}
1
+ {"version":3,"file":"robots-renderer.js","sourceRoot":"","sources":["../../../../src/astro/robots-renderer.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,MAAM,UAAU,eAAe,CAAC,WAA+B;IAC7D,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,qKAAqK,CACtK,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAoC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC;IACtE,MAAM,MAAM,GAAwB,MAAM,EAAE,MAAM,IAAI,OAAO,CAAC;IAC9D,MAAM,eAAe,GAAG,MAAM,EAAE,eAAe,IAAI,EAAE,CAAC;IACtD,MAAM,kBAAkB,GAAG,MAAM,EAAE,kBAAkB,IAAI,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAC;IAExC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B;;;OAGG;IACH,KAAK,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,eAAe,GAAG,YAAY,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,KAAK,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC,CAAC;IACjD,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,aAAa,GAAa,CAAC,eAAe,CAAC,CAAC;IAClD;;;;;;;;;;;;OAYG;IACH,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACpC,CAAC;SAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC;;;;;WAKG;QACH,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,aAAa,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAEtC;;;;;OAKG;IACH,MAAM,cAAc,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,IAAI,IAAI,CAAC;IAC/D,IAAI,cAAc,EAAE,CAAC;QACnB;;;;WAIG;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC9D,MAAM,CAAC,IAAI,CAAC,YAAY,MAAM,oBAAoB,CAAC,CAAC;IACtD,CAAC;IAED;;;OAGG;IACH,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;AACpC,CAAC;AACD,gEAAgE","sourcesContent":["import type {\n WebsiteRobotsConfig,\n} from '../schemas/manifests/website-root-config.shared.schemas';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.robots-renderer facet:layer:astro facet:family:website\n *\n * `renderRobotsTxt(siteContext)` — pure string builder for the\n * marketing site's `robots.txt` body. The consumer wires a thin\n * `src/pages/robots.txt.ts` Astro endpoint that imports this helper\n * + `WEBSITE_SITE_CONTEXT` and returns its output as a `Response`\n * with `Content-Type: text/plain` (see `website-seo-aeo-geo` SKILL).\n *\n * # Why a pure string builder (not an Astro component)\n *\n * Same rationale as `renderWebsitePageHeadHtml`:\n * 1. Pure TypeScript ships through the `@wildo-ai/saas-website/astro`\n * subpath barrel (Astro components do not).\n * 2. Vitest-friendly — every policy permutation is asserted\n * against the returned string without an Astro test harness.\n * 3. The Astro endpoint stays a 3-line shim the consumer can\n * audit at a glance.\n *\n * # Output shape\n *\n * The emitted text follows the de-facto `robots.txt` ordering\n * (RFC 9309 §2 + Google's published guidance):\n *\n * 1. Per-bot blocks for `allowedCrawlers` (deny-by-default\n * direction) — each named bot gets its own `User-agent: <bot>`\n * + `Allow: /` block.\n * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default\n * direction) — each named bot gets its own `User-agent: <bot>`\n * + `Disallow: /` block.\n * 3. Catch-all `User-agent: *` block: `Disallow: /` only when\n * `allowedCrawlers` is set (the named bots are then the only ones\n * permitted), otherwise the allow-everything `Disallow:` plus any\n * `disallow[]` path-prefix entries. `policy === 'noindex'` adds\n * nothing here — it is expressed by the page's robots meta tag,\n * which a crawler can only read if it may fetch the page (#494).\n * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when\n * `seo.sitemap?.enabled !== false` (defaults to `true`).\n * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of\n * whether the site has one or many sitemap files (the index\n * is always-present per the plugin's contract), so this is\n * the stable URL to advertise.\n *\n * # Mutual-exclusion invariant\n *\n * `WebsiteRobotsConfigSchema.superRefine` already guarantees\n * `allowedCrawlers` and `disallowedCrawlers` are NOT both set, so\n * step 1 and step 2 above are mutually exclusive at the input\n * layer. The string builder is defensive (handles both being unset)\n * but does NOT re-validate the invariant.\n *\n * # Why we always emit `Sitemap:` footer when `enabled !== false`\n *\n * The `Sitemap:` directive is independent from any `User-agent`\n * block — it is a top-level signal to any crawler that the site\n * publishes a sitemap. Even when the catch-all is `Disallow: /`\n * (`allowedCrawlers` set), the `Sitemap:` line stays useful: the\n * permitted crawlers have a single canonical URL list to consult,\n * and human readers of `robots.txt` see the publication contract\n * intact.\n *\n * # Failure modes\n *\n * - `siteContext.websiteConfig.publicOrigin` undefined → throws.\n * The marketing-site origin is the SINGLE source of truth for\n * absolute URL composition (see `WildoMarketingWebsiteConfig`\n * JSDoc); a missing origin is a configuration error, not a\n * silent fallback opportunity.\n *\n * @example\n * ```ts\n * // examples/<app>/website/src/pages/robots.txt.ts\n * import type { APIRoute } from 'astro';\n * import { renderRobotsTxt } from '@wildo-ai/saas-website/astro';\n * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';\n *\n * export const GET: APIRoute = () =>\n * new Response(renderRobotsTxt(WEBSITE_SITE_CONTEXT), {\n * headers: { 'Content-Type': 'text/plain; charset=utf-8' },\n * });\n * ```\n */\nexport function renderRobotsTxt(siteContext: WebsiteSiteContext): string {\n const { rootConfig, websiteConfig } = siteContext;\n\n if (!websiteConfig.publicOrigin) {\n throw new Error(\n 'renderRobotsTxt: WildoMarketingWebsiteConfig.publicOrigin is required to compose the absolute Sitemap: URL. Author it in your service\\'s `wildo.website.config.ts`.',\n );\n }\n\n const robots: WebsiteRobotsConfig | undefined = rootConfig.seo.robots;\n const policy: 'index' | 'noindex' = robots?.policy ?? 'index';\n const allowedCrawlers = robots?.allowedCrawlers ?? [];\n const disallowedCrawlers = robots?.disallowedCrawlers ?? [];\n const disallow = robots?.disallow ?? [];\n\n const blocks: string[] = [];\n\n /**\n * Block 1 — per-bot `Allow: /` for the deny-by-default direction.\n * Empty when `allowedCrawlers` is unset (the dominant case).\n */\n for (const bot of allowedCrawlers) {\n blocks.push(`User-agent: ${bot}\\nAllow: /`);\n }\n\n /**\n * Block 2 — per-bot `Disallow: /` for the allow-by-default\n * direction. Empty when `disallowedCrawlers` is unset (the\n * dominant case for a public marketing site).\n */\n for (const bot of disallowedCrawlers) {\n blocks.push(`User-agent: ${bot}\\nDisallow: /`);\n }\n\n /**\n * Block 3 — catch-all `User-agent: *`. `Disallow:` is the\n * spec-canonical \"allow everything\" line, combined with any per-path\n * `Disallow: <path>` entries from `disallow[]`. When `allowedCrawlers`\n * is set, the catch-all instead closes the rest of the crawler universe\n * with `Disallow: /` so the named bots are the ONLY ones permitted. The\n * `policy` is not expressed here at all — see below.\n */\n const catchAllLines: string[] = ['User-agent: *'];\n /*\n * `policy === 'noindex'` deliberately does NOT emit `Disallow: /` (#494).\n *\n * `Disallow` governs CRAWLING; the `<meta name=\"robots\" content=\"noindex\">` emitted by the page-head\n * renderer governs INDEXING, and it is the one that keeps a site out of search. Barring the crawler\n * here would mean it never fetches the page and never reads that directive — so the pair defeated\n * itself, and the site stayed indexable as a bare URL. Closure is expressed in the document; this\n * file's job for that policy is to stay out of the way.\n *\n * `allowedCrawlers` is a DIFFERENT feature and keeps its `Disallow: /`: there the catch-all closes\n * the rest of the crawler universe so the named bots are the only ones permitted, which is about\n * crawling and is exactly what `Disallow` is for.\n */\n if (allowedCrawlers.length > 0) {\n catchAllLines.push('Disallow: /');\n } else if (disallow.length === 0) {\n /**\n * Spec-canonical \"allow everything\" — `Disallow:` with no path.\n * Some crawlers treat the absence of any `Disallow:` line as\n * undefined behavior; an explicit empty Disallow is the safe\n * shape.\n */\n catchAllLines.push('Disallow:');\n }\n for (const path of disallow) {\n catchAllLines.push(`Disallow: ${path}`);\n }\n blocks.push(catchAllLines.join('\\n'));\n\n /**\n * Block 4 — `Sitemap:` footer, advertised whenever sitemap\n * emission is enabled (the marketing-site default). See file\n * preamble for why we always advertise `sitemap-index.xml` as\n * the stable URL.\n */\n const sitemapEnabled = rootConfig.seo.sitemap?.enabled ?? true;\n if (sitemapEnabled) {\n /**\n * Trim trailing slash on the origin to avoid emitting\n * `https://example.com//sitemap-index.xml`. `URL` constructor\n * is overkill for a single concat; the regex is enough.\n */\n const origin = websiteConfig.publicOrigin.replace(/\\/+$/, '');\n blocks.push(`Sitemap: ${origin}/sitemap-index.xml`);\n }\n\n /**\n * Spec-conformant trailing newline. Some crawler parsers tolerate\n * its absence; emitting it is universally safe.\n */\n return `${blocks.join('\\n\\n')}\\n`;\n}\n/** @wildo_source:part:end saas.website.astro.robots-renderer */\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"sitemap-coverage.d.ts","sourceRoot":"","sources":["../../../src/astro/sitemap-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAK9E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,MAAM,WAAW,yBAAyB;IACxC,iDAAiD;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0BAA0B;IAC1B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACrF;AAED,wBAAgB,iCAAiC,CAC/C,WAAW,EAAE,kBAAkB,GAC9B,SAAS,yBAAyB,EAAE,CAiFtC;AACD,iEAAiE"}
1
+ {"version":3,"file":"sitemap-coverage.d.ts","sourceRoot":"","sources":["../../../../src/astro/sitemap-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAK9E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,MAAM,WAAW,yBAAyB;IACxC,iDAAiD;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0BAA0B;IAC1B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACrF;AAED,wBAAgB,iCAAiC,CAC/C,WAAW,EAAE,kBAAkB,GAC9B,SAAS,yBAAyB,EAAE,CAiFtC;AACD,iEAAiE"}
@@ -1 +1 @@
1
- {"version":3,"file":"structured-data.renderer.d.ts","sourceRoot":"","sources":["../../../src/astro/structured-data.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAE7B,MAAM,mEAAmE,CAAC;AAC3E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+DAA+D,CAAC;AAgD9G,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,IAAI,CAAC,wBAAwB,EAAE,YAAY,GAAG,mBAAmB,CAAC,EAAE,CAAC;IACzF;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,SAAS,4BAA4B,EAAE,CAAC;CACxD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,+BAA+B,GAAG,MAAM,CAiDtF;AAoDD,yEAAyE"}
1
+ {"version":3,"file":"structured-data.renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/structured-data.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAE7B,MAAM,mEAAmE,CAAC;AAC3E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+DAA+D,CAAC;AAgD9G,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,IAAI,CAAC,wBAAwB,EAAE,YAAY,GAAG,mBAAmB,CAAC,EAAE,CAAC;IACzF;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,SAAS,4BAA4B,EAAE,CAAC;CACxD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,+BAA+B,GAAG,MAAM,CAiDtF;AAoDD,yEAAyE"}
@@ -1,5 +1,5 @@
1
1
  import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
- import type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
2
+ import { type WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
3
3
  import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
4
4
  import type { LabelPack } from '../schemas/validators/label-pack.validator';
5
5
  import type { WebsiteSiteContext } from './website-site-context';
@@ -1 +1 @@
1
- {"version":3,"file":"website-page-head.renderer.d.ts","sourceRoot":"","sources":["../../../src/astro/website-page-head.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAG9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+DAA+D,CAAC;AACzG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAG5E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B;;;;;OAKG;IACH,WAAW,EAAE,kBAAkB,CAAC;IAChC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,gCAAgC,GAAG,MAAM,CAuLxF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAehF;AAED;;;;;;;;GAQG"}
1
+ {"version":3,"file":"website-page-head.renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/website-page-head.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAG9E,OAAO,EAAiC,KAAK,mBAAmB,EAAE,MAAM,+DAA+D,CAAC;AACxI,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAG5E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B;;;;;OAKG;IACH,WAAW,EAAE,kBAAkB,CAAC;IAChC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,gCAAgC,GAAG,MAAM,CAuLxF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAehF;AAED;;;;;;;;GAQG"}
@@ -1,4 +1,5 @@
1
1
  import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils.js';
2
+ import { websiteDesignTokenCssProperty } from '../schemas/design-tokens/website-design-tokens.shared.schemas.js';
2
3
  import { escapeHtmlAttribute, escapeHtmlText } from './internal/html-escape.js';
3
4
  export function renderWebsitePageHeadHtml(opts) {
4
5
  const { manifest, siteContext, locale, labelPack } = opts;
@@ -181,7 +182,7 @@ export function renderWebsiteDesignTokensCss(tokens) {
181
182
  if (groupValue !== null && typeof groupValue === 'object' && !Array.isArray(groupValue)) {
182
183
  for (const [tokenName, tokenValue] of Object.entries(groupValue)) {
183
184
  if (typeof tokenValue === 'string') {
184
- lines.push(` --website-${groupName}-${tokenName}: ${tokenValue};`);
185
+ lines.push(` ${websiteDesignTokenCssProperty(groupName, tokenName)}: ${tokenValue};`);
185
186
  }
186
187
  }
187
188
  }
@@ -1 +1 @@
1
- {"version":3,"file":"website-page-head.renderer.js","sourceRoot":"","sources":["../../../src/astro/website-page-head.renderer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AAKjF,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAkE7E,MAAM,UAAU,yBAAyB,CAAC,IAAsC;IAC9E,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC1D,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,QAA6B,CAAC;IACtE,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa,CAAC,cAAmC,CAAC;IAClF,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IAClC,MAAM,WAAW,GAAG,SAAS,CAAC,cAAc,CAAC,CAAC;IAE9C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,sDAAsD,MAAM,eAAe;YACzE,uBAAuB,QAAQ,gBAAgB,QAAQ,CAAC,GAAwB,MAAM;YACtF,0EAA0E,CAC7E,CAAC;IACJ,CAAC;IACD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,sDAAsD,MAAM,eAAe;YACzE,6BAA6B,cAAc,gBAAgB,QAAQ,CAAC,GAAwB,MAAM;YAClG,0EAA0E,CAC7E,CAAC;IACJ,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,IAAI,UAAU,CAAC,GAAG,CAAC,iBAAiB,CAAC;IACzF,MAAM,SAAS,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC7C,MAAM,eAAe,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAExC;;;;;;;;;;;;OAYG;IACH,MAAM,SAAS,GAAG,QAAQ,CAAC,SAA8B,CAAC;IAC1D,MAAM,UAAU,GAAG,aAAa,CAAC,YAAY,CAAC;IAC9C,MAAM,aAAa,GAAG,aAAa,CAAC,aAAa,CAAC;IAClD,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAExC;;;;;;OAMG;IACH,SAAS,gBAAgB,CAAC,SAA4B;QACpD,MAAM,IAAI,GAAG,uBAAuB,CAAC;YACnC,MAAM,EAAE,SAAS;YACjB,aAAa;YACb,SAAS;SACV,CAAC,CAAC;QACH,OAAO,GAAG,UAAU,GAAG,IAAI,EAAE,CAAC;IAChC,CAAC;IAED,MAAM,YAAY,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAE9C,MAAM,aAAa,GAAa,EAAE,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,OAAO,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACvC,aAAa,CAAC,IAAI,CAChB,mCAAmC,mBAAmB,CAAC,SAAS,CAAC,WAAW,mBAAmB,CAAC,gBAAgB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CACtI,CAAC;IACJ,CAAC;IACD,aAAa,CAAC,IAAI,CAChB,oDAAoD,mBAAmB,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC,MAAM,CAC/G,CAAC;IAEF;;;;;OAKG;IACH,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;QACpD,YAAY,CAAC,IAAI,CACf,sCAAsC,WAAW,MAAM,EACvD,uCAAuC,WAAW,MAAM,CACzD,CAAC;IACJ,CAAC;IACD,MAAM,eAAe,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,CAAC;IAErF;;;;;;;;;;;;OAYG;IACH,MAAM,sBAAsB,GAAa,EAAE,CAAC;IAC5C,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,OAAO,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,SAAS,KAAK,gBAAgB;YAAE,SAAS;QAC7C,sBAAsB,CAAC,IAAI,CACzB,iDAAiD,mBAAmB,CAAC,SAAS,CAAC,MAAM,CACtF,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAAG,mBAAmB,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAClE,MAAM,aAAa,GAAG,mBAAmB,CAAC,YAAY,CAAC,CAAC;IACxD,MAAM,YAAY,GAAG,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;IAE3D;;;;;;;;;;;;;;OAcG;IACH,MAAM,gBAAgB,GAAa,EAAE,CAAC;IACtC,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;QAChD,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACnC,CAAC;IACD;;;;;;;;OAQG;IACH,IAAI,QAAQ,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,EAAE,CAAC;QACtC,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC7C,CAAC;IACD;;;OAGG;IACH,MAAM,aAAa,GAAa,gBAAgB,CAAC,MAAM,GAAG,CAAC;QACzD,CAAC,CAAC,CAAC,gCAAgC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QACrE,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO;QACL,UAAU,SAAS,UAAU;QAC7B,qCAAqC,eAAe,MAAM;QAC1D,+BAA+B,aAAa,MAAM;QAClD,sCAAsC,SAAS,MAAM;QACrD,4CAA4C,eAAe,MAAM;QACjE,+CAA+C;QAC/C,oCAAoC,aAAa,MAAM;QACvD,0CAA0C,YAAY,MAAM;QAC5D,uCAAuC,YAAY,MAAM;QACzD,GAAG,sBAAsB;QACzB,GAAG,YAAY;QACf,sCAAsC,eAAe,MAAM;QAC3D,uCAAuC,SAAS,MAAM;QACtD,6CAA6C,eAAe,MAAM;QAClE,GAAG,aAAa;QAChB,GAAG,aAAa;KACjB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAA2B;IACtE,MAAM,KAAK,GAAa,CAAC,SAAS,CAAC,CAAC;IACpC,KAAK,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAwB,EAAE,CAAC;QACpF,IAAI,UAAU,KAAK,IAAI,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YACxF,KAAK,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAwB,EAAE,CAAC;gBACxF,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;oBACnC,KAAK,CAAC,IAAI,CAAC,eAAe,SAAS,IAAI,SAAS,KAAK,UAAU,GAAG,CAAC,CAAC;gBACtE,CAAC;YACH,CAAC;QACH,CAAC;aAAM,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,eAAe,SAAS,KAAK,UAAU,GAAG,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;GAQG","sourcesContent":["import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';\nimport type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\nimport { escapeHtmlAttribute, escapeHtmlText } from './internal/html-escape';\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.head-renderer facet:layer:astro facet:family:website\n *\n * Head-block renderers — pure string builders the consumer's per-page\n * `.astro` file injects via `<Fragment set:html={...}/>` /\n * `<style set:html={...}></style>`. Two functions:\n *\n * - `renderWebsitePageHeadHtml(opts)` — emits the complete SEO\n * `<head>` block: `<title>`, meta description, full Open Graph\n * card (`og:title` / `og:description` / `og:type` / `og:url` /\n * `og:site_name` / `og:locale` / `og:locale:alternate` / optional\n * `og:image`), Twitter card mirrors, `<link rel=\"canonical\">`,\n * and one `<link rel=\"alternate\" hreflang>` per declared locale\n * (plus an `x-default` for Google's i18n discoverability). Every\n * URL is ABSOLUTE (composed from `websiteConfig.publicOrigin` +\n * the page's `routePath`) — relative hreflang/canonical\n * annotations are not honored consistently across crawlers\n * (Phase 6 audit findings H-B / H-C / M-A).\n * - `renderWebsiteDesignTokensCss(tokens)` — flattens the design-token\n * bundle into `--website-*` custom properties scoped to `:root`.\n * Server-static — zero island JS.\n *\n * **Why pure string builders** (rather than React components or Astro\n * components):\n *\n * 1. **Avoids the `.astro` distribution problem.** A `.astro` shell\n * cannot be re-exported from a TS subpath barrel and `tsc` does\n * not copy `.astro` files to `dist/`. Pure TS sidesteps both.\n * 2. **Avoids the React-island boundary.** Rendering head content\n * via React would require either (a) a server-only React subtree\n * (extra moving part) or (b) hydrating the `<head>` on the client\n * (waste of bundle size for static-build content).\n * 3. **Vitest-friendly.** Pure functions returning strings are\n * trivially asserted against without an Astro test harness.\n *\n * **HTML escaping**: every consumer-supplied string (title,\n * description, OG image URL, hreflang values) is escaped with\n * `escapeHtmlAttribute` / `escapeHtmlText` before interpolation into\n * the emitted markup. Marketing copy frequently contains characters\n * (quotes, ampersands, the rare `<`) that would break the surrounding\n * tag if interpolated raw — escaping is non-negotiable for a public\n * surface.\n *\n * **Failure modes** — `renderWebsitePageHeadHtml` throws when:\n * - `manifest.metaLabelKeys.titleKey` is absent from the pack\n * - `manifest.metaLabelKeys.descriptionKey` is absent from the pack\n * Both throw with the locale + key + actionable next step (`Run\n * \\`wildo website audit\\` or add the entry to <pack-path>.json`).\n */\n\nexport interface RenderWebsitePageHeadHtmlOptions {\n manifest: WebsitePageManifest;\n /**\n * Composed `WebsiteSiteContext` — the renderer reads\n * `websiteConfig.{defaultLocale, locales, publicOrigin}` for the\n * URL/locale surface and `rootConfig.seo.{siteName, defaultOgImageUrl}`\n * for the SEO branding fallback.\n */\n siteContext: WebsiteSiteContext;\n locale: AvailableLanguage;\n labelPack: LabelPack;\n}\n\nexport function renderWebsitePageHeadHtml(opts: RenderWebsitePageHeadHtmlOptions): string {\n const { manifest, siteContext, locale, labelPack } = opts;\n const { rootConfig, websiteConfig } = siteContext;\n\n const titleKey = manifest.metaLabelKeys.titleKey as unknown as string;\n const descriptionKey = manifest.metaLabelKeys.descriptionKey as unknown as string;\n const title = labelPack[titleKey];\n const description = labelPack[descriptionKey];\n\n if (title === undefined) {\n throw new Error(\n `[renderWebsitePageHeadHtml] Label pack for locale \"${locale}\" is missing ` +\n `the page title key \"${titleKey}\" (page ref=\"${manifest.ref as unknown as string}\"). ` +\n `Run \\`wildo website audit\\` or add the entry to your <locale>.json pack.`,\n );\n }\n if (description === undefined) {\n throw new Error(\n `[renderWebsitePageHeadHtml] Label pack for locale \"${locale}\" is missing ` +\n `the page description key \"${descriptionKey}\" (page ref=\"${manifest.ref as unknown as string}\"). ` +\n `Run \\`wildo website audit\\` or add the entry to your <locale>.json pack.`,\n );\n }\n\n /**\n * OG image precedence: per-page `manifest.metaLabelKeys.ogImageUrl`\n * wins; falls back to the site-level `seo.defaultOgImageUrl` so\n * marketing pages without a hero image still produce a richer\n * social-card (Phase 6 audit fix M-A). When neither is present,\n * the Twitter card kind degrades to the smaller `summary` and the\n * `og:image` / `twitter:image` lines are omitted entirely.\n */\n const ogImageUrl = manifest.metaLabelKeys.ogImageUrl ?? rootConfig.seo.defaultOgImageUrl;\n const titleAttr = escapeHtmlAttribute(title);\n const descriptionAttr = escapeHtmlAttribute(description);\n const titleText = escapeHtmlText(title);\n\n /**\n * Absolute URL builder. `prefixDefaultLocale: false` means the\n * default locale lives at the locale-neutral path; every other\n * locale sits under `/<locale>/<routePath>`. `x-default` always\n * resolves to the default-locale path.\n *\n * Every URL emitted here is FULLY-QUALIFIED (composed against\n * `websiteConfig.publicOrigin`) — Google's hreflang spec requires\n * absolute URLs, and `<link rel=\"canonical\">` relies on absolute\n * URLs to survive cross-host fetches (preview deployments, CDN\n * edges). See `WildoMarketingWebsiteConfigSchema.publicOrigin`\n * JSDoc for the full rationale (incl. trailing-slash rejection).\n */\n const routePath = manifest.routePath as unknown as string;\n const siteOrigin = websiteConfig.publicOrigin;\n const defaultLocale = websiteConfig.defaultLocale;\n const currentLocaleStr = String(locale);\n\n /**\n * Compose the absolute URL for `(locale, routePath)` by delegating\n * the locale-prefix decision to the shared `buildLocalizedRoutePath`\n * helper — same routing rule that powers `WebsiteInternalLink`. This\n * guarantees that hreflang/canonical URLs and the chrome's internal\n * links can never disagree about how a locale is prefixed.\n */\n function buildAbsoluteUrl(forLocale: AvailableLanguage): string {\n const path = buildLocalizedRoutePath({\n locale: forLocale,\n defaultLocale,\n routePath,\n });\n return `${siteOrigin}${path}`;\n }\n\n const canonicalUrl = buildAbsoluteUrl(locale);\n\n const hreflangLines: string[] = [];\n for (const entry of websiteConfig.locales) {\n const localeStr = String(entry.locale);\n hreflangLines.push(\n `<link rel=\"alternate\" hreflang=\"${escapeHtmlAttribute(localeStr)}\" href=\"${escapeHtmlAttribute(buildAbsoluteUrl(entry.locale))}\" />`,\n );\n }\n hreflangLines.push(\n `<link rel=\"alternate\" hreflang=\"x-default\" href=\"${escapeHtmlAttribute(buildAbsoluteUrl(defaultLocale))}\" />`,\n );\n\n /**\n * `og:image` + `twitter:image` are emitted only when an image URL\n * is available (page-level override OR site-level fallback).\n * Twitter card type also flips between `summary` (no image) and\n * `summary_large_image` (image present).\n */\n const ogImageLines: string[] = [];\n if (ogImageUrl !== undefined) {\n const ogImageAttr = escapeHtmlAttribute(ogImageUrl);\n ogImageLines.push(\n `<meta property=\"og:image\" content=\"${ogImageAttr}\" />`,\n `<meta name=\"twitter:image\" content=\"${ogImageAttr}\" />`,\n );\n }\n const twitterCardKind = ogImageUrl !== undefined ? 'summary_large_image' : 'summary';\n\n /**\n * `og:locale:alternate` — one per non-current declared locale. Open\n * Graph spec: the `og:locale` field carries the page's primary\n * locale; alternates declare the other locales the page is\n * available in. Emitted in declaration order.\n *\n * Locale format: we emit the bare language code (`en`, `fr`) per\n * the AvailableLanguage atom shape. Open Graph technically\n * recommends `language_TERRITORY` (e.g. `en_US`), but Facebook /\n * LinkedIn / Slack accept the bare language code in practice. The\n * future fix when the platform grows territory-tagged locales is to\n * map at this site (no consumer change required).\n */\n const ogLocaleAlternateLines: string[] = [];\n for (const entry of websiteConfig.locales) {\n const localeStr = String(entry.locale);\n if (localeStr === currentLocaleStr) continue;\n ogLocaleAlternateLines.push(\n `<meta property=\"og:locale:alternate\" content=\"${escapeHtmlAttribute(localeStr)}\" />`,\n );\n }\n\n const siteNameAttr = escapeHtmlAttribute(rootConfig.seo.siteName);\n const canonicalAttr = escapeHtmlAttribute(canonicalUrl);\n const ogLocaleAttr = escapeHtmlAttribute(currentLocaleStr);\n\n /**\n * SITE-WIDE `noindex` (#494) — the directive that actually keeps a site out of search.\n *\n * `robots.txt` `Disallow` governs CRAWLING, not INDEXING: a disallowed URL can still be listed, as a\n * bare URL with no snippet, if anything links to it. Only this directive de-indexes. Until 2026-09-08\n * a site declared `noindex` emitted the `Disallow` and NOT this, so it was closed to well-behaved\n * crawlers and still indexable — the weaker half of the pair.\n *\n * The two are also mutually defeating, which is why `renderRobotsTxt` no longer emits `Disallow: /`\n * for this policy: a crawler barred from fetching the page never reads the directive telling it not\n * to index. To stay out of search you must let the crawler IN and tell it no.\n *\n * `noindex` only — not `noindex, nofollow`. `nofollow` additionally suppresses link discovery, which\n * is a broader claim than \"keep this out of the index\" and is not what the policy declares.\n */\n const robotsDirectives: string[] = [];\n if (rootConfig.seo.robots?.policy === 'noindex') {\n robotsDirectives.push('noindex');\n }\n /**\n * AI-training opt-out (D-AG3) — emitted ONLY when the page manifest opts in via\n * `seo.noaiOptOut: true`. Default is OPT-IN to AI training (the marketing-site default). This\n * emission pairs with the sibling `_headers` entry produced by `renderWebsitePageNoaiHeaders()`\n * for crawlers that don't honor in-document meta.\n *\n * `noai` (block AI training on text) and `noimageai` (block AI training on images) are the two\n * directives the major AI crawlers (GPTBot, ClaudeBot, Google-Extended, etc.) recognise.\n */\n if (manifest.seo?.noaiOptOut === true) {\n robotsDirectives.push('noai', 'noimageai');\n }\n /*\n * ONE `<meta name=\"robots\">`, carrying every directive comma-separated — the canonical shape. Two\n * separate robots meta tags are legal and are read inconsistently; merging avoids the question.\n */\n const noaiMetaLines: string[] = robotsDirectives.length > 0\n ? [`<meta name=\"robots\" content=\"${robotsDirectives.join(', ')}\" />`]\n : [];\n\n return [\n `<title>${titleText}</title>`,\n `<meta name=\"description\" content=\"${descriptionAttr}\" />`,\n `<link rel=\"canonical\" href=\"${canonicalAttr}\" />`,\n `<meta property=\"og:title\" content=\"${titleAttr}\" />`,\n `<meta property=\"og:description\" content=\"${descriptionAttr}\" />`,\n `<meta property=\"og:type\" content=\"website\" />`,\n `<meta property=\"og:url\" content=\"${canonicalAttr}\" />`,\n `<meta property=\"og:site_name\" content=\"${siteNameAttr}\" />`,\n `<meta property=\"og:locale\" content=\"${ogLocaleAttr}\" />`,\n ...ogLocaleAlternateLines,\n ...ogImageLines,\n `<meta name=\"twitter:card\" content=\"${twitterCardKind}\" />`,\n `<meta name=\"twitter:title\" content=\"${titleAttr}\" />`,\n `<meta name=\"twitter:description\" content=\"${descriptionAttr}\" />`,\n ...noaiMetaLines,\n ...hreflangLines,\n ].join('\\n');\n}\n\n/**\n * Flatten `WebsiteDesignTokens` to a `:root { … }` block of CSS custom\n * properties. Naming convention: `--website-<group>-<token>` for nested\n * groups (`colors.primary` → `--website-colors-primary`); top-level\n * string-valued tokens (none today, future-proofed) emit as\n * `--website-<token>`.\n *\n * **Why scoped to `:root`** rather than `<WebsitePageLayout>`'s root\n * div: marketing pages frequently embed third-party widgets (live-chat,\n * analytics overlays) that paint outside the React tree. Scoping\n * tokens to `:root` makes them visible to those widgets too. The\n * design-token surface is small enough that the cost of a broader\n * scope (token-name collisions with consumer-authored `:root` rules)\n * is bounded by the `--website-` prefix.\n */\nexport function renderWebsiteDesignTokensCss(tokens: WebsiteDesignTokens): string {\n const lines: string[] = [':root {'];\n for (const [groupName, groupValue] of Object.entries(tokens) as [string, unknown][]) {\n if (groupValue !== null && typeof groupValue === 'object' && !Array.isArray(groupValue)) {\n for (const [tokenName, tokenValue] of Object.entries(groupValue) as [string, unknown][]) {\n if (typeof tokenValue === 'string') {\n lines.push(` --website-${groupName}-${tokenName}: ${tokenValue};`);\n }\n }\n } else if (typeof groupValue === 'string') {\n lines.push(` --website-${groupName}: ${groupValue};`);\n }\n }\n lines.push('}');\n return lines.join('\\n');\n}\n\n/**\n * @wildo_source:part:end saas.website.astro.head-renderer\n *\n * HTML escape helpers moved to `./internal/html-escape.ts` (Phase 7)\n * so the new `blog-post-head.renderer.ts` sibling can share the same\n * escape contract instead of redefining it. See that module's preamble\n * for the framework-mindset rationale (one escape contract per engine,\n * not one per renderer).\n */\n"]}
1
+ {"version":3,"file":"website-page-head.renderer.js","sourceRoot":"","sources":["../../../../src/astro/website-page-head.renderer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACjF,OAAO,EAAE,6BAA6B,EAA4B,MAAM,+DAA+D,CAAC;AAIxI,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAkE7E,MAAM,UAAU,yBAAyB,CAAC,IAAsC;IAC9E,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC1D,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,QAA6B,CAAC;IACtE,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa,CAAC,cAAmC,CAAC;IAClF,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IAClC,MAAM,WAAW,GAAG,SAAS,CAAC,cAAc,CAAC,CAAC;IAE9C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,sDAAsD,MAAM,eAAe;YACzE,uBAAuB,QAAQ,gBAAgB,QAAQ,CAAC,GAAwB,MAAM;YACtF,0EAA0E,CAC7E,CAAC;IACJ,CAAC;IACD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,sDAAsD,MAAM,eAAe;YACzE,6BAA6B,cAAc,gBAAgB,QAAQ,CAAC,GAAwB,MAAM;YAClG,0EAA0E,CAC7E,CAAC;IACJ,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,UAAU,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,IAAI,UAAU,CAAC,GAAG,CAAC,iBAAiB,CAAC;IACzF,MAAM,SAAS,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC7C,MAAM,eAAe,GAAG,mBAAmB,CAAC,WAAW,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAExC;;;;;;;;;;;;OAYG;IACH,MAAM,SAAS,GAAG,QAAQ,CAAC,SAA8B,CAAC;IAC1D,MAAM,UAAU,GAAG,aAAa,CAAC,YAAY,CAAC;IAC9C,MAAM,aAAa,GAAG,aAAa,CAAC,aAAa,CAAC;IAClD,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAExC;;;;;;OAMG;IACH,SAAS,gBAAgB,CAAC,SAA4B;QACpD,MAAM,IAAI,GAAG,uBAAuB,CAAC;YACnC,MAAM,EAAE,SAAS;YACjB,aAAa;YACb,SAAS;SACV,CAAC,CAAC;QACH,OAAO,GAAG,UAAU,GAAG,IAAI,EAAE,CAAC;IAChC,CAAC;IAED,MAAM,YAAY,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAE9C,MAAM,aAAa,GAAa,EAAE,CAAC;IACnC,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,OAAO,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACvC,aAAa,CAAC,IAAI,CAChB,mCAAmC,mBAAmB,CAAC,SAAS,CAAC,WAAW,mBAAmB,CAAC,gBAAgB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,CACtI,CAAC;IACJ,CAAC;IACD,aAAa,CAAC,IAAI,CAChB,oDAAoD,mBAAmB,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAAC,MAAM,CAC/G,CAAC;IAEF;;;;;OAKG;IACH,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,mBAAmB,CAAC,UAAU,CAAC,CAAC;QACpD,YAAY,CAAC,IAAI,CACf,sCAAsC,WAAW,MAAM,EACvD,uCAAuC,WAAW,MAAM,CACzD,CAAC;IACJ,CAAC;IACD,MAAM,eAAe,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,SAAS,CAAC;IAErF;;;;;;;;;;;;OAYG;IACH,MAAM,sBAAsB,GAAa,EAAE,CAAC;IAC5C,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,OAAO,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,SAAS,KAAK,gBAAgB;YAAE,SAAS;QAC7C,sBAAsB,CAAC,IAAI,CACzB,iDAAiD,mBAAmB,CAAC,SAAS,CAAC,MAAM,CACtF,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAAG,mBAAmB,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAClE,MAAM,aAAa,GAAG,mBAAmB,CAAC,YAAY,CAAC,CAAC;IACxD,MAAM,YAAY,GAAG,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;IAE3D;;;;;;;;;;;;;;OAcG;IACH,MAAM,gBAAgB,GAAa,EAAE,CAAC;IACtC,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;QAChD,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACnC,CAAC;IACD;;;;;;;;OAQG;IACH,IAAI,QAAQ,CAAC,GAAG,EAAE,UAAU,KAAK,IAAI,EAAE,CAAC;QACtC,gBAAgB,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC7C,CAAC;IACD;;;OAGG;IACH,MAAM,aAAa,GAAa,gBAAgB,CAAC,MAAM,GAAG,CAAC;QACzD,CAAC,CAAC,CAAC,gCAAgC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QACrE,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO;QACL,UAAU,SAAS,UAAU;QAC7B,qCAAqC,eAAe,MAAM;QAC1D,+BAA+B,aAAa,MAAM;QAClD,sCAAsC,SAAS,MAAM;QACrD,4CAA4C,eAAe,MAAM;QACjE,+CAA+C;QAC/C,oCAAoC,aAAa,MAAM;QACvD,0CAA0C,YAAY,MAAM;QAC5D,uCAAuC,YAAY,MAAM;QACzD,GAAG,sBAAsB;QACzB,GAAG,YAAY;QACf,sCAAsC,eAAe,MAAM;QAC3D,uCAAuC,SAAS,MAAM;QACtD,6CAA6C,eAAe,MAAM;QAClE,GAAG,aAAa;QAChB,GAAG,aAAa;KACjB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAA2B;IACtE,MAAM,KAAK,GAAa,CAAC,SAAS,CAAC,CAAC;IACpC,KAAK,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAwB,EAAE,CAAC;QACpF,IAAI,UAAU,KAAK,IAAI,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;YACxF,KAAK,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAwB,EAAE,CAAC;gBACxF,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;oBACnC,KAAK,CAAC,IAAI,CAAC,KAAK,6BAA6B,CAAC,SAAS,EAAE,SAAS,CAAC,KAAK,UAAU,GAAG,CAAC,CAAC;gBACzF,CAAC;YACH,CAAC;QACH,CAAC;aAAM,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,eAAe,SAAS,KAAK,UAAU,GAAG,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;GAQG","sourcesContent":["import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';\nimport { websiteDesignTokenCssProperty, type WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\nimport { escapeHtmlAttribute, escapeHtmlText } from './internal/html-escape';\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.head-renderer facet:layer:astro facet:family:website\n *\n * Head-block renderers — pure string builders the consumer's per-page\n * `.astro` file injects via `<Fragment set:html={...}/>` /\n * `<style set:html={...}></style>`. Two functions:\n *\n * - `renderWebsitePageHeadHtml(opts)` — emits the complete SEO\n * `<head>` block: `<title>`, meta description, full Open Graph\n * card (`og:title` / `og:description` / `og:type` / `og:url` /\n * `og:site_name` / `og:locale` / `og:locale:alternate` / optional\n * `og:image`), Twitter card mirrors, `<link rel=\"canonical\">`,\n * and one `<link rel=\"alternate\" hreflang>` per declared locale\n * (plus an `x-default` for Google's i18n discoverability). Every\n * URL is ABSOLUTE (composed from `websiteConfig.publicOrigin` +\n * the page's `routePath`) — relative hreflang/canonical\n * annotations are not honored consistently across crawlers\n * (Phase 6 audit findings H-B / H-C / M-A).\n * - `renderWebsiteDesignTokensCss(tokens)` — flattens the design-token\n * bundle into `--website-*` custom properties scoped to `:root`.\n * Server-static — zero island JS.\n *\n * **Why pure string builders** (rather than React components or Astro\n * components):\n *\n * 1. **Avoids the `.astro` distribution problem.** A `.astro` shell\n * cannot be re-exported from a TS subpath barrel and `tsc` does\n * not copy `.astro` files to `dist/`. Pure TS sidesteps both.\n * 2. **Avoids the React-island boundary.** Rendering head content\n * via React would require either (a) a server-only React subtree\n * (extra moving part) or (b) hydrating the `<head>` on the client\n * (waste of bundle size for static-build content).\n * 3. **Vitest-friendly.** Pure functions returning strings are\n * trivially asserted against without an Astro test harness.\n *\n * **HTML escaping**: every consumer-supplied string (title,\n * description, OG image URL, hreflang values) is escaped with\n * `escapeHtmlAttribute` / `escapeHtmlText` before interpolation into\n * the emitted markup. Marketing copy frequently contains characters\n * (quotes, ampersands, the rare `<`) that would break the surrounding\n * tag if interpolated raw — escaping is non-negotiable for a public\n * surface.\n *\n * **Failure modes** — `renderWebsitePageHeadHtml` throws when:\n * - `manifest.metaLabelKeys.titleKey` is absent from the pack\n * - `manifest.metaLabelKeys.descriptionKey` is absent from the pack\n * Both throw with the locale + key + actionable next step (`Run\n * \\`wildo website audit\\` or add the entry to <pack-path>.json`).\n */\n\nexport interface RenderWebsitePageHeadHtmlOptions {\n manifest: WebsitePageManifest;\n /**\n * Composed `WebsiteSiteContext` — the renderer reads\n * `websiteConfig.{defaultLocale, locales, publicOrigin}` for the\n * URL/locale surface and `rootConfig.seo.{siteName, defaultOgImageUrl}`\n * for the SEO branding fallback.\n */\n siteContext: WebsiteSiteContext;\n locale: AvailableLanguage;\n labelPack: LabelPack;\n}\n\nexport function renderWebsitePageHeadHtml(opts: RenderWebsitePageHeadHtmlOptions): string {\n const { manifest, siteContext, locale, labelPack } = opts;\n const { rootConfig, websiteConfig } = siteContext;\n\n const titleKey = manifest.metaLabelKeys.titleKey as unknown as string;\n const descriptionKey = manifest.metaLabelKeys.descriptionKey as unknown as string;\n const title = labelPack[titleKey];\n const description = labelPack[descriptionKey];\n\n if (title === undefined) {\n throw new Error(\n `[renderWebsitePageHeadHtml] Label pack for locale \"${locale}\" is missing ` +\n `the page title key \"${titleKey}\" (page ref=\"${manifest.ref as unknown as string}\"). ` +\n `Run \\`wildo website audit\\` or add the entry to your <locale>.json pack.`,\n );\n }\n if (description === undefined) {\n throw new Error(\n `[renderWebsitePageHeadHtml] Label pack for locale \"${locale}\" is missing ` +\n `the page description key \"${descriptionKey}\" (page ref=\"${manifest.ref as unknown as string}\"). ` +\n `Run \\`wildo website audit\\` or add the entry to your <locale>.json pack.`,\n );\n }\n\n /**\n * OG image precedence: per-page `manifest.metaLabelKeys.ogImageUrl`\n * wins; falls back to the site-level `seo.defaultOgImageUrl` so\n * marketing pages without a hero image still produce a richer\n * social-card (Phase 6 audit fix M-A). When neither is present,\n * the Twitter card kind degrades to the smaller `summary` and the\n * `og:image` / `twitter:image` lines are omitted entirely.\n */\n const ogImageUrl = manifest.metaLabelKeys.ogImageUrl ?? rootConfig.seo.defaultOgImageUrl;\n const titleAttr = escapeHtmlAttribute(title);\n const descriptionAttr = escapeHtmlAttribute(description);\n const titleText = escapeHtmlText(title);\n\n /**\n * Absolute URL builder. `prefixDefaultLocale: false` means the\n * default locale lives at the locale-neutral path; every other\n * locale sits under `/<locale>/<routePath>`. `x-default` always\n * resolves to the default-locale path.\n *\n * Every URL emitted here is FULLY-QUALIFIED (composed against\n * `websiteConfig.publicOrigin`) — Google's hreflang spec requires\n * absolute URLs, and `<link rel=\"canonical\">` relies on absolute\n * URLs to survive cross-host fetches (preview deployments, CDN\n * edges). See `WildoMarketingWebsiteConfigSchema.publicOrigin`\n * JSDoc for the full rationale (incl. trailing-slash rejection).\n */\n const routePath = manifest.routePath as unknown as string;\n const siteOrigin = websiteConfig.publicOrigin;\n const defaultLocale = websiteConfig.defaultLocale;\n const currentLocaleStr = String(locale);\n\n /**\n * Compose the absolute URL for `(locale, routePath)` by delegating\n * the locale-prefix decision to the shared `buildLocalizedRoutePath`\n * helper — same routing rule that powers `WebsiteInternalLink`. This\n * guarantees that hreflang/canonical URLs and the chrome's internal\n * links can never disagree about how a locale is prefixed.\n */\n function buildAbsoluteUrl(forLocale: AvailableLanguage): string {\n const path = buildLocalizedRoutePath({\n locale: forLocale,\n defaultLocale,\n routePath,\n });\n return `${siteOrigin}${path}`;\n }\n\n const canonicalUrl = buildAbsoluteUrl(locale);\n\n const hreflangLines: string[] = [];\n for (const entry of websiteConfig.locales) {\n const localeStr = String(entry.locale);\n hreflangLines.push(\n `<link rel=\"alternate\" hreflang=\"${escapeHtmlAttribute(localeStr)}\" href=\"${escapeHtmlAttribute(buildAbsoluteUrl(entry.locale))}\" />`,\n );\n }\n hreflangLines.push(\n `<link rel=\"alternate\" hreflang=\"x-default\" href=\"${escapeHtmlAttribute(buildAbsoluteUrl(defaultLocale))}\" />`,\n );\n\n /**\n * `og:image` + `twitter:image` are emitted only when an image URL\n * is available (page-level override OR site-level fallback).\n * Twitter card type also flips between `summary` (no image) and\n * `summary_large_image` (image present).\n */\n const ogImageLines: string[] = [];\n if (ogImageUrl !== undefined) {\n const ogImageAttr = escapeHtmlAttribute(ogImageUrl);\n ogImageLines.push(\n `<meta property=\"og:image\" content=\"${ogImageAttr}\" />`,\n `<meta name=\"twitter:image\" content=\"${ogImageAttr}\" />`,\n );\n }\n const twitterCardKind = ogImageUrl !== undefined ? 'summary_large_image' : 'summary';\n\n /**\n * `og:locale:alternate` — one per non-current declared locale. Open\n * Graph spec: the `og:locale` field carries the page's primary\n * locale; alternates declare the other locales the page is\n * available in. Emitted in declaration order.\n *\n * Locale format: we emit the bare language code (`en`, `fr`) per\n * the AvailableLanguage atom shape. Open Graph technically\n * recommends `language_TERRITORY` (e.g. `en_US`), but Facebook /\n * LinkedIn / Slack accept the bare language code in practice. The\n * future fix when the platform grows territory-tagged locales is to\n * map at this site (no consumer change required).\n */\n const ogLocaleAlternateLines: string[] = [];\n for (const entry of websiteConfig.locales) {\n const localeStr = String(entry.locale);\n if (localeStr === currentLocaleStr) continue;\n ogLocaleAlternateLines.push(\n `<meta property=\"og:locale:alternate\" content=\"${escapeHtmlAttribute(localeStr)}\" />`,\n );\n }\n\n const siteNameAttr = escapeHtmlAttribute(rootConfig.seo.siteName);\n const canonicalAttr = escapeHtmlAttribute(canonicalUrl);\n const ogLocaleAttr = escapeHtmlAttribute(currentLocaleStr);\n\n /**\n * SITE-WIDE `noindex` (#494) — the directive that actually keeps a site out of search.\n *\n * `robots.txt` `Disallow` governs CRAWLING, not INDEXING: a disallowed URL can still be listed, as a\n * bare URL with no snippet, if anything links to it. Only this directive de-indexes. Until 2026-09-08\n * a site declared `noindex` emitted the `Disallow` and NOT this, so it was closed to well-behaved\n * crawlers and still indexable — the weaker half of the pair.\n *\n * The two are also mutually defeating, which is why `renderRobotsTxt` no longer emits `Disallow: /`\n * for this policy: a crawler barred from fetching the page never reads the directive telling it not\n * to index. To stay out of search you must let the crawler IN and tell it no.\n *\n * `noindex` only — not `noindex, nofollow`. `nofollow` additionally suppresses link discovery, which\n * is a broader claim than \"keep this out of the index\" and is not what the policy declares.\n */\n const robotsDirectives: string[] = [];\n if (rootConfig.seo.robots?.policy === 'noindex') {\n robotsDirectives.push('noindex');\n }\n /**\n * AI-training opt-out (D-AG3) — emitted ONLY when the page manifest opts in via\n * `seo.noaiOptOut: true`. Default is OPT-IN to AI training (the marketing-site default). This\n * emission pairs with the sibling `_headers` entry produced by `renderWebsitePageNoaiHeaders()`\n * for crawlers that don't honor in-document meta.\n *\n * `noai` (block AI training on text) and `noimageai` (block AI training on images) are the two\n * directives the major AI crawlers (GPTBot, ClaudeBot, Google-Extended, etc.) recognise.\n */\n if (manifest.seo?.noaiOptOut === true) {\n robotsDirectives.push('noai', 'noimageai');\n }\n /*\n * ONE `<meta name=\"robots\">`, carrying every directive comma-separated — the canonical shape. Two\n * separate robots meta tags are legal and are read inconsistently; merging avoids the question.\n */\n const noaiMetaLines: string[] = robotsDirectives.length > 0\n ? [`<meta name=\"robots\" content=\"${robotsDirectives.join(', ')}\" />`]\n : [];\n\n return [\n `<title>${titleText}</title>`,\n `<meta name=\"description\" content=\"${descriptionAttr}\" />`,\n `<link rel=\"canonical\" href=\"${canonicalAttr}\" />`,\n `<meta property=\"og:title\" content=\"${titleAttr}\" />`,\n `<meta property=\"og:description\" content=\"${descriptionAttr}\" />`,\n `<meta property=\"og:type\" content=\"website\" />`,\n `<meta property=\"og:url\" content=\"${canonicalAttr}\" />`,\n `<meta property=\"og:site_name\" content=\"${siteNameAttr}\" />`,\n `<meta property=\"og:locale\" content=\"${ogLocaleAttr}\" />`,\n ...ogLocaleAlternateLines,\n ...ogImageLines,\n `<meta name=\"twitter:card\" content=\"${twitterCardKind}\" />`,\n `<meta name=\"twitter:title\" content=\"${titleAttr}\" />`,\n `<meta name=\"twitter:description\" content=\"${descriptionAttr}\" />`,\n ...noaiMetaLines,\n ...hreflangLines,\n ].join('\\n');\n}\n\n/**\n * Flatten `WebsiteDesignTokens` to a `:root { … }` block of CSS custom\n * properties. Naming convention: `--website-<group>-<token>` for nested\n * groups (`colors.primary` → `--website-colors-primary`); top-level\n * string-valued tokens (none today, future-proofed) emit as\n * `--website-<token>`.\n *\n * **Why scoped to `:root`** rather than `<WebsitePageLayout>`'s root\n * div: marketing pages frequently embed third-party widgets (live-chat,\n * analytics overlays) that paint outside the React tree. Scoping\n * tokens to `:root` makes them visible to those widgets too. The\n * design-token surface is small enough that the cost of a broader\n * scope (token-name collisions with consumer-authored `:root` rules)\n * is bounded by the `--website-` prefix.\n */\nexport function renderWebsiteDesignTokensCss(tokens: WebsiteDesignTokens): string {\n const lines: string[] = [':root {'];\n for (const [groupName, groupValue] of Object.entries(tokens) as [string, unknown][]) {\n if (groupValue !== null && typeof groupValue === 'object' && !Array.isArray(groupValue)) {\n for (const [tokenName, tokenValue] of Object.entries(groupValue) as [string, unknown][]) {\n if (typeof tokenValue === 'string') {\n lines.push(` ${websiteDesignTokenCssProperty(groupName, tokenName)}: ${tokenValue};`);\n }\n }\n } else if (typeof groupValue === 'string') {\n lines.push(` --website-${groupName}: ${groupValue};`);\n }\n }\n lines.push('}');\n return lines.join('\\n');\n}\n\n/**\n * @wildo_source:part:end saas.website.astro.head-renderer\n *\n * HTML escape helpers moved to `./internal/html-escape.ts` (Phase 7)\n * so the new `blog-post-head.renderer.ts` sibling can share the same\n * escape contract instead of redefining it. See that module's preamble\n * for the framework-mindset rationale (one escape contract per engine,\n * not one per renderer).\n */\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"website-page-runtime.helper.d.ts","sourceRoot":"","sources":["../../../src/astro/website-page-runtime.helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAGzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAI5E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,gFAAgF;IAChF,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,yEAAyE;IACzE,WAAW,EAAE,kBAAkB,CAAC;IAChC,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;;OAQG;IACH,aAAa,EAAE,iBAAiB,CAAC;IACjC,qEAAqE;IACrE,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,6BAA6B,GAClC,kBAAkB,CAqCpB;AA+BD,+DAA+D"}
1
+ {"version":3,"file":"website-page-runtime.helper.d.ts","sourceRoot":"","sources":["../../../../src/astro/website-page-runtime.helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAGzE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2DAA2D,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4CAA4C,CAAC;AAI5E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,MAAM,WAAW,6BAA6B;IAC5C,gFAAgF;IAChF,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,yEAAyE;IACzE,WAAW,EAAE,kBAAkB,CAAC;IAChC,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;;OAQG;IACH,aAAa,EAAE,iBAAiB,CAAC;IACjC,qEAAqE;IACrE,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,6BAA6B,GAClC,kBAAkB,CAqCpB;AA+BD,+DAA+D"}
@@ -9,7 +9,8 @@ import type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtim
9
9
  * `WebsiteRootConfig` (page registry, design tokens, navigation chrome,
10
10
  * site-level SEO branding) with the per-service `WildoMarketingWebsiteConfig`
11
11
  * loaded from `wildo.website.config.ts` (locales, default locale,
12
- * public origin, future `appOrigin` cross-origin handoff target).
12
+ * public origin, and the `appOrigin` / `apiOrigin` the site reaches the
13
+ * product through — settled for THIS build by `loadWebsiteSiteContext`).
13
14
  *
14
15
  * # Why a dedicated aggregator type (vs. threading both raw configs)
15
16
  *
@@ -82,7 +83,9 @@ export interface WebsiteSiteContext {
82
83
  * The per-service `WildoMarketingWebsiteConfig` loaded from
83
84
  * `wildo.website.config.ts`. Single source of truth for the
84
85
  * marketing-site ROUTING + ORIGIN surface (`defaultLocale`,
85
- * `locales`, `publicOrigin`, future `appOrigin`).
86
+ * `locales`, `publicOrigin`, `appOrigin`, `apiOrigin`). The two origins
87
+ * are the values for THIS build (override, local dev, authored — see
88
+ * `resolveWebsiteBuildOrigins`), which is why every page reads them here.
86
89
  */
87
90
  readonly websiteConfig: WildoMarketingWebsiteConfig;
88
91
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"website-site-context.d.ts","sourceRoot":"","sources":["../../../src/astro/website-site-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAC;AACjG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAEnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC;IAEvC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,2BAA2B,CAAC;IACpD;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,CAMpB;AACD,6DAA6D"}
1
+ {"version":3,"file":"website-site-context.d.ts","sourceRoot":"","sources":["../../../../src/astro/website-site-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,wCAAwC,CAAC;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAC;AACjG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAEnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC;IAEvC;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,EAAE,2BAA2B,CAAC;IACpD;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,sBAAsB,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,CAMpB;AACD,6DAA6D"}
@@ -1 +1 @@
1
- {"version":3,"file":"website-site-context.js","sourceRoot":"","sources":["../../../../src/astro/website-site-context.ts"],"names":[],"mappings":"AAkGA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,yBAAyB,CACvC,KAAyB;IAEzB,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;KAC3C,CAAC,CAAC;AACL,CAAC;AACD,6DAA6D","sourcesContent":["import type { WildoMarketingWebsiteConfig } from '../config/wildo-website-config.schemas';\nimport type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\n\n/**\n * @wildo_source:part:start saas.website.astro.site-context facet:layer:astro facet:family:website\n *\n * `WebsiteSiteContext` — the aggregated site-level inputs every Astro\n * build-time engine helper consumes. Composes the consumer-authored\n * `WebsiteRootConfig` (page registry, design tokens, navigation chrome,\n * site-level SEO branding) with the per-service `WildoMarketingWebsiteConfig`\n * loaded from `wildo.website.config.ts` (locales, default locale,\n * public origin, future `appOrigin` cross-origin handoff target).\n *\n * # Why a dedicated aggregator type (vs. threading both raw configs)\n *\n * The seam-collapse (`saas-website-seam-collapse.md`, Option B) made\n * `WildoMarketingWebsiteConfig` the SINGLE source of truth for the\n * marketing-site routing surface (`defaultLocale`, `locales`,\n * `publicOrigin`). Every engine helper that previously read those\n * fields from `WebsiteRootConfig` now reads them from\n * `WildoMarketingWebsiteConfig` — and four out of five of those helpers\n * also still read OTHER fields from `WebsiteRootConfig` (page\n * manifests, chrome label keys, design tokens, `seo.siteName`,\n * `seo.defaultOgImageUrl`, `skipToMainContentLabelKey`). Threading\n * both raw configs through every helper signature would:\n *\n * 1. **Bloat call sites.** Every consumer `.astro` file would have\n * to import both `WEBSITE_ROOT_CONFIG` and the loaded\n * `WILDO_WEBSITE_CONFIG`, then forward both to every helper.\n * 2. **Spread the consumer-side composition responsibility.**\n * Each `.astro` file would need to know which subset of the\n * configs each helper consumes — a refactor hazard the next time\n * we add a field that the head renderer reads from the website\n * config but the runtime loader does not.\n * 3. **Defeat top-level `await` ergonomics.** The website config\n * load is async (`loadWebsiteConfig` does jiti-backed file I/O);\n * forcing every `.astro` file to `await` it would either spread\n * the await across every page or force every page to receive the\n * already-loaded config from a separate consumer module —\n * duplicating the composition logic this aggregator centralizes.\n *\n * One aggregator, composed once at the consumer's\n * `<service-root>/src/website-site-context.ts`, is the minimum-blast\n * shape: every engine helper takes `siteContext: WebsiteSiteContext`,\n * every `.astro` file imports the singleton `WEBSITE_SITE_CONTEXT`\n * once, and the next field move (consumer-config-only field, or\n * root-config-only field) lands in this type's shape with no\n * helper-signature ripple.\n *\n * # Why frozen (defensive)\n *\n * The composed value is shared across every page render in the build\n * graph. A misbehaving Astro plugin (or a future consumer-side audit\n * subprocess) that mutated either nested config in place would\n * silently corrupt every subsequent page. `Object.freeze` is a cheap\n * top-level guard — the inner configs are themselves already produced\n * by Zod parsers (which return fresh objects), so mutating their\n * fields would require a deliberate `as any` cast at the consumer's\n * call site rather than an accidental in-place edit.\n *\n * # When this type WOULD grow\n *\n * Future inputs that are also \"site-wide\" but neither belong to\n * `WebsiteRootConfig` (chrome-rendered) nor to\n * `WildoMarketingWebsiteConfig` (per-service routing/origins) — e.g.\n * a build-environment block (`production` / `preview` / `dev`), or\n * a feature-flag bag — would land here as a third sibling field\n * rather than being plumbed through every helper signature\n * individually. The aggregator is the right place for that\n * orchestration concern; per-call ergonomics ride along for free.\n */\nexport interface WebsiteSiteContext {\n /**\n * The consumer's `WebsiteRootConfig` — page registry, design tokens,\n * navigation chrome, site-level SEO branding (`siteName`,\n * `defaultOgImageUrl?`), `chromeLabelKeys`,\n * `skipToMainContentLabelKey`. Single source of truth for chrome\n * + per-page meta declarations.\n */\n readonly rootConfig: WebsiteRootConfig;\n\n /**\n * The per-service `WildoMarketingWebsiteConfig` loaded from\n * `wildo.website.config.ts`. Single source of truth for the\n * marketing-site ROUTING + ORIGIN surface (`defaultLocale`,\n * `locales`, `publicOrigin`, future `appOrigin`).\n */\n readonly websiteConfig: WildoMarketingWebsiteConfig;\n /**\n * Optional website-facing public provider block materialized from the\n * app's provider graph for the concrete `STATIC_WEBSITE` service.\n * Build-time only: the consumer loads it once and forwards it into the\n * React island as JSON-serializable data.\n */\n readonly frontendProviders?: FrontendProvidersBlock;\n}\n\n/**\n * Compose a `WebsiteSiteContext` from the two source configs. Pure +\n * synchronous — the I/O happens upstream in `loadWebsiteConfig` and\n * in the consumer's `WEBSITE_ROOT_CONFIG` module evaluation.\n *\n * **Defensive freezing**: the returned object is `Object.freeze`-d\n * top-level so the singleton handed to every helper cannot have its\n * `rootConfig` / `websiteConfig` references swapped in place by a\n * misbehaving plugin. The nested configs are NOT deep-frozen — Zod\n * already produces fresh objects per parse, and a deep freeze would\n * cost a recursive walk per build for protection that no realistic\n * plugin would breach.\n *\n * @example\n * ```ts\n * // examples/<app>/website/src/website-site-context.ts\n * import { composeWebsiteSiteContext } from '@wildo-ai/saas-website/astro';\n * import { loadWebsiteConfig } from '@wildo-ai/saas-website/config-loader';\n *\n * import { WEBSITE_ROOT_CONFIG } from './website-root.config';\n *\n * const websiteConfig = await loadWebsiteConfig({ saasRoot: '...' });\n * export const WEBSITE_SITE_CONTEXT = composeWebsiteSiteContext({\n * rootConfig: WEBSITE_ROOT_CONFIG,\n * websiteConfig,\n * });\n * ```\n */\nexport function composeWebsiteSiteContext(\n parts: WebsiteSiteContext,\n): WebsiteSiteContext {\n return Object.freeze({\n rootConfig: parts.rootConfig,\n websiteConfig: parts.websiteConfig,\n frontendProviders: parts.frontendProviders,\n });\n}\n/** @wildo_source:part:end saas.website.astro.site-context */\n"]}
1
+ {"version":3,"file":"website-site-context.js","sourceRoot":"","sources":["../../../../src/astro/website-site-context.ts"],"names":[],"mappings":"AAqGA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,yBAAyB,CACvC,KAAyB;IAEzB,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;KAC3C,CAAC,CAAC;AACL,CAAC;AACD,6DAA6D","sourcesContent":["import type { WildoMarketingWebsiteConfig } from '../config/wildo-website-config.schemas';\nimport type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';\nimport type { FrontendProvidersBlock } from '@wildo-ai/saas-models/public-runtime';\n\n/**\n * @wildo_source:part:start saas.website.astro.site-context facet:layer:astro facet:family:website\n *\n * `WebsiteSiteContext` — the aggregated site-level inputs every Astro\n * build-time engine helper consumes. Composes the consumer-authored\n * `WebsiteRootConfig` (page registry, design tokens, navigation chrome,\n * site-level SEO branding) with the per-service `WildoMarketingWebsiteConfig`\n * loaded from `wildo.website.config.ts` (locales, default locale,\n * public origin, and the `appOrigin` / `apiOrigin` the site reaches the\n * product through — settled for THIS build by `loadWebsiteSiteContext`).\n *\n * # Why a dedicated aggregator type (vs. threading both raw configs)\n *\n * The seam-collapse (`saas-website-seam-collapse.md`, Option B) made\n * `WildoMarketingWebsiteConfig` the SINGLE source of truth for the\n * marketing-site routing surface (`defaultLocale`, `locales`,\n * `publicOrigin`). Every engine helper that previously read those\n * fields from `WebsiteRootConfig` now reads them from\n * `WildoMarketingWebsiteConfig` — and four out of five of those helpers\n * also still read OTHER fields from `WebsiteRootConfig` (page\n * manifests, chrome label keys, design tokens, `seo.siteName`,\n * `seo.defaultOgImageUrl`, `skipToMainContentLabelKey`). Threading\n * both raw configs through every helper signature would:\n *\n * 1. **Bloat call sites.** Every consumer `.astro` file would have\n * to import both `WEBSITE_ROOT_CONFIG` and the loaded\n * `WILDO_WEBSITE_CONFIG`, then forward both to every helper.\n * 2. **Spread the consumer-side composition responsibility.**\n * Each `.astro` file would need to know which subset of the\n * configs each helper consumes — a refactor hazard the next time\n * we add a field that the head renderer reads from the website\n * config but the runtime loader does not.\n * 3. **Defeat top-level `await` ergonomics.** The website config\n * load is async (`loadWebsiteConfig` does jiti-backed file I/O);\n * forcing every `.astro` file to `await` it would either spread\n * the await across every page or force every page to receive the\n * already-loaded config from a separate consumer module —\n * duplicating the composition logic this aggregator centralizes.\n *\n * One aggregator, composed once at the consumer's\n * `<service-root>/src/website-site-context.ts`, is the minimum-blast\n * shape: every engine helper takes `siteContext: WebsiteSiteContext`,\n * every `.astro` file imports the singleton `WEBSITE_SITE_CONTEXT`\n * once, and the next field move (consumer-config-only field, or\n * root-config-only field) lands in this type's shape with no\n * helper-signature ripple.\n *\n * # Why frozen (defensive)\n *\n * The composed value is shared across every page render in the build\n * graph. A misbehaving Astro plugin (or a future consumer-side audit\n * subprocess) that mutated either nested config in place would\n * silently corrupt every subsequent page. `Object.freeze` is a cheap\n * top-level guard — the inner configs are themselves already produced\n * by Zod parsers (which return fresh objects), so mutating their\n * fields would require a deliberate `as any` cast at the consumer's\n * call site rather than an accidental in-place edit.\n *\n * # When this type WOULD grow\n *\n * Future inputs that are also \"site-wide\" but neither belong to\n * `WebsiteRootConfig` (chrome-rendered) nor to\n * `WildoMarketingWebsiteConfig` (per-service routing/origins) — e.g.\n * a build-environment block (`production` / `preview` / `dev`), or\n * a feature-flag bag — would land here as a third sibling field\n * rather than being plumbed through every helper signature\n * individually. The aggregator is the right place for that\n * orchestration concern; per-call ergonomics ride along for free.\n */\nexport interface WebsiteSiteContext {\n /**\n * The consumer's `WebsiteRootConfig` — page registry, design tokens,\n * navigation chrome, site-level SEO branding (`siteName`,\n * `defaultOgImageUrl?`), `chromeLabelKeys`,\n * `skipToMainContentLabelKey`. Single source of truth for chrome\n * + per-page meta declarations.\n */\n readonly rootConfig: WebsiteRootConfig;\n\n /**\n * The per-service `WildoMarketingWebsiteConfig` loaded from\n * `wildo.website.config.ts`. Single source of truth for the\n * marketing-site ROUTING + ORIGIN surface (`defaultLocale`,\n * `locales`, `publicOrigin`, `appOrigin`, `apiOrigin`). The two origins\n * are the values for THIS build (override, local dev, authored — see\n * `resolveWebsiteBuildOrigins`), which is why every page reads them here.\n */\n readonly websiteConfig: WildoMarketingWebsiteConfig;\n /**\n * Optional website-facing public provider block materialized from the\n * app's provider graph for the concrete `STATIC_WEBSITE` service.\n * Build-time only: the consumer loads it once and forwards it into the\n * React island as JSON-serializable data.\n */\n readonly frontendProviders?: FrontendProvidersBlock;\n}\n\n/**\n * Compose a `WebsiteSiteContext` from the two source configs. Pure +\n * synchronous — the I/O happens upstream in `loadWebsiteConfig` and\n * in the consumer's `WEBSITE_ROOT_CONFIG` module evaluation.\n *\n * **Defensive freezing**: the returned object is `Object.freeze`-d\n * top-level so the singleton handed to every helper cannot have its\n * `rootConfig` / `websiteConfig` references swapped in place by a\n * misbehaving plugin. The nested configs are NOT deep-frozen — Zod\n * already produces fresh objects per parse, and a deep freeze would\n * cost a recursive walk per build for protection that no realistic\n * plugin would breach.\n *\n * @example\n * ```ts\n * // examples/<app>/website/src/website-site-context.ts\n * import { composeWebsiteSiteContext } from '@wildo-ai/saas-website/astro';\n * import { loadWebsiteConfig } from '@wildo-ai/saas-website/config-loader';\n *\n * import { WEBSITE_ROOT_CONFIG } from './website-root.config';\n *\n * const websiteConfig = await loadWebsiteConfig({ saasRoot: '...' });\n * export const WEBSITE_SITE_CONTEXT = composeWebsiteSiteContext({\n * rootConfig: WEBSITE_ROOT_CONFIG,\n * websiteConfig,\n * });\n * ```\n */\nexport function composeWebsiteSiteContext(\n parts: WebsiteSiteContext,\n): WebsiteSiteContext {\n return Object.freeze({\n rootConfig: parts.rootConfig,\n websiteConfig: parts.websiteConfig,\n frontendProviders: parts.frontendProviders,\n });\n}\n/** @wildo_source:part:end saas.website.astro.site-context */\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"astro-island.d.ts","sourceRoot":"","sources":["../../src/astro-island.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAOH,cAAc,wBAAwB,CAAC;AACvC;;;;;;;;;;;;;;GAcG"}
1
+ {"version":3,"file":"astro-island.d.ts","sourceRoot":"","sources":["../../../src/astro-island.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAOH,cAAc,wBAAwB,CAAC;AACvC;;;;;;;;;;;;;;GAcG"}
@@ -1 +1 @@
1
- {"version":3,"file":"astro.d.ts","sourceRoot":"","sources":["../../src/astro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qCAAqC,CAAC;AACpD,cAAc,2BAA2B,CAAC;AAC1C;;;;;;;;;;;GAWG;AACH,cAAc,qCAAqC,CAAC;AACpD,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD;;;;;;;;GAQG;AACH,cAAc,iCAAiC,CAAC;AAChD;;;;;;;;GAQG;AACH,cAAc,yBAAyB,CAAC;AACxC;;;;;;;;;;;;;;;;;;GAkBG;AACH,cAAc,0BAA0B,CAAC;AACzC;;;;;;;;;;GAUG;AACH,cAAc,kCAAkC,CAAC;AACjD,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC;;;;;;;;GAQG;AACH,cAAc,kEAAkE,CAAC"}
1
+ {"version":3,"file":"astro.d.ts","sourceRoot":"","sources":["../../../src/astro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AAEH,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qCAAqC,CAAC;AACpD,cAAc,2BAA2B,CAAC;AAC1C;;;;;;;;;;;GAWG;AACH,cAAc,qCAAqC,CAAC;AACpD,cAAc,oCAAoC,CAAC;AACnD,cAAc,kCAAkC,CAAC;AACjD;;;;;;;;GAQG;AACH,cAAc,iCAAiC,CAAC;AAChD;;;;;;;;GAQG;AACH,cAAc,yBAAyB,CAAC;AACxC;;;;;;;;;;;;;;;;;;GAkBG;AACH,cAAc,0BAA0B,CAAC;AACzC;;;;;;;;;;GAUG;AACH,cAAc,kCAAkC,CAAC;AACjD,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC;;;;;;;;GAQG;AACH,cAAc,kEAAkE,CAAC"}
@@ -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
  export * from './schemas/refs/page-ref.schemas';
29
22
  export * from './schemas/refs/section-ref.schemas';
@@ -1 +1 @@
1
- {"version":3,"file":"companion-exports.d.ts","sourceRoot":"","sources":["../../src/companion-exports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,cAAc,iCAAiC,CAAC;AAChD,cAAc,oCAAoC,CAAC;AACnD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,oDAAoD,CAAC;AACnE,cAAc,8DAA8D,CAAC;AAC7E,cAAc,8DAA8D,CAAC;AAC7E,cAAc,0DAA0D,CAAC;AACzE,cAAc,wDAAwD,CAAC;AACvE,cAAc,qDAAqD,CAAC;AACpE,cAAc,2CAA2C,CAAC"}
1
+ {"version":3,"file":"companion-exports.d.ts","sourceRoot":"","sources":["../../../src/companion-exports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,cAAc,iCAAiC,CAAC;AAChD,cAAc,oCAAoC,CAAC;AACnD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,oDAAoD,CAAC;AACnE,cAAc,8DAA8D,CAAC;AAC7E,cAAc,8DAA8D,CAAC;AAC7E,cAAc,0DAA0D,CAAC;AACzE,cAAc,wDAAwD,CAAC;AACvE,cAAc,qDAAqD,CAAC;AACpE,cAAc,2CAA2C,CAAC"}