@wildo-ai/saas-website 1.1.4 → 1.1.5

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 (153) hide show
  1. package/dist/esm/astro/blog-post-bridge.d.ts +14 -20
  2. package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -1
  3. package/dist/esm/astro/blog-post-bridge.js +4 -2
  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 +30 -20
  7. package/dist/esm/astro/bridge-runtime.d.ts.map +1 -1
  8. package/dist/esm/astro/bridge-runtime.js +13 -3
  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.map +1 -1
  14. package/dist/esm/astro/label-pack-loader.js +5 -0
  15. package/dist/esm/astro/label-pack-loader.js.map +1 -1
  16. package/dist/esm/astro/llms-txt-server.d.ts.map +1 -1
  17. package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -1
  18. package/dist/esm/astro/robots-renderer.d.ts.map +1 -1
  19. package/dist/esm/astro/robots-renderer.js.map +1 -1
  20. package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -1
  21. package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -1
  22. package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -1
  23. package/dist/esm/astro/website-page-head.renderer.js.map +1 -1
  24. package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -1
  25. package/dist/esm/astro/website-site-context.d.ts.map +1 -1
  26. package/dist/esm/astro-island.d.ts.map +1 -1
  27. package/dist/esm/astro.d.ts.map +1 -1
  28. package/dist/esm/companion-exports.d.ts.map +1 -1
  29. package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -1
  30. package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -1
  31. package/dist/esm/config/define-website-config.d.ts.map +1 -1
  32. package/dist/esm/config/index.d.ts.map +1 -1
  33. package/dist/esm/config/load-website-config.d.ts.map +1 -1
  34. package/dist/esm/config/load-website-config.js +2 -1
  35. package/dist/esm/config/load-website-config.js.map +1 -1
  36. package/dist/esm/config/wildo-website-config.schemas.d.ts +10 -5
  37. package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -1
  38. package/dist/esm/config/wildo-website-config.schemas.js +10 -5
  39. package/dist/esm/config/wildo-website-config.schemas.js.map +1 -1
  40. package/dist/esm/config-loader.d.ts.map +1 -1
  41. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -1
  42. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +3 -3
  43. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -1
  44. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +13 -9
  45. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -1
  46. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +11 -0
  47. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -1
  48. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +14 -1
  49. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -1
  50. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts +6 -0
  51. package/dist/esm/core/consent/WebsiteConsentBanner.d.ts.map +1 -0
  52. package/dist/esm/core/consent/WebsiteConsentBanner.js +128 -0
  53. package/dist/esm/core/consent/WebsiteConsentBanner.js.map +1 -0
  54. package/dist/esm/core/consent/WebsiteConsentContext.d.ts +85 -0
  55. package/dist/esm/core/consent/WebsiteConsentContext.d.ts.map +1 -0
  56. package/dist/esm/core/consent/WebsiteConsentContext.js +89 -0
  57. package/dist/esm/core/consent/WebsiteConsentContext.js.map +1 -0
  58. package/dist/esm/core/consent/WebsiteConsentGate.d.ts +27 -0
  59. package/dist/esm/core/consent/WebsiteConsentGate.d.ts.map +1 -0
  60. package/dist/esm/core/consent/WebsiteConsentGate.js +9 -0
  61. package/dist/esm/core/consent/WebsiteConsentGate.js.map +1 -0
  62. package/dist/esm/core/consent/useWebsiteConsent.d.ts +10 -0
  63. package/dist/esm/core/consent/useWebsiteConsent.d.ts.map +1 -0
  64. package/dist/esm/core/consent/useWebsiteConsent.js +17 -0
  65. package/dist/esm/core/consent/useWebsiteConsent.js.map +1 -0
  66. package/dist/esm/core/consent/website-consent-storage.d.ts +22 -0
  67. package/dist/esm/core/consent/website-consent-storage.d.ts.map +1 -0
  68. package/dist/esm/core/consent/website-consent-storage.js +102 -0
  69. package/dist/esm/core/consent/website-consent-storage.js.map +1 -0
  70. package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -1
  71. package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -1
  72. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +8 -1
  73. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -1
  74. package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -1
  75. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -1
  76. package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -1
  77. package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -1
  78. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -1
  79. package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -1
  80. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +1 -2
  81. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -1
  82. package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -1
  83. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts +33 -10
  84. package/dist/esm/core/external-providers/useWebsiteProviderScripts.d.ts.map +1 -1
  85. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js +64 -16
  86. package/dist/esm/core/external-providers/useWebsiteProviderScripts.js.map +1 -1
  87. package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -1
  88. package/dist/esm/core/factories/define-website-section.d.ts.map +1 -1
  89. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -1
  90. package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -1
  91. package/dist/esm/core/layouts/WebsitePageLayout.d.ts +21 -1
  92. package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -1
  93. package/dist/esm/core/layouts/WebsitePageLayout.js +32 -4
  94. package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -1
  95. package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -1
  96. package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -1
  97. package/dist/esm/index.d.ts +6 -0
  98. package/dist/esm/index.d.ts.map +1 -1
  99. package/dist/esm/index.js +6 -0
  100. package/dist/esm/index.js.map +1 -1
  101. package/dist/esm/mdx/BlogPost.d.ts.map +1 -1
  102. package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -1
  103. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -1
  104. package/dist/esm/mdx/index.d.ts.map +1 -1
  105. package/dist/esm/mdx.d.ts.map +1 -1
  106. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -1
  107. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -1
  108. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts +61 -0
  109. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.d.ts.map +1 -0
  110. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js +73 -0
  111. package/dist/esm/schemas/label-keys/website-consent-label-keys.schemas.js.map +1 -0
  112. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -1
  113. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -1
  114. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -1
  115. package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -1
  116. package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -1
  117. package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -1
  118. package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -1
  119. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -1
  120. package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -1
  121. package/dist/tsconfig.build.tsbuildinfo +1 -1
  122. package/package.json +6 -29
  123. package/src/astro/__tests__/blog-post-bridge.test.tsx +41 -0
  124. package/src/astro/__tests__/bridge-runtime.test.tsx +25 -0
  125. package/src/astro/__tests__/label-pack-loader.test.ts +26 -1
  126. package/src/astro/blog-post-bridge.tsx +18 -22
  127. package/src/astro/bridge-runtime.tsx +45 -23
  128. package/src/astro/label-pack-loader.ts +4 -0
  129. package/src/config/load-website-config.ts +2 -1
  130. package/src/config/wildo-website-config.schemas.ts +10 -5
  131. package/src/core/__tests__/WebsitePageLayout.test.tsx +1 -1
  132. package/src/core/__tests__/website-consent.test.tsx +311 -0
  133. package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +11 -4
  134. package/src/core/anonymous-session/inbound-contact-form.schema.ts +14 -9
  135. package/src/core/anonymous-session/website-anonymous-session-client.ts +15 -1
  136. package/src/core/consent/WebsiteConsentBanner.tsx +222 -0
  137. package/src/core/consent/WebsiteConsentContext.tsx +164 -0
  138. package/src/core/consent/WebsiteConsentGate.tsx +34 -0
  139. package/src/core/consent/useWebsiteConsent.ts +18 -0
  140. package/src/core/consent/website-consent-storage.ts +116 -0
  141. package/src/core/contexts/WebsiteRuntimeContext.tsx +8 -1
  142. package/src/core/external-providers/frontend-provider-registry.website.ts +1 -2
  143. package/src/core/external-providers/useWebsiteProviderScripts.ts +72 -17
  144. package/src/core/layouts/WebsitePageLayout.tsx +66 -3
  145. package/src/index.ts +7 -0
  146. package/src/schemas/label-keys/__tests__/scaffolded-consent-label-pack.parity.test.ts +71 -0
  147. package/src/schemas/label-keys/website-consent-label-keys.schemas.ts +78 -0
  148. package/dist/esm/core/external-providers/provider-scripts.website.d.ts +0 -68
  149. package/dist/esm/core/external-providers/provider-scripts.website.d.ts.map +0 -1
  150. package/dist/esm/core/external-providers/provider-scripts.website.js +0 -99
  151. package/dist/esm/core/external-providers/provider-scripts.website.js.map +0 -1
  152. package/src/core/external-providers/__tests__/provider-scripts.website.test.ts +0 -146
  153. package/src/core/external-providers/provider-scripts.website.ts +0 -126
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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 +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 +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,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 +1 @@
1
- {"version":3,"file":"website-page-head.renderer.js","sourceRoot":"","sources":["../../../src/astro/website-page-head.renderer.ts"],"names":[],"mappings":"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","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":"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","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 +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"}
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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 +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"}
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;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 +1 @@
1
- {"version":3,"file":"WebsiteInternalButton.d.ts","sourceRoot":"","sources":["../../../../src/components/low-level/WebsiteInternalButton.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAOlE,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAC;AAmBzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,OAAO,EACP,IAAI,EACJ,SAAS,EACT,KAAK,EACL,EAAE,EACF,SAAS,EACT,QAAQ,GACT,EAAE,0BAA0B,GAAG,SAAS,CA+CxC;yBAxDe,qBAAqB;;;AA0DrC,qEAAqE"}
1
+ {"version":3,"file":"WebsiteInternalButton.d.ts","sourceRoot":"","sources":["../../../../../src/components/low-level/WebsiteInternalButton.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAOlE,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAC;AAmBzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,OAAO,EACP,IAAI,EACJ,SAAS,EACT,KAAK,EACL,EAAE,EACF,SAAS,EACT,QAAQ,GACT,EAAE,0BAA0B,GAAG,SAAS,CA+CxC;yBAxDe,qBAAqB;;;AA0DrC,qEAAqE"}
@@ -1 +1 @@
1
- {"version":3,"file":"WebsiteInternalLink.d.ts","sourceRoot":"","sources":["../../../../src/components/low-level/WebsiteInternalLink.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AASlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAqCD,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,SAAS,EACT,KAAK,EACL,EAAE,EACF,SAAS,EACT,QAAQ,GACT,EAAE,wBAAwB,GAAG,SAAS,CA8CtC;yBArDe,mBAAmB;;;AAuDnC,mEAAmE"}
1
+ {"version":3,"file":"WebsiteInternalLink.d.ts","sourceRoot":"","sources":["../../../../../src/components/low-level/WebsiteInternalLink.tsx"],"names":[],"mappings":"AAAA,OAAc,EAAE,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AASlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAqCD,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,SAAS,EACT,KAAK,EACL,EAAE,EACF,SAAS,EACT,QAAQ,GACT,EAAE,wBAAwB,GAAG,SAAS,CA8CtC;yBArDe,mBAAmB;;;AAuDnC,mEAAmE"}
@@ -1 +1 @@
1
- {"version":3,"file":"define-website-config.d.ts","sourceRoot":"","sources":["../../../src/config/define-website-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,gCAAgC,EACtC,MAAM,gCAAgC,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,gCAAgC,GACvC,2BAA2B,CAI7B"}
1
+ {"version":3,"file":"define-website-config.d.ts","sourceRoot":"","sources":["../../../../src/config/define-website-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,gCAAgC,EACtC,MAAM,gCAAgC,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,gCAAgC,GACvC,2BAA2B,CAI7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"load-website-config.d.ts","sourceRoot":"","sources":["../../../src/config/load-website-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oCAAoC,4BAA4B,CAAC;AAkB9E;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,kBAAkB,EAAE,MAAM,EAC1B,MAAM,EAAE,2BAA2B,GAClC,IAAI,CAEN;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,UAAU,EAAE,eAAe,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,2BAA2B,CAAC,CA4DtC"}
1
+ {"version":3,"file":"load-website-config.d.ts","sourceRoot":"","sources":["../../../../src/config/load-website-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAGlF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oCAAoC,4BAA4B,CAAC;AAkB9E;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,kBAAkB,EAAE,MAAM,EAC1B,MAAM,EAAE,2BAA2B,GAClC,IAAI,CAEN;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,UAAU,EAAE,eAAe,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,2BAA2B,CAAC,CA4DtC"}
@@ -33,6 +33,7 @@
33
33
  import { existsSync } from 'node:fs';
34
34
  import { resolve as resolvePath } from 'node:path';
35
35
  import { AppFrontendType, loadConfig } from '@wildo-ai/platform-config-lib';
36
+ import { describeThrownValue } from '@wildo-ai/saas-models/public-runtime';
36
37
  /**
37
38
  * Conventional file name for the per-service marketing-website
38
39
  * configuration (Step 7.5 refactor — `saas-website-slice.md` /
@@ -163,7 +164,7 @@ export async function loadWebsiteConfig(args) {
163
164
  loaded = await loadConfig(websiteConfigPath);
164
165
  }
165
166
  catch (e) {
166
- const cause = e instanceof Error ? e.message : String(e);
167
+ const cause = describeThrownValue(e);
167
168
  throw new Error(`[wildo-website-config] Failed to load per-service marketing-website config at `
168
169
  + `${websiteConfigPath}. The file MUST do `
169
170
  + '`export default defineWebsiteConfig({...})` and pass '
@@ -1 +1 @@
1
- {"version":3,"file":"load-website-config.js","sourceRoot":"","sources":["../../../../src/config/load-website-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAI5E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,oCAAoC,GAAG,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAuC,CAAC;AAE5E;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CACrC,kBAA0B,EAC1B,MAAmC;IAEnC,oBAAoB,CAAC,GAAG,CAAC,WAAW,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB;IACrC,oBAAoB,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC;AAyBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAA2B;IAE3B,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAEtC,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,CAClE,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,KAAK,eAAe,CAAC,cAAc,CACzE,CAAC;IACF,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,gGAAgG;cAC5F,wFAAwF;cACxF,0EAA0E;cAC1E,+EAA+E;cAC/E,gFAAgF;cAChF,uBAAuB,CAC5B,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,iBAAiB,EAAE,cAAc,CAAC,GAAG,mBAAmB,CAAC;IAChE,MAAM,iBAAiB,GAAG,WAAW,CACnC,QAAQ,EACR,cAAc,CAAC,IAAI,EACnB,oCAAoC,CACrC,CAAC;IAEF,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC3D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,kDAAkD,iBAAiB,mBAAmB;cAClF,gEAAgE;cAChE,IAAI,oCAAoC,qBAAqB;cAC7D,IAAI,cAAc,CAAC,IAAI,yCAAyC;cAChE,uEAAuE;cACvE,2EAA2E;cAC3E,8BAA8B;cAC9B,IAAI,iBAAiB,SAAS;cAC9B,8EAA8E,CACnF,CAAC;IACJ,CAAC;IAED,IAAI,MAAmC,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,UAAU,CAA8B,iBAAiB,CAAC,CAAC;IAC5E,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,KAAK,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACzD,MAAM,IAAI,KAAK,CACb,gFAAgF;cAC5E,GAAG,iBAAiB,qBAAqB;cACzC,uDAAuD;cACvD,6EAA6E;cAC7E,6CAA6C;cAC7C,UAAU,KAAK,EAAE,CACtB,CAAC;IACJ,CAAC;IAED,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * @fileoverview Per-service `wildo.website.config.ts` loader.\n *\n * Mirrors the `loadTechnicalDocConfig()` shape used by\n * `platform/factory/wildo-dev-companion/src/technical-doc/services/technical-doc.companion.service.ts`\n * (see `saas-technical-doc.md` Step 7.5). Discovers the unique\n * `frontendType: STATIC_WEBSITE` service in the SaaS config, joins\n * `<saasRoot>/<service.path>/wildo.website.config.ts`, and loads it\n * via the jiti-backed `loadConfig` from `@wildo-ai/platform-config-lib`.\n *\n * The actual Zod validation runs INSIDE `defineWebsiteConfig` (which\n * the per-service config file invokes via\n * `export default defineWebsiteConfig({...})`), so by the time\n * `loadConfig` returns we already hold a parsed\n * `WildoMarketingWebsiteConfig`.\n *\n * @wildo-boundary\n * This module is **Node-only**. It imports `loadConfig` and\n * `AppFrontendType` from `@wildo-ai/platform-config-lib`, which\n * transitively pulls `jiti`, `node:fs`, `node:path`, `node:crypto`,\n * and other Node-only API surface. It MUST NOT be imported from\n * any browser-bundled entry of this engine package — specifically\n * NOT from `index.ts`, `companion-exports.ts`, or `astro-island.ts`.\n * The dedicated `./config-loader` package subpath exists precisely\n * to keep this surface OUT of the React-island bundle (enforced by\n * `__tests__/bundle-isolation.test.ts`).\n *\n * The Astro build's `astro.config.ts` (a Node-side file by\n * construction) is the canonical caller. A future companion\n * subprocess that audits the marketing surface would also call\n * from here. Browser bundles MUST NOT.\n */\n\nimport { existsSync } from 'node:fs';\nimport { resolve as resolvePath } from 'node:path';\nimport { AppFrontendType, loadConfig } from '@wildo-ai/platform-config-lib';\nimport type { WildoSaasConfig } from '@wildo-ai/platform-config-lib';\nimport type { WildoMarketingWebsiteConfig } from './wildo-website-config.schemas';\n\n/**\n * Conventional file name for the per-service marketing-website\n * configuration (Step 7.5 refactor — `saas-website-slice.md` /\n * `saas-technical-doc.md` Step 7.5 mirror). Authored at the website\n * service root next to `package.json` / `astro.config.ts`.\n *\n * Discovery is \"by convention, not by configuration\":\n *\n * - The loader locates this file by walking the `services` entry\n * whose `frontendType === STATIC_WEBSITE` and joining\n * `<saasRoot>/<service.path>/`. The saas-config schema's\n * `superRefine` already enforces \"at most one STATIC_WEBSITE\n * service per app\", so the lookup is deterministic — apps can\n * name the website service anything (`'website'`, `'marketing'`,\n * `'site'`).\n *\n * - File presence is the enable signal — there is NO `enabled`\n * field on `WildoMarketingWebsiteConfigSchema`. A STATIC_WEBSITE\n * service without this file is treated as a misconfiguration\n * and the loader throws (rather than silently shipping an Astro\n * build with no `i18n` routing / no canonical origin).\n */\nexport const PER_SERVICE_WEBSITE_CONFIG_FILE_NAME = 'wildo.website.config.ts';\n\n/**\n * Memoization slot scoped to the loader module. Keyed by absolute\n * config file path so multiple SaaS configs evaluated in the same\n * process (rare in production — possible in tests) do not collide.\n *\n * Lifetime: the loader is invoked from the Astro build / dev-server\n * pipeline, both of which restart the Node process on every dev-stack\n * reload (the watcher that picks up changes to `wildo.website.config.ts`\n * is the same watcher that reboots the build). A stale entry here\n * therefore cannot outlive a config edit.\n *\n * Tests can pre-populate a known path via {@link primeWebsiteConfigCache}\n * to skip the jiti round-trip when exercising downstream consumers.\n */\nconst WEBSITE_CONFIG_CACHE = new Map<string, WildoMarketingWebsiteConfig>();\n\n/**\n * Test helper — pre-populate the loader's memoization slot for a\n * given absolute config path. Mirrors the\n * `cachedTechnicalDocConfig` pre-population trick used by\n * `technical-doc.publish-openapi.service.test.ts` so the loader's\n * downstream consumers can be exercised in tests without writing a\n * `.ts` file to disk + invoking jiti.\n *\n * Production callers MUST NOT use this helper.\n */\nexport function primeWebsiteConfigCache(\n absoluteConfigPath: string,\n config: WildoMarketingWebsiteConfig,\n): void {\n WEBSITE_CONFIG_CACHE.set(resolvePath(absoluteConfigPath), config);\n}\n\n/**\n * Test helper — clear every cached entry. Use in `beforeEach`/`afterEach`\n * to prevent test cross-contamination.\n */\nexport function clearWebsiteConfigCache(): void {\n WEBSITE_CONFIG_CACHE.clear();\n}\n\n/**\n * Arguments for {@link loadWebsiteConfig}. Kept as a structured shape\n * (rather than positional `loadWebsiteConfig(saasRoot, saasConfig)`)\n * so that future per-call options (e.g. an explicit `serviceKey`\n * override for multi-website apps the day the singleton invariant\n * relaxes) slot in without breaking callers.\n */\nexport interface LoadWebsiteConfigArgs {\n /**\n * Absolute path to the SaaS app root — the directory holding\n * `wildo.saas.config.ts`. Used to resolve `<saasRoot>/<service.path>/`\n * for the website service folder.\n */\n saasRoot: string;\n /**\n * The already-parsed SaaS config (load once via\n * `loadConfig<WildoSaasConfig>(...)` upstream and pass it down here\n * — re-parsing per call would invoke jiti twice for one logical\n * load).\n */\n saasConfig: WildoSaasConfig;\n}\n\n/**\n * Discover and load the per-service marketing-website config.\n *\n * Resolution algorithm (mirrors\n * `TechnicalDocCompanionService.loadTechnicalDocConfig`):\n *\n * 1. Find the unique service in `saasConfig.services` whose\n * `frontendType === STATIC_WEBSITE`. The saas-config\n * `superRefine` guarantees there is at most one — if there is\n * none, throw with the misconfiguration message.\n * 2. Resolve `<saasRoot>/<service.path>/wildo.website.config.ts`.\n * 3. If the file does not exist, throw (file presence IS the\n * enable signal — a STATIC_WEBSITE service without this file\n * is broken).\n * 4. Check the memoization slot keyed on the absolute path; return\n * the cached value if present.\n * 5. Call `loadConfig<WildoMarketingWebsiteConfig>(absolutePath)`.\n * jiti evaluates the file, which invokes `defineWebsiteConfig`,\n * which invokes `WildoMarketingWebsiteConfigSchema.parse` and\n * emits any `@unimplemented` warnings. The returned object is\n * already-validated — we trust the in-file `defineWebsiteConfig`\n * contract here (mirrors the technical-doc loader pattern in\n * `TechnicalDocCompanionService.loadTechnicalDocConfig`, which\n * also does NOT re-parse). A file that hand-rolls the default\n * export without going through `defineWebsiteConfig` would\n * surface as a downstream type mismatch — acceptable given the\n * authoring contract is explicit and grep-discoverable.\n * 6. Memoize and return.\n *\n * @throws Error with a `[wildo-website-config]` prefix if no\n * STATIC_WEBSITE service is declared, the per-service config\n * file is missing, or the file's default export does not\n * pass `WildoMarketingWebsiteConfigSchema`. The loader\n * intentionally throws plain `Error`s rather than building a\n * `WildoBackendError` so it stays usable from the Astro\n * build pipeline (which has no DI container) — the companion\n * can wrap with `errorBuilder` at the call site if needed\n * (mirrors how `loadConfig` itself throws plain `Error`).\n */\nexport async function loadWebsiteConfig(\n args: LoadWebsiteConfigArgs,\n): Promise<WildoMarketingWebsiteConfig> {\n const { saasRoot, saasConfig } = args;\n\n const websiteServiceEntry = Object.entries(saasConfig.services).find(\n ([, service]) => service.frontendType === AppFrontendType.STATIC_WEBSITE,\n );\n if (!websiteServiceEntry) {\n throw new Error(\n '[wildo-website-config] No frontend service with `frontendType: STATIC_WEBSITE` is declared in '\n + 'wildo.saas.config.ts. The marketing-website Astro build / loader requires an authored '\n + 'STATIC_WEBSITE service because that service folder owns the per-service '\n + '`wildo.website.config.ts` (publicOrigin, appOrigin, defaultLocale, locales). '\n + 'Add a STATIC_WEBSITE service in wildo.saas.config.ts and ship the per-service '\n + 'config file under it.',\n );\n }\n\n const [websiteServiceKey, websiteService] = websiteServiceEntry;\n const websiteConfigPath = resolvePath(\n saasRoot,\n websiteService.path,\n PER_SERVICE_WEBSITE_CONFIG_FILE_NAME,\n );\n\n const cached = WEBSITE_CONFIG_CACHE.get(websiteConfigPath);\n if (cached !== undefined) {\n return cached;\n }\n\n if (!existsSync(websiteConfigPath)) {\n throw new Error(\n `[wildo-website-config] STATIC_WEBSITE service '${websiteServiceKey}' is declared in `\n + `wildo.saas.config.ts but the required per-service config file `\n + `'${PER_SERVICE_WEBSITE_CONFIG_FILE_NAME}' is missing under `\n + `'${websiteService.path}/'. File presence IS the enable signal `\n + '(saas-website-slice.md / saas-technical-doc.md Step 7.5 — the legacy '\n + '`marketingWebsite` block on wildo.saas.config.ts was dropped in favor of '\n + 'a per-service file). Create '\n + `'${websiteConfigPath}' with `\n + '`export default defineWebsiteConfig({ ... })` from `@wildo-ai/saas-website`.',\n );\n }\n\n let loaded: WildoMarketingWebsiteConfig;\n try {\n loaded = await loadConfig<WildoMarketingWebsiteConfig>(websiteConfigPath);\n } catch (e) {\n const cause = e instanceof Error ? e.message : String(e);\n throw new Error(\n `[wildo-website-config] Failed to load per-service marketing-website config at `\n + `${websiteConfigPath}. The file MUST do `\n + '`export default defineWebsiteConfig({...})` and pass '\n + 'WildoMarketingWebsiteConfigSchema (`z.strictObject` — unknown keys reject; '\n + '`defaultLocale` must appear in `locales`). '\n + `Cause: ${cause}`,\n );\n }\n\n WEBSITE_CONFIG_CACHE.set(websiteConfigPath, loaded);\n return loaded;\n}\n"]}
1
+ {"version":3,"file":"load-website-config.js","sourceRoot":"","sources":["../../../../src/config/load-website-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAI5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,oCAAoC,GAAG,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAuC,CAAC;AAE5E;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CACrC,kBAA0B,EAC1B,MAAmC;IAEnC,oBAAoB,CAAC,GAAG,CAAC,WAAW,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB;IACrC,oBAAoB,CAAC,KAAK,EAAE,CAAC;AAC/B,CAAC;AAyBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAA2B;IAE3B,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC;IAEtC,MAAM,mBAAmB,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,CAClE,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,KAAK,eAAe,CAAC,cAAc,CACzE,CAAC;IACF,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,gGAAgG;cAC5F,wFAAwF;cACxF,0EAA0E;cAC1E,+EAA+E;cAC/E,gFAAgF;cAChF,uBAAuB,CAC5B,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,iBAAiB,EAAE,cAAc,CAAC,GAAG,mBAAmB,CAAC;IAChE,MAAM,iBAAiB,GAAG,WAAW,CACnC,QAAQ,EACR,cAAc,CAAC,IAAI,EACnB,oCAAoC,CACrC,CAAC;IAEF,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC3D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,kDAAkD,iBAAiB,mBAAmB;cAClF,gEAAgE;cAChE,IAAI,oCAAoC,qBAAqB;cAC7D,IAAI,cAAc,CAAC,IAAI,yCAAyC;cAChE,uEAAuE;cACvE,2EAA2E;cAC3E,8BAA8B;cAC9B,IAAI,iBAAiB,SAAS;cAC9B,8EAA8E,CACnF,CAAC;IACJ,CAAC;IAED,IAAI,MAAmC,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,UAAU,CAA8B,iBAAiB,CAAC,CAAC;IAC5E,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,KAAK,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,gFAAgF;cAC5E,GAAG,iBAAiB,qBAAqB;cACzC,uDAAuD;cACvD,6EAA6E;cAC7E,6CAA6C;cAC7C,UAAU,KAAK,EAAE,CACtB,CAAC;IACJ,CAAC;IAED,oBAAoB,CAAC,GAAG,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/**\n * @fileoverview Per-service `wildo.website.config.ts` loader.\n *\n * Mirrors the `loadTechnicalDocConfig()` shape used by\n * `platform/factory/wildo-dev-companion/src/technical-doc/services/technical-doc.companion.service.ts`\n * (see `saas-technical-doc.md` Step 7.5). Discovers the unique\n * `frontendType: STATIC_WEBSITE` service in the SaaS config, joins\n * `<saasRoot>/<service.path>/wildo.website.config.ts`, and loads it\n * via the jiti-backed `loadConfig` from `@wildo-ai/platform-config-lib`.\n *\n * The actual Zod validation runs INSIDE `defineWebsiteConfig` (which\n * the per-service config file invokes via\n * `export default defineWebsiteConfig({...})`), so by the time\n * `loadConfig` returns we already hold a parsed\n * `WildoMarketingWebsiteConfig`.\n *\n * @wildo-boundary\n * This module is **Node-only**. It imports `loadConfig` and\n * `AppFrontendType` from `@wildo-ai/platform-config-lib`, which\n * transitively pulls `jiti`, `node:fs`, `node:path`, `node:crypto`,\n * and other Node-only API surface. It MUST NOT be imported from\n * any browser-bundled entry of this engine package — specifically\n * NOT from `index.ts`, `companion-exports.ts`, or `astro-island.ts`.\n * The dedicated `./config-loader` package subpath exists precisely\n * to keep this surface OUT of the React-island bundle (enforced by\n * `__tests__/bundle-isolation.test.ts`).\n *\n * The Astro build's `astro.config.ts` (a Node-side file by\n * construction) is the canonical caller. A future companion\n * subprocess that audits the marketing surface would also call\n * from here. Browser bundles MUST NOT.\n */\n\nimport { existsSync } from 'node:fs';\nimport { resolve as resolvePath } from 'node:path';\nimport { AppFrontendType, loadConfig } from '@wildo-ai/platform-config-lib';\nimport type { WildoSaasConfig } from '@wildo-ai/platform-config-lib';\nimport type { WildoMarketingWebsiteConfig } from './wildo-website-config.schemas';\n\nimport { describeThrownValue } from '@wildo-ai/saas-models/public-runtime';\n/**\n * Conventional file name for the per-service marketing-website\n * configuration (Step 7.5 refactor — `saas-website-slice.md` /\n * `saas-technical-doc.md` Step 7.5 mirror). Authored at the website\n * service root next to `package.json` / `astro.config.ts`.\n *\n * Discovery is \"by convention, not by configuration\":\n *\n * - The loader locates this file by walking the `services` entry\n * whose `frontendType === STATIC_WEBSITE` and joining\n * `<saasRoot>/<service.path>/`. The saas-config schema's\n * `superRefine` already enforces \"at most one STATIC_WEBSITE\n * service per app\", so the lookup is deterministic — apps can\n * name the website service anything (`'website'`, `'marketing'`,\n * `'site'`).\n *\n * - File presence is the enable signal — there is NO `enabled`\n * field on `WildoMarketingWebsiteConfigSchema`. A STATIC_WEBSITE\n * service without this file is treated as a misconfiguration\n * and the loader throws (rather than silently shipping an Astro\n * build with no `i18n` routing / no canonical origin).\n */\nexport const PER_SERVICE_WEBSITE_CONFIG_FILE_NAME = 'wildo.website.config.ts';\n\n/**\n * Memoization slot scoped to the loader module. Keyed by absolute\n * config file path so multiple SaaS configs evaluated in the same\n * process (rare in production — possible in tests) do not collide.\n *\n * Lifetime: the loader is invoked from the Astro build / dev-server\n * pipeline, both of which restart the Node process on every dev-stack\n * reload (the watcher that picks up changes to `wildo.website.config.ts`\n * is the same watcher that reboots the build). A stale entry here\n * therefore cannot outlive a config edit.\n *\n * Tests can pre-populate a known path via {@link primeWebsiteConfigCache}\n * to skip the jiti round-trip when exercising downstream consumers.\n */\nconst WEBSITE_CONFIG_CACHE = new Map<string, WildoMarketingWebsiteConfig>();\n\n/**\n * Test helper — pre-populate the loader's memoization slot for a\n * given absolute config path. Mirrors the\n * `cachedTechnicalDocConfig` pre-population trick used by\n * `technical-doc.publish-openapi.service.test.ts` so the loader's\n * downstream consumers can be exercised in tests without writing a\n * `.ts` file to disk + invoking jiti.\n *\n * Production callers MUST NOT use this helper.\n */\nexport function primeWebsiteConfigCache(\n absoluteConfigPath: string,\n config: WildoMarketingWebsiteConfig,\n): void {\n WEBSITE_CONFIG_CACHE.set(resolvePath(absoluteConfigPath), config);\n}\n\n/**\n * Test helper — clear every cached entry. Use in `beforeEach`/`afterEach`\n * to prevent test cross-contamination.\n */\nexport function clearWebsiteConfigCache(): void {\n WEBSITE_CONFIG_CACHE.clear();\n}\n\n/**\n * Arguments for {@link loadWebsiteConfig}. Kept as a structured shape\n * (rather than positional `loadWebsiteConfig(saasRoot, saasConfig)`)\n * so that future per-call options (e.g. an explicit `serviceKey`\n * override for multi-website apps the day the singleton invariant\n * relaxes) slot in without breaking callers.\n */\nexport interface LoadWebsiteConfigArgs {\n /**\n * Absolute path to the SaaS app root — the directory holding\n * `wildo.saas.config.ts`. Used to resolve `<saasRoot>/<service.path>/`\n * for the website service folder.\n */\n saasRoot: string;\n /**\n * The already-parsed SaaS config (load once via\n * `loadConfig<WildoSaasConfig>(...)` upstream and pass it down here\n * — re-parsing per call would invoke jiti twice for one logical\n * load).\n */\n saasConfig: WildoSaasConfig;\n}\n\n/**\n * Discover and load the per-service marketing-website config.\n *\n * Resolution algorithm (mirrors\n * `TechnicalDocCompanionService.loadTechnicalDocConfig`):\n *\n * 1. Find the unique service in `saasConfig.services` whose\n * `frontendType === STATIC_WEBSITE`. The saas-config\n * `superRefine` guarantees there is at most one — if there is\n * none, throw with the misconfiguration message.\n * 2. Resolve `<saasRoot>/<service.path>/wildo.website.config.ts`.\n * 3. If the file does not exist, throw (file presence IS the\n * enable signal — a STATIC_WEBSITE service without this file\n * is broken).\n * 4. Check the memoization slot keyed on the absolute path; return\n * the cached value if present.\n * 5. Call `loadConfig<WildoMarketingWebsiteConfig>(absolutePath)`.\n * jiti evaluates the file, which invokes `defineWebsiteConfig`,\n * which invokes `WildoMarketingWebsiteConfigSchema.parse` and\n * emits any `@unimplemented` warnings. The returned object is\n * already-validated — we trust the in-file `defineWebsiteConfig`\n * contract here (mirrors the technical-doc loader pattern in\n * `TechnicalDocCompanionService.loadTechnicalDocConfig`, which\n * also does NOT re-parse). A file that hand-rolls the default\n * export without going through `defineWebsiteConfig` would\n * surface as a downstream type mismatch — acceptable given the\n * authoring contract is explicit and grep-discoverable.\n * 6. Memoize and return.\n *\n * @throws Error with a `[wildo-website-config]` prefix if no\n * STATIC_WEBSITE service is declared, the per-service config\n * file is missing, or the file's default export does not\n * pass `WildoMarketingWebsiteConfigSchema`. The loader\n * intentionally throws plain `Error`s rather than building a\n * `WildoBackendError` so it stays usable from the Astro\n * build pipeline (which has no DI container) — the companion\n * can wrap with `errorBuilder` at the call site if needed\n * (mirrors how `loadConfig` itself throws plain `Error`).\n */\nexport async function loadWebsiteConfig(\n args: LoadWebsiteConfigArgs,\n): Promise<WildoMarketingWebsiteConfig> {\n const { saasRoot, saasConfig } = args;\n\n const websiteServiceEntry = Object.entries(saasConfig.services).find(\n ([, service]) => service.frontendType === AppFrontendType.STATIC_WEBSITE,\n );\n if (!websiteServiceEntry) {\n throw new Error(\n '[wildo-website-config] No frontend service with `frontendType: STATIC_WEBSITE` is declared in '\n + 'wildo.saas.config.ts. The marketing-website Astro build / loader requires an authored '\n + 'STATIC_WEBSITE service because that service folder owns the per-service '\n + '`wildo.website.config.ts` (publicOrigin, appOrigin, defaultLocale, locales). '\n + 'Add a STATIC_WEBSITE service in wildo.saas.config.ts and ship the per-service '\n + 'config file under it.',\n );\n }\n\n const [websiteServiceKey, websiteService] = websiteServiceEntry;\n const websiteConfigPath = resolvePath(\n saasRoot,\n websiteService.path,\n PER_SERVICE_WEBSITE_CONFIG_FILE_NAME,\n );\n\n const cached = WEBSITE_CONFIG_CACHE.get(websiteConfigPath);\n if (cached !== undefined) {\n return cached;\n }\n\n if (!existsSync(websiteConfigPath)) {\n throw new Error(\n `[wildo-website-config] STATIC_WEBSITE service '${websiteServiceKey}' is declared in `\n + `wildo.saas.config.ts but the required per-service config file `\n + `'${PER_SERVICE_WEBSITE_CONFIG_FILE_NAME}' is missing under `\n + `'${websiteService.path}/'. File presence IS the enable signal `\n + '(saas-website-slice.md / saas-technical-doc.md Step 7.5 — the legacy '\n + '`marketingWebsite` block on wildo.saas.config.ts was dropped in favor of '\n + 'a per-service file). Create '\n + `'${websiteConfigPath}' with `\n + '`export default defineWebsiteConfig({ ... })` from `@wildo-ai/saas-website`.',\n );\n }\n\n let loaded: WildoMarketingWebsiteConfig;\n try {\n loaded = await loadConfig<WildoMarketingWebsiteConfig>(websiteConfigPath);\n } catch (e) {\n const cause = describeThrownValue(e);\n throw new Error(\n `[wildo-website-config] Failed to load per-service marketing-website config at `\n + `${websiteConfigPath}. The file MUST do `\n + '`export default defineWebsiteConfig({...})` and pass '\n + 'WildoMarketingWebsiteConfigSchema (`z.strictObject` — unknown keys reject; '\n + '`defaultLocale` must appear in `locales`). '\n + `Cause: ${cause}`,\n );\n }\n\n WEBSITE_CONFIG_CACHE.set(websiteConfigPath, loaded);\n return loaded;\n}\n"]}
@@ -177,11 +177,16 @@ export declare const WildoMarketingWebsiteConfigSchema: z.ZodObject<{
177
177
  * Origin of the SaaS application the marketing site links into
178
178
  * (signup / login / pricing → checkout).
179
179
  *
180
- * **Today**: NO runtime consumer. The cross-origin handoff
181
- * surface that will read it does not exist yet. Authored
182
- * eagerly so the link-surface PR is a single framework-owned move
183
- * — until then the field is exercised only by the schema
184
- * parser.
180
+ * Read at runtime since #493: each `.astro` page forwards it to its
181
+ * bridge island, and the CTA reads it off `WebsiteRuntimeContext`.
182
+ *
183
+ * Per environment: a deployment's app lives at a different host in each
184
+ * environment, and a static site bakes the value in at build time, so the
185
+ * generated deploy workflow passes the environment's app origin as
186
+ * `PUBLIC_APP_ORIGIN` — derived from the infra config's `serviceHosts` and
187
+ * `publicDomain`, not authored a second time. An application's
188
+ * `wildo.website.config.ts` reads that first and keeps its authored value
189
+ * as the default for a build that passes none.
185
190
  *
186
191
  * Distinct from the general `WildoSaasConfig.services.<name>`
187
192
  * URL surface because the marketing site is a separate Astro
@@ -1 +1 @@
1
- {"version":3,"file":"wildo-website-config.schemas.d.ts","sourceRoot":"","sources":["../../../src/config/wildo-website-config.schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsHG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,oDAAoD,CAAC;AAEvF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB;;;kBASpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iCAAiC;IAE1C;;;;;;;;;;OAUG;;IASH;;;;;;;;;;;;;;OAcG;;IAGH;;;;;;;;;;;;;;;;;;;;;;OAsBG;;IAWH;;;;;OAKG;;;;;;;;IAGH;;;;;;;;;OASG;;IAGH;;;;;;;;;OASG;;;;;kBAqDH,CAAC;AACL,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAC5F,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AACjG,wDAAwD"}
1
+ {"version":3,"file":"wildo-website-config.schemas.d.ts","sourceRoot":"","sources":["../../../../src/config/wildo-website-config.schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsHG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,oDAAoD,CAAC;AAEvF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB;;;kBASpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,iCAAiC;IAE1C;;;;;;;;;;OAUG;;IASH;;;;;;;;;;;;;;;;;;;OAmBG;;IAGH;;;;;;;;;;;;;;;;;;;;;;OAsBG;;IAWH;;;;;OAKG;;;;;;;;IAGH;;;;;;;;;OASG;;IAGH;;;;;;;;;OASG;;;;;kBAqDH,CAAC;AACL,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAC5F,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AACjG,wDAAwD"}
@@ -186,11 +186,16 @@ export const WildoMarketingWebsiteConfigSchema = z
186
186
  * Origin of the SaaS application the marketing site links into
187
187
  * (signup / login / pricing → checkout).
188
188
  *
189
- * **Today**: NO runtime consumer. The cross-origin handoff
190
- * surface that will read it does not exist yet. Authored
191
- * eagerly so the link-surface PR is a single framework-owned move
192
- * — until then the field is exercised only by the schema
193
- * parser.
189
+ * Read at runtime since #493: each `.astro` page forwards it to its
190
+ * bridge island, and the CTA reads it off `WebsiteRuntimeContext`.
191
+ *
192
+ * Per environment: a deployment's app lives at a different host in each
193
+ * environment, and a static site bakes the value in at build time, so the
194
+ * generated deploy workflow passes the environment's app origin as
195
+ * `PUBLIC_APP_ORIGIN` — derived from the infra config's `serviceHosts` and
196
+ * `publicDomain`, not authored a second time. An application's
197
+ * `wildo.website.config.ts` reads that first and keeps its authored value
198
+ * as the default for a build that passes none.
194
199
  *
195
200
  * Distinct from the general `WildoSaasConfig.services.<name>`
196
201
  * URL surface because the marketing site is a separate Astro
@@ -1 +1 @@
1
- {"version":3,"file":"wildo-website-config.schemas.js","sourceRoot":"","sources":["../../../src/config/wildo-website-config.schemas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @wildo_source:part:start engine.saas-website.config facet:layer:engine facet:family:marketing-website\n *\n * Per-service marketing-website configuration block, authored in\n * the website service folder as `wildo.website.config.ts`.\n *\n * # What this file's loaded value is used for TODAY\n *\n * This schema is the single source of truth for the marketing-site routing\n * and origin surface. Every consumer reads here:\n *\n * - `defaultLocale` + `locales` — read by `buildAstroI18nConfig(...)`,\n * `assertLabelPacksValid(...)`, `loadWebsitePageRuntime(...)` and\n * `renderWebsitePageHeadHtml(...)` via the shared `WebsiteSiteContext`.\n * - `publicOrigin` — read by the head renderer to compose absolute\n * `<link rel=\"canonical\">`, `og:url`, hreflang `href` URLs and\n * the structured-data `mainEntityOfPage`. The previous\n * `WebsiteRootConfig.seo.siteOrigin` was deleted.\n * - `appOrigin` — the marketing → app handoff target, read at runtime\n * since #493. A `wildo.website.config.ts` is Node-loaded and so\n * invisible to a client island, which is why this value reaches the\n * browser the way `apiOrigin` does: the `.astro` page forwards it to\n * its bridge island, which puts it on `WebsiteRuntimeContext.appOrigin`\n * for the CTA to read.\n *\n * It previously had NO runtime consumer, and a site resolved the app\n * origin a SECOND time from a public build-time environment variable —\n * one host authored twice with no mechanical link, and a silent\n * fallback to localhost when the variable was unset. That is how a\n * marketing site comes to point at the wrong environment.\n * - `csp?` — read by the website-local `sync-nginx-csp` build step,\n * which merges the app-static posture with `infra.csp?.<serviceKey>`,\n * emits the generated artifact consumed by Astro pages, and rewrites\n * the checked-in nginx marker file used by the website Docker image.\n *\n * Future companion subprocesses that introspect the marketing\n * surface (locale-coverage audits, label-pack validation against\n * authored locales, SEO-link sanity checks) will also read here —\n * none exist today.\n *\n * # Why this is per-service website configuration\n *\n * Co-locating the schema with the engine package that owns its semantics\n * keeps three concerns separate from application-root service declarations:\n *\n * 1. **Right reuse axis.** Marketing-site content (locales, SEO origins,\n * future label-pack overrides, marketing-only CTAs) is conceptually\n * different from saas-app-level service declarations (`path`,\n * `serviceName`, `frontendType`, `sessionStorage`). The marketing\n * config travels with the website service folder, NOT with the\n * SaaS app root. Two unrelated lifecycles, two different review\n * audiences.\n * 2. **Engine boundary.** `platform-config-lib` no longer needs to\n * depend on `@wildo-ai/presets-components-models` (the home of\n * `AvailableLanguage`) just to type the marketing-site locale\n * enum. Adding a new website primitive (a new SEO field, a new\n * locale-pack id) belongs with the website package and its public\n * configuration contract.\n * 3. **Disambiguator hazard.** The previous root-level\n * `marketingWebsite?` block silently assumed \"one STATIC_WEBSITE\n * service per SaaS app\" — but nothing in the schema enforced it.\n * Two STATIC_WEBSITE services would have silently shared one\n * config block with no obvious \"which one wins?\" answer. The\n * application configuration rejects more than one STATIC_WEBSITE (and\n * TECH_DOC) service, so the per-service config-file lookup is\n * deterministic by construction.\n *\n * # Schema decisions encoded here\n *\n * - `z.strictObject` so authoring an unknown key surfaces as a parse\n * error at `defineWebsiteConfig` time instead of being silently\n * dropped (same posture as `WildoSaasConfigSchema` and\n * `WildoTechnicalDocConfigSchema`).\n *\n * - **No `enabled` field.** The gate is \"is there a `wildo.website.config.ts`\n * in a `frontendType: STATIC_WEBSITE` service folder?\" — file\n * presence under a STATIC_WEBSITE service IS the enable signal.\n * The Astro build / dev companion / future audit CLI requires the\n * file when it walks a STATIC_WEBSITE service folder, so a\n * half-authored config simply cannot exist (the loader fails\n * closed). Removing the boolean closes a foot-gun: an authored\n * `enabled: false` block was previously a silent way to keep the\n * config \"warm\" without shipping anything, and consumers had to\n * remember to read the flag in addition to the file presence — a\n * future read-the-config / forget-the-flag bug waiting to happen.\n * A separate `services.<key>.enabled` pattern (orthogonal to\n * marketing-website) is the right place for any \"temporarily\n * disable this service\" story (deferred — see DASHBOARD).\n *\n * - URL fields are `z.url()` (Zod 4 native validator) so we get\n * protocol + host parsing for free, matching the rest of the\n * codebase.\n *\n * - `csp?` is typed against `WildoCspConfigSchema` from\n * `@wildo-ai/platform-config-lib`, mirroring the docs service's\n * `wildo.tech-doc.config.ts` contract. The parsed value is consumed by\n * the shared `resolveCspPolicy()` / `emitCspArtifacts()` generator flow.\n *\n * - `defaultLocale ∈ locales` cross-check via `.superRefine` —\n * keeping `defaultLocale` as a top-level field is product-explicit\n * (\"which locale URL is the canonical one\") and decouples it from\n * list ordering. Locale-path uniqueness AND locale-value\n * uniqueness are also enforced — duplicate paths would silently\n * overwrite each other in the Astro build and corrupt the sitemap;\n * duplicate locales would produce duplicate hreflang entries.\n *\n * - Locale primitive is `AvailableLanguage`. The schema imports it\n * from `@wildo-ai/presets-components-models/public-runtime` (the\n * side-effect-free public-runtime seam), but **consumers authoring\n * `wildo.website.config.ts` must import it from\n * `@wildo-ai/saas-models/public-runtime`**. Both paths produce identical\n * values; saas-models simply re-exports the presets atom. Picking\n * one path consistently across consumer files prevents drift in\n * docs / examples / search results. Values authored in\n * `wildo.website.config.ts` are the lowercase BCP-47 strings\n * (`'en'`, `'fr'`), NOT the UPPERCASE TS enum member names\n * (`EN`, `FR`). `z.enum(AvailableLanguage)` parses against the\n * enum VALUES, which is what we want.\n */\n\nimport { WildoCspConfigSchema } from '@wildo-ai/platform-config-lib';\nimport { z } from 'zod';\nimport { AvailableLanguage } from '@wildo-ai/presets-components-models/public-runtime';\n\n/**\n * Per-locale configuration. Each authored locale gets one entry.\n *\n * `path` is the locale URL prefix segment (e.g. `'en'`, `'fr'`) — the\n * Astro builder maps `/en/...` and `/fr/...` paths via this value. We\n * do NOT auto-derive `path` from the locale string because some\n * deployments may want a flat root for the default locale (`/...`\n * instead of `/en/...`); keeping it explicit per locale leaves that\n * door open without a config-shape change.\n */\nexport const WebsiteLocaleConfigSchema = z.strictObject({\n locale: z.enum(AvailableLanguage),\n path: z\n .string()\n .min(1)\n .regex(\n /^[a-z0-9-]+$/,\n 'locale path must be lowercase alphanumeric with hyphens (becomes a URL segment)',\n ),\n});\nexport type WebsiteLocaleConfig = z.infer<typeof WebsiteLocaleConfigSchema>;\n\n/**\n * Per-service marketing-website configuration block.\n *\n * Authored as `export default defineWebsiteConfig({...})` in\n * `<website-service-root>/wildo.website.config.ts`. The Astro\n * build loads this file once at bootstrap via jiti and composes\n * a `WebsiteSiteContext`; helpers consume that composed context rather than\n * this raw value.\n *\n * @example\n * ```ts\n * import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n * import { defineWebsiteConfig } from '@wildo-ai/saas-website';\n *\n * export default defineWebsiteConfig({\n * publicOrigin: 'https://example.com',\n * appOrigin: 'https://app.example.com',\n * defaultLocale: AvailableLanguage.EN,\n * locales: [\n * { locale: AvailableLanguage.EN, path: 'en' },\n * { locale: AvailableLanguage.FR, path: 'fr' },\n * ],\n * csp: { enabled: true, reportOnly: true },\n * });\n * ```\n */\nexport const WildoMarketingWebsiteConfigSchema = z\n .strictObject({\n /**\n * Public origin of the marketing site (apex / www) — canonical\n * input the Astro head renderer emits as absolute URLs\n * (`<link rel=\"canonical\">`, `og:url`, hreflang `href`,\n * sitemap base, structured-data `mainEntityOfPage`).\n *\n * MUST NOT end with `/` — the head renderer composes\n * `${publicOrigin}${routePath}` and a trailing slash would emit\n * `https://example.com//`. Surface the constraint loudly here\n * rather than silently corrupting URLs at build time.\n */\n publicOrigin: z\n .url()\n .refine((value) => !value.endsWith('/'), {\n error:\n 'WildoMarketingWebsiteConfig.publicOrigin must NOT end with \"/\" (the head ' +\n 'renderer concatenates publicOrigin + routePath; trailing slash would emit \"//\")',\n }),\n\n /**\n * Origin of the SaaS application the marketing site links into\n * (signup / login / pricing → checkout).\n *\n * **Today**: NO runtime consumer. The cross-origin handoff\n * surface that will read it does not exist yet. Authored\n * eagerly so the link-surface PR is a single framework-owned move\n * — until then the field is exercised only by the schema\n * parser.\n *\n * Distinct from the general `WildoSaasConfig.services.<name>`\n * URL surface because the marketing site is a separate Astro\n * project that does not load the full SaaS app graph at build\n * time.\n */\n appOrigin: z.url(),\n\n /**\n * Backend API origin the marketing site POSTs to for anonymous\n * captures (lead / draft-note forms) — e.g.\n * `https://api.example.com`. Threaded through\n * `WebsiteRuntimeContext.apiOrigin` into the React island that\n * constructs the `WebsiteAnonymousSessionClient`.\n *\n * OPTIONAL because a pure brochure site with no capture forms never\n * talks to an API — only sites that host an anonymous-capture\n * section need it. When such a section renders without a resolved\n * `apiOrigin`, the section surfaces a loud misconfiguration rather\n * than silently POSTing nowhere.\n *\n * MUST NOT end with `/` — the anon client composes\n * `${apiOrigin}${MAIN_API_BASE_PATH}${path}` and a trailing slash\n * would emit a doubled `//` in the request URL. (The client also\n * defensively strips trailing slashes, but the refine surfaces the\n * mistake at authoring time.)\n *\n * Distinct from `appOrigin` (the human-facing app the marketing CTAs\n * link into) and `publicOrigin` (the marketing site's own canonical\n * origin): this is the machine-to-machine API host.\n */\n apiOrigin: z\n .url()\n .refine((value) => !value.endsWith('/'), {\n error:\n 'WildoMarketingWebsiteConfig.apiOrigin must NOT end with \"/\" (the anonymous ' +\n 'client concatenates apiOrigin + MAIN_API_BASE_PATH + path; a trailing slash ' +\n 'would emit \"//\")',\n })\n .optional(),\n\n /**\n * Optional Content-Security-Policy override for the marketing site.\n * Consumed by the website-local `sync-nginx-csp` step, which emits\n * the generated artifact for Astro page injection and rewrites the\n * website nginx marker file from the same resolved output.\n */\n csp: WildoCspConfigSchema.optional(),\n\n /**\n * Default locale the marketing site serves for unmatched URLs\n * and as the canonical hreflang signal. Must appear in\n * `locales` below (cross-check enforced by the schema-level\n * `.superRefine`).\n *\n * Read by `buildAstroI18nConfig(...)` (drives Astro's own\n * `i18n.defaultLocale`) and by the head renderer (drives\n * `<html lang=\"...\">`, `og:locale`, hreflang `x-default`).\n */\n defaultLocale: z.enum(AvailableLanguage),\n\n /**\n * Ordered list of locales the marketing site builds. Order is\n * preserved (used by the language switcher rendering and by\n * the sitemap emitter — when the sitemap surface lands).\n *\n * Read by `buildAstroI18nConfig(...)` (Astro `i18n.locales`),\n * by `assertLabelPacksValid(...)` (one label pack per locale),\n * by `loadWebsitePageRuntime(...)` (per-page locale resolution),\n * and by the head renderer (hreflang alternates).\n */\n locales: z.array(WebsiteLocaleConfigSchema).min(1),\n })\n .superRefine((config, ctx) => {\n const declaredLocales = new Set(config.locales.map((entry) => entry.locale));\n if (!declaredLocales.has(config.defaultLocale)) {\n ctx.addIssue({\n code: 'custom',\n path: ['defaultLocale'],\n message:\n `defaultLocale '${config.defaultLocale}' is not declared in 'locales' ` +\n `(declared: [${[...declaredLocales].join(', ')}]). The default locale ` +\n `must be one of the locales the site actually builds.`,\n });\n }\n\n // Locale-path uniqueness: two entries collapsing to the same URL\n // segment would silently overwrite each other in the Astro build\n // and corrupt the sitemap. Fail closed at parse time.\n const pathOwners = new Map<string, AvailableLanguage>();\n for (const entry of config.locales) {\n const previous = pathOwners.get(entry.path);\n if (previous !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['locales'],\n message:\n `Locale path '${entry.path}' is declared by both '${previous}' and ` +\n `'${entry.locale}'. Each locale must have a unique URL path segment.`,\n });\n continue;\n }\n pathOwners.set(entry.path, entry.locale);\n }\n\n // Locale-value uniqueness: same locale appearing twice would\n // produce duplicate hreflang entries and ambiguous default-locale\n // resolution.\n const localeOwners = new Map<AvailableLanguage, string>();\n for (const entry of config.locales) {\n const previousPath = localeOwners.get(entry.locale);\n if (previousPath !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['locales'],\n message:\n `Locale '${entry.locale}' is declared twice (paths '${previousPath}' ` +\n `and '${entry.path}'). Each locale must appear at most once.`,\n });\n continue;\n }\n localeOwners.set(entry.locale, entry.path);\n }\n });\nexport type WildoMarketingWebsiteConfig = z.infer<typeof WildoMarketingWebsiteConfigSchema>;\nexport type WildoMarketingWebsiteConfigInput = z.input<typeof WildoMarketingWebsiteConfigSchema>;\n/** @wildo_source:part:end engine.saas-website.config */\n"]}
1
+ {"version":3,"file":"wildo-website-config.schemas.js","sourceRoot":"","sources":["../../../../src/config/wildo-website-config.schemas.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @wildo_source:part:start engine.saas-website.config facet:layer:engine facet:family:marketing-website\n *\n * Per-service marketing-website configuration block, authored in\n * the website service folder as `wildo.website.config.ts`.\n *\n * # What this file's loaded value is used for TODAY\n *\n * This schema is the single source of truth for the marketing-site routing\n * and origin surface. Every consumer reads here:\n *\n * - `defaultLocale` + `locales` — read by `buildAstroI18nConfig(...)`,\n * `assertLabelPacksValid(...)`, `loadWebsitePageRuntime(...)` and\n * `renderWebsitePageHeadHtml(...)` via the shared `WebsiteSiteContext`.\n * - `publicOrigin` — read by the head renderer to compose absolute\n * `<link rel=\"canonical\">`, `og:url`, hreflang `href` URLs and\n * the structured-data `mainEntityOfPage`. The previous\n * `WebsiteRootConfig.seo.siteOrigin` was deleted.\n * - `appOrigin` — the marketing → app handoff target, read at runtime\n * since #493. A `wildo.website.config.ts` is Node-loaded and so\n * invisible to a client island, which is why this value reaches the\n * browser the way `apiOrigin` does: the `.astro` page forwards it to\n * its bridge island, which puts it on `WebsiteRuntimeContext.appOrigin`\n * for the CTA to read.\n *\n * It previously had NO runtime consumer, and a site resolved the app\n * origin a SECOND time from a public build-time environment variable —\n * one host authored twice with no mechanical link, and a silent\n * fallback to localhost when the variable was unset. That is how a\n * marketing site comes to point at the wrong environment.\n * - `csp?` — read by the website-local `sync-nginx-csp` build step,\n * which merges the app-static posture with `infra.csp?.<serviceKey>`,\n * emits the generated artifact consumed by Astro pages, and rewrites\n * the checked-in nginx marker file used by the website Docker image.\n *\n * Future companion subprocesses that introspect the marketing\n * surface (locale-coverage audits, label-pack validation against\n * authored locales, SEO-link sanity checks) will also read here —\n * none exist today.\n *\n * # Why this is per-service website configuration\n *\n * Co-locating the schema with the engine package that owns its semantics\n * keeps three concerns separate from application-root service declarations:\n *\n * 1. **Right reuse axis.** Marketing-site content (locales, SEO origins,\n * future label-pack overrides, marketing-only CTAs) is conceptually\n * different from saas-app-level service declarations (`path`,\n * `serviceName`, `frontendType`, `sessionStorage`). The marketing\n * config travels with the website service folder, NOT with the\n * SaaS app root. Two unrelated lifecycles, two different review\n * audiences.\n * 2. **Engine boundary.** `platform-config-lib` no longer needs to\n * depend on `@wildo-ai/presets-components-models` (the home of\n * `AvailableLanguage`) just to type the marketing-site locale\n * enum. Adding a new website primitive (a new SEO field, a new\n * locale-pack id) belongs with the website package and its public\n * configuration contract.\n * 3. **Disambiguator hazard.** The previous root-level\n * `marketingWebsite?` block silently assumed \"one STATIC_WEBSITE\n * service per SaaS app\" — but nothing in the schema enforced it.\n * Two STATIC_WEBSITE services would have silently shared one\n * config block with no obvious \"which one wins?\" answer. The\n * application configuration rejects more than one STATIC_WEBSITE (and\n * TECH_DOC) service, so the per-service config-file lookup is\n * deterministic by construction.\n *\n * # Schema decisions encoded here\n *\n * - `z.strictObject` so authoring an unknown key surfaces as a parse\n * error at `defineWebsiteConfig` time instead of being silently\n * dropped (same posture as `WildoSaasConfigSchema` and\n * `WildoTechnicalDocConfigSchema`).\n *\n * - **No `enabled` field.** The gate is \"is there a `wildo.website.config.ts`\n * in a `frontendType: STATIC_WEBSITE` service folder?\" — file\n * presence under a STATIC_WEBSITE service IS the enable signal.\n * The Astro build / dev companion / future audit CLI requires the\n * file when it walks a STATIC_WEBSITE service folder, so a\n * half-authored config simply cannot exist (the loader fails\n * closed). Removing the boolean closes a foot-gun: an authored\n * `enabled: false` block was previously a silent way to keep the\n * config \"warm\" without shipping anything, and consumers had to\n * remember to read the flag in addition to the file presence — a\n * future read-the-config / forget-the-flag bug waiting to happen.\n * A separate `services.<key>.enabled` pattern (orthogonal to\n * marketing-website) is the right place for any \"temporarily\n * disable this service\" story (deferred — see DASHBOARD).\n *\n * - URL fields are `z.url()` (Zod 4 native validator) so we get\n * protocol + host parsing for free, matching the rest of the\n * codebase.\n *\n * - `csp?` is typed against `WildoCspConfigSchema` from\n * `@wildo-ai/platform-config-lib`, mirroring the docs service's\n * `wildo.tech-doc.config.ts` contract. The parsed value is consumed by\n * the shared `resolveCspPolicy()` / `emitCspArtifacts()` generator flow.\n *\n * - `defaultLocale ∈ locales` cross-check via `.superRefine` —\n * keeping `defaultLocale` as a top-level field is product-explicit\n * (\"which locale URL is the canonical one\") and decouples it from\n * list ordering. Locale-path uniqueness AND locale-value\n * uniqueness are also enforced — duplicate paths would silently\n * overwrite each other in the Astro build and corrupt the sitemap;\n * duplicate locales would produce duplicate hreflang entries.\n *\n * - Locale primitive is `AvailableLanguage`. The schema imports it\n * from `@wildo-ai/presets-components-models/public-runtime` (the\n * side-effect-free public-runtime seam), but **consumers authoring\n * `wildo.website.config.ts` must import it from\n * `@wildo-ai/saas-models/public-runtime`**. Both paths produce identical\n * values; saas-models simply re-exports the presets atom. Picking\n * one path consistently across consumer files prevents drift in\n * docs / examples / search results. Values authored in\n * `wildo.website.config.ts` are the lowercase BCP-47 strings\n * (`'en'`, `'fr'`), NOT the UPPERCASE TS enum member names\n * (`EN`, `FR`). `z.enum(AvailableLanguage)` parses against the\n * enum VALUES, which is what we want.\n */\n\nimport { WildoCspConfigSchema } from '@wildo-ai/platform-config-lib';\nimport { z } from 'zod';\nimport { AvailableLanguage } from '@wildo-ai/presets-components-models/public-runtime';\n\n/**\n * Per-locale configuration. Each authored locale gets one entry.\n *\n * `path` is the locale URL prefix segment (e.g. `'en'`, `'fr'`) — the\n * Astro builder maps `/en/...` and `/fr/...` paths via this value. We\n * do NOT auto-derive `path` from the locale string because some\n * deployments may want a flat root for the default locale (`/...`\n * instead of `/en/...`); keeping it explicit per locale leaves that\n * door open without a config-shape change.\n */\nexport const WebsiteLocaleConfigSchema = z.strictObject({\n locale: z.enum(AvailableLanguage),\n path: z\n .string()\n .min(1)\n .regex(\n /^[a-z0-9-]+$/,\n 'locale path must be lowercase alphanumeric with hyphens (becomes a URL segment)',\n ),\n});\nexport type WebsiteLocaleConfig = z.infer<typeof WebsiteLocaleConfigSchema>;\n\n/**\n * Per-service marketing-website configuration block.\n *\n * Authored as `export default defineWebsiteConfig({...})` in\n * `<website-service-root>/wildo.website.config.ts`. The Astro\n * build loads this file once at bootstrap via jiti and composes\n * a `WebsiteSiteContext`; helpers consume that composed context rather than\n * this raw value.\n *\n * @example\n * ```ts\n * import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n * import { defineWebsiteConfig } from '@wildo-ai/saas-website';\n *\n * export default defineWebsiteConfig({\n * publicOrigin: 'https://example.com',\n * appOrigin: 'https://app.example.com',\n * defaultLocale: AvailableLanguage.EN,\n * locales: [\n * { locale: AvailableLanguage.EN, path: 'en' },\n * { locale: AvailableLanguage.FR, path: 'fr' },\n * ],\n * csp: { enabled: true, reportOnly: true },\n * });\n * ```\n */\nexport const WildoMarketingWebsiteConfigSchema = z\n .strictObject({\n /**\n * Public origin of the marketing site (apex / www) — canonical\n * input the Astro head renderer emits as absolute URLs\n * (`<link rel=\"canonical\">`, `og:url`, hreflang `href`,\n * sitemap base, structured-data `mainEntityOfPage`).\n *\n * MUST NOT end with `/` — the head renderer composes\n * `${publicOrigin}${routePath}` and a trailing slash would emit\n * `https://example.com//`. Surface the constraint loudly here\n * rather than silently corrupting URLs at build time.\n */\n publicOrigin: z\n .url()\n .refine((value) => !value.endsWith('/'), {\n error:\n 'WildoMarketingWebsiteConfig.publicOrigin must NOT end with \"/\" (the head ' +\n 'renderer concatenates publicOrigin + routePath; trailing slash would emit \"//\")',\n }),\n\n /**\n * Origin of the SaaS application the marketing site links into\n * (signup / login / pricing → checkout).\n *\n * Read at runtime since #493: each `.astro` page forwards it to its\n * bridge island, and the CTA reads it off `WebsiteRuntimeContext`.\n *\n * Per environment: a deployment's app lives at a different host in each\n * environment, and a static site bakes the value in at build time, so the\n * generated deploy workflow passes the environment's app origin as\n * `PUBLIC_APP_ORIGIN` — derived from the infra config's `serviceHosts` and\n * `publicDomain`, not authored a second time. An application's\n * `wildo.website.config.ts` reads that first and keeps its authored value\n * as the default for a build that passes none.\n *\n * Distinct from the general `WildoSaasConfig.services.<name>`\n * URL surface because the marketing site is a separate Astro\n * project that does not load the full SaaS app graph at build\n * time.\n */\n appOrigin: z.url(),\n\n /**\n * Backend API origin the marketing site POSTs to for anonymous\n * captures (lead / draft-note forms) — e.g.\n * `https://api.example.com`. Threaded through\n * `WebsiteRuntimeContext.apiOrigin` into the React island that\n * constructs the `WebsiteAnonymousSessionClient`.\n *\n * OPTIONAL because a pure brochure site with no capture forms never\n * talks to an API — only sites that host an anonymous-capture\n * section need it. When such a section renders without a resolved\n * `apiOrigin`, the section surfaces a loud misconfiguration rather\n * than silently POSTing nowhere.\n *\n * MUST NOT end with `/` — the anon client composes\n * `${apiOrigin}${MAIN_API_BASE_PATH}${path}` and a trailing slash\n * would emit a doubled `//` in the request URL. (The client also\n * defensively strips trailing slashes, but the refine surfaces the\n * mistake at authoring time.)\n *\n * Distinct from `appOrigin` (the human-facing app the marketing CTAs\n * link into) and `publicOrigin` (the marketing site's own canonical\n * origin): this is the machine-to-machine API host.\n */\n apiOrigin: z\n .url()\n .refine((value) => !value.endsWith('/'), {\n error:\n 'WildoMarketingWebsiteConfig.apiOrigin must NOT end with \"/\" (the anonymous ' +\n 'client concatenates apiOrigin + MAIN_API_BASE_PATH + path; a trailing slash ' +\n 'would emit \"//\")',\n })\n .optional(),\n\n /**\n * Optional Content-Security-Policy override for the marketing site.\n * Consumed by the website-local `sync-nginx-csp` step, which emits\n * the generated artifact for Astro page injection and rewrites the\n * website nginx marker file from the same resolved output.\n */\n csp: WildoCspConfigSchema.optional(),\n\n /**\n * Default locale the marketing site serves for unmatched URLs\n * and as the canonical hreflang signal. Must appear in\n * `locales` below (cross-check enforced by the schema-level\n * `.superRefine`).\n *\n * Read by `buildAstroI18nConfig(...)` (drives Astro's own\n * `i18n.defaultLocale`) and by the head renderer (drives\n * `<html lang=\"...\">`, `og:locale`, hreflang `x-default`).\n */\n defaultLocale: z.enum(AvailableLanguage),\n\n /**\n * Ordered list of locales the marketing site builds. Order is\n * preserved (used by the language switcher rendering and by\n * the sitemap emitter — when the sitemap surface lands).\n *\n * Read by `buildAstroI18nConfig(...)` (Astro `i18n.locales`),\n * by `assertLabelPacksValid(...)` (one label pack per locale),\n * by `loadWebsitePageRuntime(...)` (per-page locale resolution),\n * and by the head renderer (hreflang alternates).\n */\n locales: z.array(WebsiteLocaleConfigSchema).min(1),\n })\n .superRefine((config, ctx) => {\n const declaredLocales = new Set(config.locales.map((entry) => entry.locale));\n if (!declaredLocales.has(config.defaultLocale)) {\n ctx.addIssue({\n code: 'custom',\n path: ['defaultLocale'],\n message:\n `defaultLocale '${config.defaultLocale}' is not declared in 'locales' ` +\n `(declared: [${[...declaredLocales].join(', ')}]). The default locale ` +\n `must be one of the locales the site actually builds.`,\n });\n }\n\n // Locale-path uniqueness: two entries collapsing to the same URL\n // segment would silently overwrite each other in the Astro build\n // and corrupt the sitemap. Fail closed at parse time.\n const pathOwners = new Map<string, AvailableLanguage>();\n for (const entry of config.locales) {\n const previous = pathOwners.get(entry.path);\n if (previous !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['locales'],\n message:\n `Locale path '${entry.path}' is declared by both '${previous}' and ` +\n `'${entry.locale}'. Each locale must have a unique URL path segment.`,\n });\n continue;\n }\n pathOwners.set(entry.path, entry.locale);\n }\n\n // Locale-value uniqueness: same locale appearing twice would\n // produce duplicate hreflang entries and ambiguous default-locale\n // resolution.\n const localeOwners = new Map<AvailableLanguage, string>();\n for (const entry of config.locales) {\n const previousPath = localeOwners.get(entry.locale);\n if (previousPath !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['locales'],\n message:\n `Locale '${entry.locale}' is declared twice (paths '${previousPath}' ` +\n `and '${entry.path}'). Each locale must appear at most once.`,\n });\n continue;\n }\n localeOwners.set(entry.locale, entry.path);\n }\n });\nexport type WildoMarketingWebsiteConfig = z.infer<typeof WildoMarketingWebsiteConfigSchema>;\nexport type WildoMarketingWebsiteConfigInput = z.input<typeof WildoMarketingWebsiteConfigSchema>;\n/** @wildo_source:part:end engine.saas-website.config */\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../../src/config-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,cAAc,8BAA8B,CAAC"}
1
+ {"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../../../src/config-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,cAAc,8BAA8B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"InboundContactForm.d.ts","sourceRoot":"","sources":["../../../../src/core/anonymous-session/InboundContactForm.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAyC,MAAM,OAAO,CAAC;AAE9D,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AAExF,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,qBAAqB,EAAE,MAAM,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB;AAgBD,MAAM,WAAW,uBAAuB;IACtC,yFAAyF;IACzF,MAAM,EAAE,6BAA6B,CAAC;IACtC,wFAAwF;IACxF,MAAM,CAAC,EAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC3C,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAUD,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CA6F/G"}
1
+ {"version":3,"file":"InboundContactForm.d.ts","sourceRoot":"","sources":["../../../../../src/core/anonymous-session/InboundContactForm.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAyC,MAAM,OAAO,CAAC;AAE9D,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oCAAoC,CAAC;AAExF,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,qBAAqB,EAAE,MAAM,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB;AAgBD,MAAM,WAAW,uBAAuB;IACtC,yFAAyF;IACzF,MAAM,EAAE,6BAA6B,CAAC;IACtC,wFAAwF;IACxF,MAAM,CAAC,EAAE,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAC3C,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B;AAUD,wBAAgB,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,uBAAuB,GAAG,KAAK,CAAC,YAAY,CA6F/G"}
@@ -11,8 +11,8 @@
11
11
  *
12
12
  * Mirrors the non-excluded, non-backend-only CREATE inputs of `InboundContact`
13
13
  * (`email` required; `firstName` / `lastName` / `phone` optional), plus ONE privacy
14
- * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.dataProcessingConsent`
15
- * tier). There is no second, required agreement field: a governed notice, contract or
14
+ * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.consentedPurposes` set).
15
+ * There is no second, required agreement field: a governed notice, contract or
16
16
  * consent interaction must use the separate exact-release receipt contract, because this
17
17
  * DTO cannot manufacture legal evidence from a checkbox and a client timestamp.
18
18
  * `relatedUserId` is intentionally absent — it is `.excludeFromCreate()` on the
@@ -34,7 +34,7 @@ export declare const InboundContactFormSchema: z.ZodObject<{
34
34
  export type InboundContactFormValues = z.infer<typeof InboundContactFormSchema>;
35
35
  /**
36
36
  * Build the exact JSON body the backend CREATE expects from validated form values.
37
- * Keeps the `privacy` nesting + the consent enum mapping in ONE place so the form
37
+ * Keeps the `privacy` nesting + the consent purpose mapping in ONE place so the form
38
38
  * component stays declarative.
39
39
  */
40
40
  export declare function buildInboundContactCreateBody(values: InboundContactFormValues): Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"inbound-contact-form.schema.d.ts","sourceRoot":"","sources":["../../../../src/core/anonymous-session/inbound-contact-form.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAanC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYvG"}
1
+ {"version":3,"file":"inbound-contact-form.schema.d.ts","sourceRoot":"","sources":["../../../../../src/core/anonymous-session/inbound-contact-form.schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAcnC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAcvG"}
@@ -11,14 +11,15 @@
11
11
  *
12
12
  * Mirrors the non-excluded, non-backend-only CREATE inputs of `InboundContact`
13
13
  * (`email` required; `firstName` / `lastName` / `phone` optional), plus ONE privacy
14
- * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.dataProcessingConsent`
15
- * tier). There is no second, required agreement field: a governed notice, contract or
14
+ * signal: an OPTIONAL marketing opt-in (mapped to the `privacy.consentedPurposes` set).
15
+ * There is no second, required agreement field: a governed notice, contract or
16
16
  * consent interaction must use the separate exact-release receipt contract, because this
17
17
  * DTO cannot manufacture legal evidence from a checkbox and a client timestamp.
18
18
  * `relatedUserId` is intentionally absent — it is `.excludeFromCreate()` on the
19
19
  * resource (sales/admin-only, never client-authored).
20
20
  */
21
21
  import { z } from "zod";
22
+ import { ConsentPurpose } from "@wildo-ai/saas-models/public-runtime";
22
23
  /**
23
24
  * The wire payload POSTed to `/api/v1/inbound-contacts`. `privacy` matches the
24
25
  * resource's `InboundContactPrivacySchema` shape (a nested consent object) so the
@@ -32,15 +33,16 @@ export const InboundContactFormSchema = z.object({
32
33
  lastName: z.string().trim().max(200).optional(),
33
34
  phone: z.string().trim().max(64).optional(),
34
35
  /**
35
- * OPTIONAL marketing opt-in — maps into the resource's `privacy.dataProcessingConsent`
36
- * tier ('marketing' vs the BASIC default). It is the only privacy field on this form,
37
- * and it gates nothing: an unchecked box still submits, at the BASIC tier.
36
+ * OPTIONAL marketing opt-in — maps into the resource's `privacy.consentedPurposes` set.
37
+ * It is the only privacy field on this form, and it gates nothing: sending the form is the
38
+ * essential act and never depends on optional consent, so an unchecked box still submits,
39
+ * granting nothing.
38
40
  */
39
41
  marketingConsent: z.boolean().default(false),
40
42
  });
41
43
  /**
42
44
  * Build the exact JSON body the backend CREATE expects from validated form values.
43
- * Keeps the `privacy` nesting + the consent enum mapping in ONE place so the form
45
+ * Keeps the `privacy` nesting + the consent purpose mapping in ONE place so the form
44
46
  * component stays declarative.
45
47
  */
46
48
  export function buildInboundContactCreateBody(values) {
@@ -50,9 +52,11 @@ export function buildInboundContactCreateBody(values) {
50
52
  ...(values.lastName ? { lastName: values.lastName } : {}),
51
53
  ...(values.phone ? { phone: values.phone } : {}),
52
54
  privacy: {
53
- // OPTIONAL marketing tier: 'marketing' when the visitor opted in, else the
54
- // resource's BASIC default.
55
- dataProcessingConsent: values.marketingConsent ? "marketing" : "basic",
55
+ // The visitor SAW the opt-in and submitted, so this is a decision either way (#534): the set
56
+ // they granted — MARKETING or nothing — plus a CLAIM that they decided now. The backend's
57
+ // CREATE prefix replaces the instant with its own clock; a browser clock is never recorded.
58
+ consentedPurposes: values.marketingConsent ? [ConsentPurpose.MARKETING] : [],
59
+ consentDecidedAt: new Date().toISOString(),
56
60
  },
57
61
  };
58
62
  }