@wildo-ai/saas-website 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/LICENSE +34 -0
  2. package/dist/esm/.builder.pid +9 -0
  3. package/dist/esm/astro/blog-post-bridge.d.ts +324 -0
  4. package/dist/esm/astro/blog-post-bridge.d.ts.map +1 -0
  5. package/dist/esm/astro/blog-post-bridge.js +57 -0
  6. package/dist/esm/astro/blog-post-bridge.js.map +1 -0
  7. package/dist/esm/astro/blog-post-head.renderer.d.ts +129 -0
  8. package/dist/esm/astro/blog-post-head.renderer.d.ts.map +1 -0
  9. package/dist/esm/astro/blog-post-head.renderer.js +139 -0
  10. package/dist/esm/astro/blog-post-head.renderer.js.map +1 -0
  11. package/dist/esm/astro/bridge-runtime.d.ts +224 -0
  12. package/dist/esm/astro/bridge-runtime.d.ts.map +1 -0
  13. package/dist/esm/astro/bridge-runtime.js +30 -0
  14. package/dist/esm/astro/bridge-runtime.js.map +1 -0
  15. package/dist/esm/astro/collect-expected-label-keys.d.ts +65 -0
  16. package/dist/esm/astro/collect-expected-label-keys.d.ts.map +1 -0
  17. package/dist/esm/astro/collect-expected-label-keys.js +101 -0
  18. package/dist/esm/astro/collect-expected-label-keys.js.map +1 -0
  19. package/dist/esm/astro/headers-renderer.d.ts +32 -0
  20. package/dist/esm/astro/headers-renderer.d.ts.map +1 -0
  21. package/dist/esm/astro/headers-renderer.js +64 -0
  22. package/dist/esm/astro/headers-renderer.js.map +1 -0
  23. package/dist/esm/astro/i18n-routing.helper.d.ts +91 -0
  24. package/dist/esm/astro/i18n-routing.helper.d.ts.map +1 -0
  25. package/dist/esm/astro/i18n-routing.helper.js +23 -0
  26. package/dist/esm/astro/i18n-routing.helper.js.map +1 -0
  27. package/dist/esm/astro/internal/html-escape.d.ts +31 -0
  28. package/dist/esm/astro/internal/html-escape.d.ts.map +1 -0
  29. package/dist/esm/astro/internal/html-escape.js +40 -0
  30. package/dist/esm/astro/internal/html-escape.js.map +1 -0
  31. package/dist/esm/astro/label-pack-loader.d.ts +134 -0
  32. package/dist/esm/astro/label-pack-loader.d.ts.map +1 -0
  33. package/dist/esm/astro/label-pack-loader.js +298 -0
  34. package/dist/esm/astro/label-pack-loader.js.map +1 -0
  35. package/dist/esm/astro/llms-txt-server.d.ts +28 -0
  36. package/dist/esm/astro/llms-txt-server.d.ts.map +1 -0
  37. package/dist/esm/astro/llms-txt-server.js +33 -0
  38. package/dist/esm/astro/llms-txt-server.js.map +1 -0
  39. package/dist/esm/astro/pick-labels-for-page.d.ts +63 -0
  40. package/dist/esm/astro/pick-labels-for-page.d.ts.map +1 -0
  41. package/dist/esm/astro/pick-labels-for-page.js +73 -0
  42. package/dist/esm/astro/pick-labels-for-page.js.map +1 -0
  43. package/dist/esm/astro/robots-renderer.d.ts +85 -0
  44. package/dist/esm/astro/robots-renderer.d.ts.map +1 -0
  45. package/dist/esm/astro/robots-renderer.js +157 -0
  46. package/dist/esm/astro/robots-renderer.js.map +1 -0
  47. package/dist/esm/astro/sitemap-coverage.d.ts +101 -0
  48. package/dist/esm/astro/sitemap-coverage.d.ts.map +1 -0
  49. package/dist/esm/astro/sitemap-coverage.js +67 -0
  50. package/dist/esm/astro/sitemap-coverage.js.map +1 -0
  51. package/dist/esm/astro/structured-data.renderer.d.ts +21 -0
  52. package/dist/esm/astro/structured-data.renderer.d.ts.map +1 -0
  53. package/dist/esm/astro/structured-data.renderer.js +142 -0
  54. package/dist/esm/astro/structured-data.renderer.js.map +1 -0
  55. package/dist/esm/astro/website-page-head.renderer.d.ts +93 -0
  56. package/dist/esm/astro/website-page-head.renderer.d.ts.map +1 -0
  57. package/dist/esm/astro/website-page-head.renderer.js +184 -0
  58. package/dist/esm/astro/website-page-head.renderer.js.map +1 -0
  59. package/dist/esm/astro/website-page-runtime.helper.d.ts +112 -0
  60. package/dist/esm/astro/website-page-runtime.helper.d.ts.map +1 -0
  61. package/dist/esm/astro/website-page-runtime.helper.js +60 -0
  62. package/dist/esm/astro/website-page-runtime.helper.js.map +1 -0
  63. package/dist/esm/astro/website-site-context.d.ts +126 -0
  64. package/dist/esm/astro/website-site-context.d.ts.map +1 -0
  65. package/dist/esm/astro/website-site-context.js +37 -0
  66. package/dist/esm/astro/website-site-context.js.map +1 -0
  67. package/dist/esm/astro-island.d.ts +56 -0
  68. package/dist/esm/astro-island.d.ts.map +1 -0
  69. package/dist/esm/astro-island.js +61 -0
  70. package/dist/esm/astro-island.js.map +1 -0
  71. package/dist/esm/astro.d.ts +151 -0
  72. package/dist/esm/astro.d.ts.map +1 -0
  73. package/dist/esm/astro.js +151 -0
  74. package/dist/esm/astro.js.map +1 -0
  75. package/dist/esm/companion-exports.d.ts +38 -0
  76. package/dist/esm/companion-exports.d.ts.map +1 -0
  77. package/dist/esm/companion-exports.js +38 -0
  78. package/dist/esm/companion-exports.js.map +1 -0
  79. package/dist/esm/components/low-level/WebsiteButton.d.ts +73 -0
  80. package/dist/esm/components/low-level/WebsiteButton.d.ts.map +1 -0
  81. package/dist/esm/components/low-level/WebsiteButton.js +68 -0
  82. package/dist/esm/components/low-level/WebsiteButton.js.map +1 -0
  83. package/dist/esm/components/low-level/WebsiteHeading.d.ts +78 -0
  84. package/dist/esm/components/low-level/WebsiteHeading.d.ts.map +1 -0
  85. package/dist/esm/components/low-level/WebsiteHeading.js +41 -0
  86. package/dist/esm/components/low-level/WebsiteHeading.js.map +1 -0
  87. package/dist/esm/components/low-level/WebsiteImage.d.ts +73 -0
  88. package/dist/esm/components/low-level/WebsiteImage.d.ts.map +1 -0
  89. package/dist/esm/components/low-level/WebsiteImage.js +21 -0
  90. package/dist/esm/components/low-level/WebsiteImage.js.map +1 -0
  91. package/dist/esm/components/low-level/WebsiteInternalButton.d.ts +90 -0
  92. package/dist/esm/components/low-level/WebsiteInternalButton.d.ts.map +1 -0
  93. package/dist/esm/components/low-level/WebsiteInternalButton.js +47 -0
  94. package/dist/esm/components/low-level/WebsiteInternalButton.js.map +1 -0
  95. package/dist/esm/components/low-level/WebsiteInternalLink.d.ts +99 -0
  96. package/dist/esm/components/low-level/WebsiteInternalLink.d.ts.map +1 -0
  97. package/dist/esm/components/low-level/WebsiteInternalLink.js +68 -0
  98. package/dist/esm/components/low-level/WebsiteInternalLink.js.map +1 -0
  99. package/dist/esm/components/low-level/WebsiteLink.d.ts +50 -0
  100. package/dist/esm/components/low-level/WebsiteLink.d.ts.map +1 -0
  101. package/dist/esm/components/low-level/WebsiteLink.js +22 -0
  102. package/dist/esm/components/low-level/WebsiteLink.js.map +1 -0
  103. package/dist/esm/components/low-level/WebsiteText.d.ts +57 -0
  104. package/dist/esm/components/low-level/WebsiteText.d.ts.map +1 -0
  105. package/dist/esm/components/low-level/WebsiteText.js +57 -0
  106. package/dist/esm/components/low-level/WebsiteText.js.map +1 -0
  107. package/dist/esm/config/define-website-config.d.ts +83 -0
  108. package/dist/esm/config/define-website-config.d.ts.map +1 -0
  109. package/dist/esm/config/define-website-config.js +126 -0
  110. package/dist/esm/config/define-website-config.js.map +1 -0
  111. package/dist/esm/config/index.d.ts +38 -0
  112. package/dist/esm/config/index.d.ts.map +1 -0
  113. package/dist/esm/config/index.js +38 -0
  114. package/dist/esm/config/index.js.map +1 -0
  115. package/dist/esm/config/load-website-config.d.ts +136 -0
  116. package/dist/esm/config/load-website-config.d.ts.map +1 -0
  117. package/dist/esm/config/load-website-config.js +177 -0
  118. package/dist/esm/config/load-website-config.js.map +1 -0
  119. package/dist/esm/config/wildo-website-config.schemas.d.ts +251 -0
  120. package/dist/esm/config/wildo-website-config.schemas.d.ts.map +1 -0
  121. package/dist/esm/config/wildo-website-config.schemas.js +302 -0
  122. package/dist/esm/config/wildo-website-config.schemas.js.map +1 -0
  123. package/dist/esm/config-loader.d.ts +35 -0
  124. package/dist/esm/config-loader.d.ts.map +1 -0
  125. package/dist/esm/config-loader.js +35 -0
  126. package/dist/esm/config-loader.js.map +1 -0
  127. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts +38 -0
  128. package/dist/esm/core/anonymous-session/InboundContactForm.d.ts.map +1 -0
  129. package/dist/esm/core/anonymous-session/InboundContactForm.js +77 -0
  130. package/dist/esm/core/anonymous-session/InboundContactForm.js.map +1 -0
  131. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts +41 -0
  132. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.d.ts.map +1 -0
  133. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js +58 -0
  134. package/dist/esm/core/anonymous-session/inbound-contact-form.schema.js.map +1 -0
  135. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts +81 -0
  136. package/dist/esm/core/anonymous-session/website-anonymous-session-client.d.ts.map +1 -0
  137. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js +177 -0
  138. package/dist/esm/core/anonymous-session/website-anonymous-session-client.js.map +1 -0
  139. package/dist/esm/core/contexts/WebsitePageContext.d.ts +40 -0
  140. package/dist/esm/core/contexts/WebsitePageContext.d.ts.map +1 -0
  141. package/dist/esm/core/contexts/WebsitePageContext.js +8 -0
  142. package/dist/esm/core/contexts/WebsitePageContext.js.map +1 -0
  143. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts +162 -0
  144. package/dist/esm/core/contexts/WebsiteRuntimeContext.d.ts.map +1 -0
  145. package/dist/esm/core/contexts/WebsiteRuntimeContext.js +22 -0
  146. package/dist/esm/core/contexts/WebsiteRuntimeContext.js.map +1 -0
  147. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts +47 -0
  148. package/dist/esm/core/contexts/WebsiteSectionContext.d.ts.map +1 -0
  149. package/dist/esm/core/contexts/WebsiteSectionContext.js +8 -0
  150. package/dist/esm/core/contexts/WebsiteSectionContext.js.map +1 -0
  151. package/dist/esm/core/contexts/useWebsitePage.d.ts +13 -0
  152. package/dist/esm/core/contexts/useWebsitePage.d.ts.map +1 -0
  153. package/dist/esm/core/contexts/useWebsitePage.js +22 -0
  154. package/dist/esm/core/contexts/useWebsitePage.js.map +1 -0
  155. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts +19 -0
  156. package/dist/esm/core/contexts/useWebsiteRuntime.d.ts.map +1 -0
  157. package/dist/esm/core/contexts/useWebsiteRuntime.js +28 -0
  158. package/dist/esm/core/contexts/useWebsiteRuntime.js.map +1 -0
  159. package/dist/esm/core/contexts/useWebsiteSection.d.ts +12 -0
  160. package/dist/esm/core/contexts/useWebsiteSection.d.ts.map +1 -0
  161. package/dist/esm/core/contexts/useWebsiteSection.js +22 -0
  162. package/dist/esm/core/contexts/useWebsiteSection.js.map +1 -0
  163. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts +17 -0
  164. package/dist/esm/core/external-providers/frontend-provider-registry.website.d.ts.map +1 -0
  165. package/dist/esm/core/external-providers/frontend-provider-registry.website.js +23 -0
  166. package/dist/esm/core/external-providers/frontend-provider-registry.website.js.map +1 -0
  167. package/dist/esm/core/factories/define-website-page-manifest.d.ts +70 -0
  168. package/dist/esm/core/factories/define-website-page-manifest.d.ts.map +1 -0
  169. package/dist/esm/core/factories/define-website-page-manifest.js +51 -0
  170. package/dist/esm/core/factories/define-website-page-manifest.js.map +1 -0
  171. package/dist/esm/core/factories/define-website-section.d.ts +101 -0
  172. package/dist/esm/core/factories/define-website-section.d.ts.map +1 -0
  173. package/dist/esm/core/factories/define-website-section.js +71 -0
  174. package/dist/esm/core/factories/define-website-section.js.map +1 -0
  175. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts +23 -0
  176. package/dist/esm/core/hooks/useWebsiteDesignTokens.d.ts.map +1 -0
  177. package/dist/esm/core/hooks/useWebsiteDesignTokens.js +25 -0
  178. package/dist/esm/core/hooks/useWebsiteDesignTokens.js.map +1 -0
  179. package/dist/esm/core/hooks/useWebsiteLabel.d.ts +68 -0
  180. package/dist/esm/core/hooks/useWebsiteLabel.d.ts.map +1 -0
  181. package/dist/esm/core/hooks/useWebsiteLabel.js +103 -0
  182. package/dist/esm/core/hooks/useWebsiteLabel.js.map +1 -0
  183. package/dist/esm/core/layouts/WebsitePageLayout.d.ts +87 -0
  184. package/dist/esm/core/layouts/WebsitePageLayout.d.ts.map +1 -0
  185. package/dist/esm/core/layouts/WebsitePageLayout.js +170 -0
  186. package/dist/esm/core/layouts/WebsitePageLayout.js.map +1 -0
  187. package/dist/esm/core/layouts/WebsiteSection.d.ts +91 -0
  188. package/dist/esm/core/layouts/WebsiteSection.d.ts.map +1 -0
  189. package/dist/esm/core/layouts/WebsiteSection.js +26 -0
  190. package/dist/esm/core/layouts/WebsiteSection.js.map +1 -0
  191. package/dist/esm/core/routing/internal-routing.utils.d.ts +65 -0
  192. package/dist/esm/core/routing/internal-routing.utils.d.ts.map +1 -0
  193. package/dist/esm/core/routing/internal-routing.utils.js +14 -0
  194. package/dist/esm/core/routing/internal-routing.utils.js.map +1 -0
  195. package/dist/esm/index.d.ts +129 -0
  196. package/dist/esm/index.d.ts.map +1 -0
  197. package/dist/esm/index.js +129 -0
  198. package/dist/esm/index.js.map +1 -0
  199. package/dist/esm/mdx/BlogPost.d.ts +193 -0
  200. package/dist/esm/mdx/BlogPost.d.ts.map +1 -0
  201. package/dist/esm/mdx/BlogPost.js +161 -0
  202. package/dist/esm/mdx/BlogPost.js.map +1 -0
  203. package/dist/esm/mdx/blog-post-collection.config.d.ts +139 -0
  204. package/dist/esm/mdx/blog-post-collection.config.d.ts.map +1 -0
  205. package/dist/esm/mdx/blog-post-collection.config.js +27 -0
  206. package/dist/esm/mdx/blog-post-collection.config.js.map +1 -0
  207. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts +12 -0
  208. package/dist/esm/mdx/blog-post-frontmatter-source.d.ts.map +1 -0
  209. package/dist/esm/mdx/blog-post-frontmatter-source.js +13 -0
  210. package/dist/esm/mdx/blog-post-frontmatter-source.js.map +1 -0
  211. package/dist/esm/mdx/index.d.ts +22 -0
  212. package/dist/esm/mdx/index.d.ts.map +1 -0
  213. package/dist/esm/mdx/index.js +22 -0
  214. package/dist/esm/mdx/index.js.map +1 -0
  215. package/dist/esm/mdx/website-mdx-provider.d.ts +90 -0
  216. package/dist/esm/mdx/website-mdx-provider.d.ts.map +1 -0
  217. package/dist/esm/mdx/website-mdx-provider.js +8 -0
  218. package/dist/esm/mdx/website-mdx-provider.js.map +1 -0
  219. package/dist/esm/mdx.d.ts +38 -0
  220. package/dist/esm/mdx.d.ts.map +1 -0
  221. package/dist/esm/mdx.js +38 -0
  222. package/dist/esm/mdx.js.map +1 -0
  223. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts +75 -0
  224. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.d.ts.map +1 -0
  225. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js +85 -0
  226. package/dist/esm/schemas/blog/blog-post-frontmatter.shared.schemas.js.map +1 -0
  227. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts +120 -0
  228. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.d.ts.map +1 -0
  229. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js +152 -0
  230. package/dist/esm/schemas/design-tokens/website-design-tokens.shared.schemas.js.map +1 -0
  231. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts +73 -0
  232. package/dist/esm/schemas/label-keys/website-label-key.schemas.d.ts.map +1 -0
  233. package/dist/esm/schemas/label-keys/website-label-key.schemas.js +76 -0
  234. package/dist/esm/schemas/label-keys/website-label-key.schemas.js.map +1 -0
  235. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts +209 -0
  236. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.d.ts.map +1 -0
  237. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js +218 -0
  238. package/dist/esm/schemas/manifests/website-page-manifest.shared.schemas.js.map +1 -0
  239. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts +431 -0
  240. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.d.ts.map +1 -0
  241. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js +438 -0
  242. package/dist/esm/schemas/manifests/website-root-config.shared.schemas.js.map +1 -0
  243. package/dist/esm/schemas/refs/page-ref.schemas.d.ts +40 -0
  244. package/dist/esm/schemas/refs/page-ref.schemas.d.ts.map +1 -0
  245. package/dist/esm/schemas/refs/page-ref.schemas.js +43 -0
  246. package/dist/esm/schemas/refs/page-ref.schemas.js.map +1 -0
  247. package/dist/esm/schemas/refs/section-ref.schemas.d.ts +41 -0
  248. package/dist/esm/schemas/refs/section-ref.schemas.d.ts.map +1 -0
  249. package/dist/esm/schemas/refs/section-ref.schemas.js +44 -0
  250. package/dist/esm/schemas/refs/section-ref.schemas.js.map +1 -0
  251. package/dist/esm/schemas/sections/website-section-category.shared.d.ts +63 -0
  252. package/dist/esm/schemas/sections/website-section-category.shared.d.ts.map +1 -0
  253. package/dist/esm/schemas/sections/website-section-category.shared.js +64 -0
  254. package/dist/esm/schemas/sections/website-section-category.shared.js.map +1 -0
  255. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts +114 -0
  256. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.d.ts.map +1 -0
  257. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js +69 -0
  258. package/dist/esm/schemas/sections/website-section-definition.shared.schemas.js.map +1 -0
  259. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts +100 -0
  260. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.d.ts.map +1 -0
  261. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js +355 -0
  262. package/dist/esm/schemas/structured-data/structured-data-reconciliation.shared.js.map +1 -0
  263. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts +58 -0
  264. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.d.ts.map +1 -0
  265. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js +2 -0
  266. package/dist/esm/schemas/structured-data/website-structured-data.shared.schemas.js.map +1 -0
  267. package/dist/esm/schemas/validators/label-pack.validator.d.ts +144 -0
  268. package/dist/esm/schemas/validators/label-pack.validator.d.ts.map +1 -0
  269. package/dist/esm/schemas/validators/label-pack.validator.js +100 -0
  270. package/dist/esm/schemas/validators/label-pack.validator.js.map +1 -0
  271. package/dist/tsconfig.build.tsbuildinfo +1 -0
  272. package/package.json +143 -0
  273. package/src/__tests__/bundle-isolation.test.ts +607 -0
  274. package/src/astro/__tests__/blog-post-bridge.test.tsx +288 -0
  275. package/src/astro/__tests__/blog-post-head.renderer.test.ts +249 -0
  276. package/src/astro/__tests__/bridge-runtime.test.tsx +198 -0
  277. package/src/astro/__tests__/collect-expected-label-keys.test.ts +69 -0
  278. package/src/astro/__tests__/headers-renderer.test.ts +143 -0
  279. package/src/astro/__tests__/i18n-routing.helper.test.ts +116 -0
  280. package/src/astro/__tests__/label-pack-loader.test.ts +305 -0
  281. package/src/astro/__tests__/llms-txt-server.test.ts +127 -0
  282. package/src/astro/__tests__/pick-labels-for-page.test.ts +60 -0
  283. package/src/astro/__tests__/robots-renderer.test.ts +228 -0
  284. package/src/astro/__tests__/sitemap-coverage.test.ts +230 -0
  285. package/src/astro/__tests__/structured-data.renderer.test.ts +172 -0
  286. package/src/astro/__tests__/website-page-head.renderer.test.ts +424 -0
  287. package/src/astro/__tests__/website-page-runtime.helper.test.ts +157 -0
  288. package/src/astro/__tests__/website-site-context.test.ts +121 -0
  289. package/src/astro/blog-post-bridge.tsx +404 -0
  290. package/src/astro/blog-post-head.renderer.ts +293 -0
  291. package/src/astro/bridge-runtime.tsx +270 -0
  292. package/src/astro/collect-expected-label-keys.ts +113 -0
  293. package/src/astro/headers-renderer.ts +82 -0
  294. package/src/astro/i18n-routing.helper.ts +116 -0
  295. package/src/astro/internal/html-escape.ts +41 -0
  296. package/src/astro/label-pack-loader.ts +385 -0
  297. package/src/astro/llms-txt-server.ts +56 -0
  298. package/src/astro/pick-labels-for-page.ts +85 -0
  299. package/src/astro/robots-renderer.ts +171 -0
  300. package/src/astro/sitemap-coverage.ts +186 -0
  301. package/src/astro/structured-data.renderer.ts +171 -0
  302. package/src/astro/website-page-head.renderer.ts +279 -0
  303. package/src/astro/website-page-runtime.helper.ts +188 -0
  304. package/src/astro/website-site-context.ts +136 -0
  305. package/src/astro-island.ts +61 -0
  306. package/src/astro.ts +151 -0
  307. package/src/companion-exports.ts +38 -0
  308. package/src/components/low-level/WebsiteButton.tsx +118 -0
  309. package/src/components/low-level/WebsiteHeading.tsx +96 -0
  310. package/src/components/low-level/WebsiteImage.tsx +96 -0
  311. package/src/components/low-level/WebsiteInternalButton.tsx +172 -0
  312. package/src/components/low-level/WebsiteInternalLink.tsx +193 -0
  313. package/src/components/low-level/WebsiteLink.tsx +71 -0
  314. package/src/components/low-level/WebsiteText.tsx +75 -0
  315. package/src/components/low-level/__tests__/WebsiteInternalButton.test.tsx +169 -0
  316. package/src/components/low-level/__tests__/WebsiteInternalLink.test.tsx +202 -0
  317. package/src/components/low-level/__tests__/primitives.test.tsx +141 -0
  318. package/src/config/__tests__/define-website-config.test.ts +231 -0
  319. package/src/config/__tests__/load-website-config.test.ts +215 -0
  320. package/src/config/define-website-config.ts +134 -0
  321. package/src/config/index.ts +38 -0
  322. package/src/config/load-website-config.ts +229 -0
  323. package/src/config/wildo-website-config.schemas.ts +322 -0
  324. package/src/config-loader.ts +35 -0
  325. package/src/core/__tests__/WebsitePageLayout.test.tsx +184 -0
  326. package/src/core/__tests__/WebsiteSection.test.tsx +85 -0
  327. package/src/core/__tests__/contexts.test.tsx +133 -0
  328. package/src/core/__tests__/core-boundary.test.ts +85 -0
  329. package/src/core/__tests__/define-website-page-manifest.test.tsx +111 -0
  330. package/src/core/__tests__/define-website-section.test.tsx +89 -0
  331. package/src/core/__tests__/useWebsiteDesignTokens.test.tsx +41 -0
  332. package/src/core/__tests__/useWebsiteLabel.test.tsx +117 -0
  333. package/src/core/anonymous-session/InboundContactForm.tsx +157 -0
  334. package/src/core/anonymous-session/__tests__/inbound-contact-form.schema.test.ts +44 -0
  335. package/src/core/anonymous-session/inbound-contact-form.schema.ts +62 -0
  336. package/src/core/anonymous-session/website-anonymous-session-client.ts +194 -0
  337. package/src/core/contexts/WebsitePageContext.tsx +52 -0
  338. package/src/core/contexts/WebsiteRuntimeContext.tsx +175 -0
  339. package/src/core/contexts/WebsiteSectionContext.tsx +59 -0
  340. package/src/core/contexts/useWebsitePage.ts +28 -0
  341. package/src/core/contexts/useWebsiteRuntime.ts +34 -0
  342. package/src/core/contexts/useWebsiteSection.ts +28 -0
  343. package/src/core/external-providers/frontend-provider-registry.website.ts +62 -0
  344. package/src/core/factories/define-website-page-manifest.ts +102 -0
  345. package/src/core/factories/define-website-section.ts +128 -0
  346. package/src/core/hooks/useWebsiteDesignTokens.ts +26 -0
  347. package/src/core/hooks/useWebsiteLabel.ts +121 -0
  348. package/src/core/layouts/WebsitePageLayout.tsx +311 -0
  349. package/src/core/layouts/WebsiteSection.tsx +139 -0
  350. package/src/core/routing/__tests__/internal-routing.utils.test.ts +134 -0
  351. package/src/core/routing/internal-routing.utils.ts +77 -0
  352. package/src/index.ts +132 -0
  353. package/src/mdx/BlogPost.tsx +296 -0
  354. package/src/mdx/__tests__/BlogPost.test.tsx +218 -0
  355. package/src/mdx/__tests__/blog-post-collection.config.test.ts +127 -0
  356. package/src/mdx/__tests__/website-mdx-provider.test.tsx +82 -0
  357. package/src/mdx/blog-post-collection.config.ts +177 -0
  358. package/src/mdx/blog-post-frontmatter-source.ts +31 -0
  359. package/src/mdx/index.ts +39 -0
  360. package/src/mdx/website-mdx-provider.tsx +95 -0
  361. package/src/mdx.ts +38 -0
  362. package/src/schemas/__tests__/blog-frontmatter.test.ts +50 -0
  363. package/src/schemas/__tests__/companion-exports-react-free.test.ts +51 -0
  364. package/src/schemas/__tests__/design-tokens.test.ts +163 -0
  365. package/src/schemas/__tests__/label-key.test.ts +46 -0
  366. package/src/schemas/__tests__/label-pack-validator.test.ts +146 -0
  367. package/src/schemas/__tests__/page-manifest.test.ts +66 -0
  368. package/src/schemas/__tests__/refs.test.ts +37 -0
  369. package/src/schemas/__tests__/root-config.test.ts +292 -0
  370. package/src/schemas/__tests__/section-definition.test.ts +47 -0
  371. package/src/schemas/__tests__/structured-data-reconciliation.test.ts +543 -0
  372. package/src/schemas/blog/blog-post-frontmatter.shared.schemas.ts +111 -0
  373. package/src/schemas/design-tokens/website-design-tokens.shared.schemas.ts +166 -0
  374. package/src/schemas/label-keys/website-label-key.schemas.ts +88 -0
  375. package/src/schemas/manifests/website-page-manifest.shared.schemas.ts +266 -0
  376. package/src/schemas/manifests/website-root-config.shared.schemas.ts +504 -0
  377. package/src/schemas/refs/page-ref.schemas.ts +49 -0
  378. package/src/schemas/refs/section-ref.schemas.ts +50 -0
  379. package/src/schemas/sections/website-section-category.shared.ts +65 -0
  380. package/src/schemas/sections/website-section-definition.shared.schemas.ts +120 -0
  381. package/src/schemas/structured-data/structured-data-reconciliation.shared.ts +579 -0
  382. package/src/schemas/structured-data/website-structured-data.shared.schemas.ts +62 -0
  383. package/src/schemas/validators/label-pack.validator.ts +201 -0
@@ -0,0 +1,82 @@
1
+ import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';
2
+ import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
3
+
4
+ import type { WebsiteSiteContext } from './website-site-context';
5
+
6
+ const NOAI_ROBOTS_HEADER_VALUE = 'noai, noimageai' as const;
7
+
8
+ /**
9
+ * @wildo_source:part:start saas.website.astro.headers-renderer facet:layer:astro facet:family:website
10
+ *
11
+ * Render a static-host-compatible `_headers` body for every page
12
+ * manifest whose `seo.noaiOptOut === true`.
13
+ *
14
+ * The output format matches Cloudflare Pages / Netlify / Vercel's
15
+ * path-scoped `_headers` conventions:
16
+ *
17
+ * ```txt
18
+ * /pricing
19
+ * X-Robots-Tag: noai, noimageai
20
+ *
21
+ * /fr/pricing
22
+ * X-Robots-Tag: noai, noimageai
23
+ * ```
24
+ */
25
+ export function renderWebsitePageNoaiHeaders(siteContext: WebsiteSiteContext): string {
26
+ const flaggedPages = siteContext.rootConfig.pageManifests.filter(
27
+ (manifest) => manifest.seo?.noaiOptOut === true,
28
+ );
29
+ if (flaggedPages.length === 0) {
30
+ return '';
31
+ }
32
+
33
+ const lines: string[] = [];
34
+ for (const manifest of flaggedPages) {
35
+ appendManifestHeaders(lines, manifest, siteContext);
36
+ }
37
+ return lines.slice(0, -1).join('\n');
38
+ }
39
+ /** @wildo_source:part:end saas.website.astro.headers-renderer */
40
+
41
+ /**
42
+ * Future blog-post companion for the same header channel. Kept small
43
+ * and pure so a consumer's `[slug].astro` route or `_headers`
44
+ * generation script can opt blog posts out of AI training without
45
+ * re-implementing the locale-path logic.
46
+ */
47
+ export function renderBlogPostNoaiHeaders(params: {
48
+ siteContext: WebsiteSiteContext;
49
+ routePath: string;
50
+ noaiOptOut?: boolean;
51
+ }): string {
52
+ const { siteContext, routePath, noaiOptOut } = params;
53
+ if (noaiOptOut !== true) {
54
+ return '';
55
+ }
56
+
57
+ const lines: string[] = [];
58
+ for (const { locale } of siteContext.websiteConfig.locales) {
59
+ const localizedPath = buildLocalizedRoutePath({
60
+ locale,
61
+ defaultLocale: siteContext.websiteConfig.defaultLocale,
62
+ routePath,
63
+ });
64
+ lines.push(localizedPath, ` X-Robots-Tag: ${NOAI_ROBOTS_HEADER_VALUE}`, '');
65
+ }
66
+ return lines.slice(0, -1).join('\n');
67
+ }
68
+
69
+ function appendManifestHeaders(
70
+ lines: string[],
71
+ manifest: WebsitePageManifest,
72
+ siteContext: WebsiteSiteContext,
73
+ ): void {
74
+ for (const { locale } of siteContext.websiteConfig.locales) {
75
+ const localizedPath = buildLocalizedRoutePath({
76
+ locale,
77
+ defaultLocale: siteContext.websiteConfig.defaultLocale,
78
+ routePath: String(manifest.routePath),
79
+ });
80
+ lines.push(localizedPath, ` X-Robots-Tag: ${NOAI_ROBOTS_HEADER_VALUE}`, '');
81
+ }
82
+ }
@@ -0,0 +1,116 @@
1
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
+
3
+ import type { WebsiteSiteContext } from './website-site-context';
4
+
5
+ /**
6
+ * @wildo_source:part:start saas.website.astro.i18n-helper facet:layer:astro facet:family:website
7
+ *
8
+ * `buildAstroI18nConfig(siteContext)` — derives the `i18n` block consumed
9
+ * by `astro/config`'s `defineConfig({ i18n })` from the shared
10
+ * `WebsiteSiteContext`.
11
+ *
12
+ * **Why a helper and not hand-authored in `astro.config.mjs`**: a
13
+ * marketing site has TWO sources of truth for "which locales does this
14
+ * site ship?" — the `WildoMarketingWebsiteConfig.locales` array
15
+ * (consumed by every Astro build helper) AND the Astro routing config
16
+ * (consumed by the file-based router and the dev server). Letting the
17
+ * consumer hand-author both creates a silent-drift surface: a locale
18
+ * added to one but not the other ships a working dev server with a
19
+ * missing label pack (or vice versa). This helper makes
20
+ * `WildoMarketingWebsiteConfig` (the per-service `wildo.website.config.ts`)
21
+ * the single source of truth.
22
+ *
23
+ * **Design decisions** (each carrying its own semantic weight):
24
+ *
25
+ * - **`prefixDefaultLocale = false`**. Astro's default routing strategy
26
+ * is the marketing-site convention: the default locale mounts at the
27
+ * un-prefixed path (`/`, `/pricing`, `/contact`) and other locales
28
+ * live under `/<locale>/…`. Forcing prefixes for the default locale
29
+ * would surface a `/en/` URL alongside `/` — duplicate content from
30
+ * an SEO standpoint and a UX wart. Marketing teams overwhelmingly
31
+ * ship the default at the root.
32
+ *
33
+ * - **`redirectToDefaultLocale = false`**. The default value Astro
34
+ * would otherwise infer (`true`) issues a redirect from `/` to
35
+ * `/<defaultLocale>/`, which directly contradicts the
36
+ * `prefixDefaultLocale = false` convention above. Setting both
37
+ * explicitly makes the contract loud at the call site (and in the
38
+ * compiled config) instead of relying on Astro's defaults staying
39
+ * stable across versions.
40
+ *
41
+ * - **`fallbackType = 'redirect'` is INTENTIONALLY OMITTED**.
42
+ * Marketing sites typically prefer one of two strategies: ship a
43
+ * page in every advertised locale OR hide the un-shipped locale
44
+ * entirely (no entry in `locales`). The `fallback` machinery (which
45
+ * would redirect `/de/pricing` to `/en/pricing` when the German
46
+ * page is missing) creates SEO duplicate-content risk that's hard
47
+ * to undo once Google indexes the redirected URL. Consumers who
48
+ * genuinely need fallback can spread this helper's output and
49
+ * add `fallback: { … }` themselves.
50
+ *
51
+ * **Type rationale**: the return type uses Astro's published `i18n`
52
+ * shape but we declare it as a structural literal type rather than
53
+ * importing from `astro` so this module stays in the saas-website
54
+ * source closure without forcing `astro` into the React-island side of the
55
+ * bundle. The package's bundle-boundary checks enforce that separation.
56
+ * The structural type is straightforward and matches Astro 4+ — see
57
+ * https://docs.astro.build/en/reference/configuration-reference/#i18n.
58
+ *
59
+ * @example
60
+ * // astro.config.mjs
61
+ * import { defineConfig } from 'astro/config';
62
+ * import { buildAstroI18nConfig } from '@wildo-ai/saas-website/astro';
63
+ * import { WEBSITE_SITE_CONTEXT } from './src/website-site-context';
64
+ *
65
+ * export default defineConfig({
66
+ * i18n: buildAstroI18nConfig(WEBSITE_SITE_CONTEXT),
67
+ * });
68
+ */
69
+ export interface AstroI18nConfig {
70
+ /** ISO locale string Astro mounts at the un-prefixed path. */
71
+ defaultLocale: string;
72
+ /** Every locale the site ships in (default included). */
73
+ locales: readonly string[];
74
+ routing: {
75
+ /**
76
+ * `false` means the default locale is served at the root path
77
+ * (`/`); other locales sit under `/<locale>/`. Typed as a
78
+ * widened `boolean` (not a literal `false`) so consumers who
79
+ * spread the config and override the field downstream do not
80
+ * fight literal-type narrowing — the runtime still emits
81
+ * `false` here, the type system just allows the override.
82
+ */
83
+ prefixDefaultLocale: boolean;
84
+ /**
85
+ * `false` means a request for `/` is served as the default-locale
86
+ * landing page (no implicit redirect to `/<defaultLocale>/`). Same
87
+ * literal-vs-widened rationale as `prefixDefaultLocale` above.
88
+ */
89
+ redirectToDefaultLocale: boolean;
90
+ };
91
+ }
92
+
93
+ export function buildAstroI18nConfig(siteContext: WebsiteSiteContext): AstroI18nConfig {
94
+ const { websiteConfig } = siteContext;
95
+ /**
96
+ * Defensive copy: `WildoMarketingWebsiteConfig.locales` is an array
97
+ * of `{ locale, path }` entries; Astro's i18n config wants raw
98
+ * locale strings in declaration order. We extract `locale` and map
99
+ * through `String()` so the `AvailableLanguage` brand layer does
100
+ * not leak into the Astro public API surface, and we freeze the
101
+ * result so an Astro plugin authored against this shape cannot
102
+ * mutate the source-of-truth array indirectly.
103
+ */
104
+ const locales: readonly string[] = Object.freeze(
105
+ websiteConfig.locales.map((entry: { locale: AvailableLanguage }) => String(entry.locale)),
106
+ );
107
+ return {
108
+ defaultLocale: String(websiteConfig.defaultLocale),
109
+ locales,
110
+ routing: {
111
+ prefixDefaultLocale: false,
112
+ redirectToDefaultLocale: false,
113
+ },
114
+ };
115
+ }
116
+ /** @wildo_source:part:end saas.website.astro.i18n-helper */
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared HTML-escape helpers used by every Astro head-block string
3
+ * builder in the saas-website engine (page-head + blog-post-head, and
4
+ * any future per-route renderer that interpolates consumer-supplied
5
+ * copy into a string-output `<head>` template).
6
+ *
7
+ * # Why a shared module rather than duplicating per-renderer
8
+ *
9
+ * Marketing copy frequently contains characters (quotes, ampersands,
10
+ * the rare `<`) that would break the surrounding tag if interpolated
11
+ * raw. Two renderers escaping with two slightly-different character
12
+ * sets is exactly the kind of silent-drift surface this codebase
13
+ * forbids — one renderer might forget the `'` → `&#39;` substitution
14
+ * and produce attribute breakouts in JSON-LD copy, while the other
15
+ * does it correctly. Centralizing both helpers here gives every
16
+ * renderer ONE escape contract to audit.
17
+ *
18
+ * # Why both `Attribute` and `Text` variants exist (currently identical)
19
+ *
20
+ * Conceptually distinct (an attribute escape needs `"`/`'` covered;
21
+ * a text-content escape technically only needs `<`/`>`/`&`). Today
22
+ * they share a body for convenience — the cost of over-escaping `"`
23
+ * inside text content is negligible (browsers render the entity
24
+ * correctly) and the win is one fewer code path to audit. Splitting
25
+ * the implementations later (when a real divergence shows up — e.g. a
26
+ * JSON-LD emitter that legitimately wants raw `<script type="application/ld+json">`
27
+ * inner-content escaping rules) is a one-file change.
28
+ */
29
+
30
+ export function escapeHtmlAttribute(value: string): string {
31
+ return value
32
+ .replace(/&/g, '&amp;')
33
+ .replace(/</g, '&lt;')
34
+ .replace(/>/g, '&gt;')
35
+ .replace(/"/g, '&quot;')
36
+ .replace(/'/g, '&#39;');
37
+ }
38
+
39
+ export function escapeHtmlText(value: string): string {
40
+ return escapeHtmlAttribute(value);
41
+ }
@@ -0,0 +1,385 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
5
+
6
+ import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
7
+ import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
8
+ import type { WebsiteRootConfig } from '../schemas/manifests/website-root-config.shared.schemas';
9
+ import {
10
+ validateLabelPackAgainstManifest,
11
+ type ExpectedLabelKeysBySectionRef,
12
+ type LabelPack,
13
+ type LabelPackValidationResult,
14
+ } from '../schemas/validators/label-pack.validator';
15
+
16
+ import type { WebsiteSiteContext } from './website-site-context';
17
+
18
+ /**
19
+ * @wildo_source:part:start saas.website.astro.label-loader facet:layer:astro facet:family:website
20
+ *
21
+ * `label-pack-loader.ts` — synchronous, fs-backed loader for
22
+ * marketing-site label packs. Consumed by:
23
+ * - the Astro adapter (`WebsitePageBridge.astro` reads the active
24
+ * locale's pack at request time during SSR / pre-render),
25
+ * - the `wildo website audit` CLI (Phase 7) to validate every
26
+ * locale pack against every manifest at CI time,
27
+ * - any build-time hook that wants to fail fast on a missing or
28
+ * malformed pack.
29
+ *
30
+ * **Convention** (matches the slice plan + the validator's contract):
31
+ *
32
+ * - One file per locale: `<packsDir>/<locale>.json`
33
+ * - JSON object: flat key-value map, `Record<string, string>`
34
+ * - Keys are fully-composed `WebsiteLabelKey`s (`website.<page>.<section>.<slot>`)
35
+ * - Values are the localized strings
36
+ *
37
+ * **Why synchronous I/O**:
38
+ * - The Astro frontmatter executes at build / SSR time in a Node
39
+ * process (no event loop pressure to relieve), and the bridge
40
+ * wants the pack resolved before it emits any HTML — async would
41
+ * force the consumer to `await` inside an Astro frontmatter,
42
+ * which is supported but adds a needless yield-point on a single
43
+ * small JSON file.
44
+ * - The validator is pure and synchronous; making the loader async
45
+ * would add a coloring boundary for no benefit.
46
+ *
47
+ * **Failure modes — all surfaced as thrown errors with actionable
48
+ * messages**:
49
+ * - File missing — wrapped in a `[label-pack-loader] Could not read
50
+ * pack at <path>` error so a typo in the dir / locale is obvious.
51
+ * - JSON parse failure — error includes the file path and the
52
+ * parser's own message.
53
+ * - Top-level shape is not an object, or a value is not a string —
54
+ * error names the offending key + observed type.
55
+ *
56
+ * **Why three exports** (loader, multi-loader, validator-runner):
57
+ * - `loadLabelPack(packsDir, locale)` is the atomic primitive
58
+ * (one file → one validated `LabelPack`). Used by the bridge at
59
+ * request time.
60
+ * - `loadAllLabelPacks(packsDir, siteContext)` is the build-time
61
+ * batch — every locale pack at once. Used by the CLI auditor
62
+ * and by the build-start hook.
63
+ * - `validateAllLabelPacks(packsDir, siteContext, expectedKeysBySectionRef)`
64
+ * is the highest-level entry point: load every pack, run the
65
+ * validator on each, and either return the diagnostic results
66
+ * or throw on the first hard error. The returned shape carries
67
+ * per-locale diagnostics so the caller can format a CLI report.
68
+ *
69
+ * **`siteContext` parameter** — the consumer's composed
70
+ * `WebsiteSiteContext` (see `website-site-context.ts`). The loader
71
+ * reads `siteContext.websiteConfig.locales` for "which locales does
72
+ * the site ship?" and `siteContext.rootConfig.{pageManifests,
73
+ * chromeLabelKeys, skipToMainContentLabelKey}` for the manifest
74
+ * registry / chrome-key set the validator runs against.
75
+ */
76
+
77
+ /**
78
+ * Load and validate a single locale's label pack. Synchronous —
79
+ * suitable for Astro frontmatter execution.
80
+ *
81
+ * @param packsDir Absolute or cwd-relative path to the directory
82
+ * containing per-locale `<locale>.json` files.
83
+ * @param locale The locale to load. Must be a member of
84
+ * `WildoMarketingWebsiteConfig.locales`.
85
+ * @returns A flat `LabelPack` (`Record<string, string>`) ready to
86
+ * feed into `WebsiteRuntimeContextValue.labelPack`.
87
+ * @throws When the file is missing, malformed JSON, not an object,
88
+ * or contains a non-string value.
89
+ */
90
+ export function loadLabelPack(packsDir: string, locale: AvailableLanguage): LabelPack {
91
+ const filePath = resolve(packsDir, `${locale}.json`);
92
+
93
+ let raw: string;
94
+ try {
95
+ raw = readFileSync(filePath, 'utf-8');
96
+ } catch (cause) {
97
+ throw new Error(
98
+ `[label-pack-loader] Could not read label pack at ${filePath} ` +
99
+ `(locale="${locale}"). ` +
100
+ `Make sure the directory exists and contains a "${locale}.json" file. ` +
101
+ `Underlying error: ${(cause as Error).message}`,
102
+ { cause },
103
+ );
104
+ }
105
+
106
+ let parsed: unknown;
107
+ try {
108
+ parsed = JSON.parse(raw);
109
+ } catch (cause) {
110
+ throw new Error(
111
+ `[label-pack-loader] Failed to parse JSON in ${filePath} ` +
112
+ `(locale="${locale}"). Underlying parser error: ${(cause as Error).message}`,
113
+ { cause },
114
+ );
115
+ }
116
+
117
+ return assertLabelPackShape(parsed, filePath, locale);
118
+ }
119
+
120
+ /**
121
+ * Batch loader — every locale declared on the site at once. The map
122
+ * iteration order matches the declaration order of
123
+ * `siteContext.websiteConfig.locales` so CLI reports come out in a
124
+ * deterministic, authored sequence.
125
+ */
126
+ export function loadAllLabelPacks(
127
+ packsDir: string,
128
+ siteContext: WebsiteSiteContext,
129
+ ): Map<AvailableLanguage, LabelPack> {
130
+ const packs = new Map<AvailableLanguage, LabelPack>();
131
+ for (const entry of siteContext.websiteConfig.locales) {
132
+ packs.set(entry.locale, loadLabelPack(packsDir, entry.locale));
133
+ }
134
+ return packs;
135
+ }
136
+
137
+ /**
138
+ * Per-locale validation result — the validator's flat output extended
139
+ * with the locale tag so a multi-locale report can flatten + group.
140
+ */
141
+ export interface LocaleLabelPackValidation {
142
+ locale: AvailableLanguage;
143
+ result: LabelPackValidationResult;
144
+ }
145
+
146
+ /**
147
+ * Highest-level entry point: load every locale's pack and run the
148
+ * label-pack validator against the manifest registry.
149
+ *
150
+ * **Return shape decision** — this function does NOT throw on
151
+ * `missing` keys; it returns the per-locale results so the caller
152
+ * (the build-start hook OR the CLI auditor OR a debugging
153
+ * subprocess) can decide how to surface the failure (process exit
154
+ * code, formatted CLI output, JSON report, …). A thin `throwIfAny…`
155
+ * helper sits next to it for the common "fail the build on first
156
+ * missing key" path.
157
+ */
158
+ export function loadAndValidateAllLabelPacks(
159
+ packsDir: string,
160
+ siteContext: WebsiteSiteContext,
161
+ expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,
162
+ ): readonly LocaleLabelPackValidation[] {
163
+ const { rootConfig } = siteContext;
164
+ const packs = loadAllLabelPacks(packsDir, siteContext);
165
+ const manifests: readonly WebsitePageManifest[] = rootConfig.pageManifests;
166
+ const effectiveChromeKeys = composeEffectiveChromeLabelKeys(rootConfig);
167
+
168
+ const validations: LocaleLabelPackValidation[] = [];
169
+ for (const [locale, pack] of packs) {
170
+ validations.push({
171
+ locale,
172
+ result: validateLabelPackAgainstManifest(
173
+ manifests,
174
+ expectedKeysBySectionRef,
175
+ pack,
176
+ effectiveChromeKeys,
177
+ ),
178
+ });
179
+ }
180
+ return validations;
181
+ }
182
+
183
+ /**
184
+ * Compose the chrome-label-key set the validator should treat as
185
+ * "expected" — `WebsiteRootConfig.chromeLabelKeys` (consumer-authored)
186
+ * UNION the framework-required `skipToMainContentLabelKey`.
187
+ *
188
+ * Centralized here because:
189
+ * - Both `loadAndValidateAllLabelPacks` (used at build time) and
190
+ * `buildKeyOriginMap` (used to annotate missing-key errors) need
191
+ * the same composed list, and forking the union into two places
192
+ * would be a drift hazard.
193
+ * - The framework owns the skip-link key (it is rendered by
194
+ * `WebsitePageLayout`, not the consumer's chrome), so the
195
+ * consumer should NOT have to remember to also list it in
196
+ * `chromeLabelKeys`. Auto-injection here keeps the contract
197
+ * minimal at the consumer's call site (just declare the key in
198
+ * `WebsiteRootConfig.skipToMainContentLabelKey` once + ship the
199
+ * translation in every locale pack) while still surfacing the key
200
+ * through the validator's "missing"/"orphaned" diagnostics.
201
+ * - De-duplication is via `Set` semantics so a consumer who
202
+ * defensively lists the key in both places (no-op but harmless)
203
+ * does not produce a duplicate-key warning downstream.
204
+ */
205
+ function composeEffectiveChromeLabelKeys(
206
+ rootConfig: WebsiteRootConfig,
207
+ ): readonly WebsiteLabelKey[] {
208
+ const set = new Set<WebsiteLabelKey>(rootConfig.chromeLabelKeys);
209
+ set.add(rootConfig.skipToMainContentLabelKey);
210
+ return Array.from(set);
211
+ }
212
+
213
+ /**
214
+ * Convenience wrapper for the build-start hook: validates every
215
+ * declared locale's pack and throws a SINGLE aggregated error if any
216
+ * locale has a hard failure (`missing` keys or `unmappedSectionRefs`).
217
+ * Orphaned keys are logged via `console.warn` (soft warning per the
218
+ * validator's contract) but do NOT throw.
219
+ *
220
+ * **Why aggregate (not first-fail)**: a multi-locale marketing site
221
+ * with `en` and `fr` packs both missing keys would, under a
222
+ * first-fail policy, force the author to fix `en`, re-run the build,
223
+ * THEN see `fr`'s problem. Aggregating means one build run surfaces
224
+ * every locale's hard errors together — material UX win for the
225
+ * authoring loop. The aggregated message keeps each locale's failure
226
+ * in its own block so the diagnostic stays scannable.
227
+ *
228
+ * **Why throw vs. return**: the build-start hook lives in
229
+ * `astro.config.mjs` (or a `vite-plugin`-style integration) and
230
+ * benefits from the shortest possible code path — a thrown error
231
+ * naturally aborts `astro build` with a non-zero exit code and
232
+ * surfaces the message in the build log. The richer
233
+ * `loadAndValidateAllLabelPacks` is what `wildo website audit` calls
234
+ * when it wants programmatic access to the per-locale results.
235
+ *
236
+ * **Key-origin annotation**: every `missing` key is tagged with the
237
+ * page (and, if section-scoped, the section) that declared it, so the
238
+ * author can grep + open the right `defineWebsiteSection` /
239
+ * `defineWebsitePageManifest` call site without leaving the build log.
240
+ */
241
+ export function assertLabelPacksValid(
242
+ packsDir: string,
243
+ siteContext: WebsiteSiteContext,
244
+ expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,
245
+ ): void {
246
+ const { rootConfig } = siteContext;
247
+ const validations = loadAndValidateAllLabelPacks(
248
+ packsDir,
249
+ siteContext,
250
+ expectedKeysBySectionRef,
251
+ );
252
+
253
+ /**
254
+ * Build a key → origin map ONCE per assert call (cost is bounded by
255
+ * the total number of declared keys across the whole site, which is
256
+ * typically small for a marketing surface). Enriches the missing-key
257
+ * error with the page/section that committed to each key — the
258
+ * author can grep the ref to find the site authoring call site.
259
+ */
260
+ const keyOrigins = buildKeyOriginMap(
261
+ rootConfig.pageManifests,
262
+ expectedKeysBySectionRef,
263
+ composeEffectiveChromeLabelKeys(rootConfig),
264
+ );
265
+
266
+ const localeErrorBlocks: string[] = [];
267
+
268
+ for (const { locale, result } of validations) {
269
+ const blocks: string[] = [];
270
+ if (result.unmappedSectionRefs.length > 0) {
271
+ blocks.push(
272
+ ` unmappedSectionRefs (build-pipeline bug — sections referenced by a ` +
273
+ `manifest but missing from the expected-keys collector): ` +
274
+ `[${(result.unmappedSectionRefs as unknown as readonly string[]).join(', ')}]. ` +
275
+ `Add every referenced section to the array passed to ` +
276
+ `collectExpectedLabelKeysFromSections().`,
277
+ );
278
+ }
279
+ if (result.missing.length > 0) {
280
+ const annotated = (result.missing as unknown as readonly string[]).map(
281
+ (key) => ` "${key}" ← ${keyOrigins.get(key) ?? 'unknown origin'}`,
282
+ );
283
+ blocks.push(
284
+ ` missing ${result.missing.length} required key(s) in ${packsDir}/${locale}.json:\n` +
285
+ annotated.join('\n'),
286
+ );
287
+ }
288
+ if (blocks.length > 0) {
289
+ localeErrorBlocks.push(`Locale "${locale}":\n${blocks.join('\n')}`);
290
+ }
291
+ if (result.orphaned.length > 0) {
292
+ console.warn(
293
+ `[label-pack-loader] Locale "${locale}": ${result.orphaned.length} ` +
294
+ `orphaned key(s) in ${locale}.json (declared in pack but not consumed ` +
295
+ `by any section or page manifest):\n ${result.orphaned.join('\n ')}`,
296
+ );
297
+ }
298
+ }
299
+
300
+ if (localeErrorBlocks.length > 0) {
301
+ throw new Error(
302
+ `[label-pack-loader] Label pack validation failed for ` +
303
+ `${localeErrorBlocks.length} locale(s):\n\n${localeErrorBlocks.join('\n\n')}`,
304
+ );
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Build a `keyString -> "page <pageRef> meta titleKey" | "section <sectionRef> in page <pageRef>"`
310
+ * lookup so the assert message can name the call site that committed
311
+ * to each missing key. Pure and small (cost is O(totalKeys) per build
312
+ * — bounded by the marketing surface).
313
+ */
314
+ function buildKeyOriginMap(
315
+ manifests: readonly WebsitePageManifest[],
316
+ expectedKeysBySectionRef: ExpectedLabelKeysBySectionRef,
317
+ chromeLabelKeys: readonly WebsiteLabelKey[],
318
+ ): Map<string, string> {
319
+ const origins = new Map<string, string>();
320
+ for (const manifest of manifests) {
321
+ const pageRefStr = manifest.ref as unknown as string;
322
+ const titleKey = manifest.metaLabelKeys.titleKey as unknown as string;
323
+ const descriptionKey = manifest.metaLabelKeys.descriptionKey as unknown as string;
324
+ if (!origins.has(titleKey)) {
325
+ origins.set(titleKey, `page "${pageRefStr}" metaLabelKeys.titleKey`);
326
+ }
327
+ if (!origins.has(descriptionKey)) {
328
+ origins.set(descriptionKey, `page "${pageRefStr}" metaLabelKeys.descriptionKey`);
329
+ }
330
+ for (const sectionRef of manifest.sectionRefs) {
331
+ const sectionRefStr = sectionRef as unknown as string;
332
+ const sectionKeys = expectedKeysBySectionRef.get(sectionRef);
333
+ if (sectionKeys === undefined) continue;
334
+ for (const sectionKey of sectionKeys) {
335
+ const keyStr = sectionKey as unknown as string;
336
+ if (!origins.has(keyStr)) {
337
+ origins.set(keyStr, `section "${sectionRefStr}" in page "${pageRefStr}"`);
338
+ }
339
+ }
340
+ }
341
+ }
342
+ for (const chromeKey of chromeLabelKeys) {
343
+ const keyStr = chromeKey as unknown as string;
344
+ if (!origins.has(keyStr)) {
345
+ origins.set(keyStr, `WebsiteRootConfig.chromeLabelKeys`);
346
+ }
347
+ }
348
+ return origins;
349
+ }
350
+
351
+ /**
352
+ * Defensive shape guard. Surface the offending key + observed type
353
+ * so the consumer can find the malformed entry quickly. Pure
354
+ * validation — no transformation; the returned value is the same
355
+ * object reference passed in (cast to `LabelPack`) so the caller
356
+ * does not pay an unnecessary copy.
357
+ */
358
+ function assertLabelPackShape(
359
+ parsed: unknown,
360
+ filePath: string,
361
+ locale: AvailableLanguage,
362
+ ): LabelPack {
363
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
364
+ throw new Error(
365
+ `[label-pack-loader] ${filePath} (locale="${locale}") must contain a ` +
366
+ `JSON object at the top level (got ${describeJson(parsed)}).`,
367
+ );
368
+ }
369
+ for (const [key, value] of Object.entries(parsed)) {
370
+ if (typeof value !== 'string') {
371
+ throw new Error(
372
+ `[label-pack-loader] ${filePath} (locale="${locale}"): key "${key}" ` +
373
+ `must map to a string (got ${describeJson(value)}).`,
374
+ );
375
+ }
376
+ }
377
+ return parsed as LabelPack;
378
+ }
379
+
380
+ function describeJson(value: unknown): string {
381
+ if (value === null) return 'null';
382
+ if (Array.isArray(value)) return 'array';
383
+ return typeof value;
384
+ }
385
+ /** @wildo_source:part:end saas.website.astro.label-loader */
@@ -0,0 +1,56 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ import type { WebsiteSiteContext } from './website-site-context';
5
+
6
+ export interface ReadLlmsTxtBodyOptions {
7
+ /**
8
+ * Directory containing the hand-authored `llms.txt` file. Defaults
9
+ * to the conventional `./src/content` path from the website package
10
+ * root.
11
+ */
12
+ contentDir?: string;
13
+ /**
14
+ * File name under `contentDir`. Exposed for tests; consumers should
15
+ * stick to the canonical `llms.txt`.
16
+ */
17
+ fileName?: string;
18
+ }
19
+
20
+ /**
21
+ * @wildo_source:part:start saas.website.astro.llms-txt-server facet:layer:astro facet:family:website
22
+ *
23
+ * Read the hand-authored `llms.txt` body for the marketing site.
24
+ *
25
+ * Publication policy:
26
+ * - `seo.llmsTxt?.enabled === false` → return `null`
27
+ * - `seo.llmsTxt?.enabled === true` → read the file if present, else `null`
28
+ * - `seo.llmsTxt === undefined` or `enabled === undefined` → auto-detect:
29
+ * publish when the file exists, otherwise return `null`
30
+ */
31
+ export function readLlmsTxtBody(
32
+ siteContext: WebsiteSiteContext,
33
+ options: ReadLlmsTxtBodyOptions = {},
34
+ ): string | null {
35
+ const contentDir = options.contentDir ?? './src/content';
36
+ const fileName = options.fileName ?? 'llms.txt';
37
+ const filePath = resolve(contentDir, fileName);
38
+
39
+ if (siteContext.rootConfig.seo.llmsTxt?.enabled === false) {
40
+ return null;
41
+ }
42
+ if (!existsSync(filePath)) {
43
+ return null;
44
+ }
45
+
46
+ try {
47
+ return readFileSync(filePath, 'utf-8');
48
+ } catch (cause) {
49
+ throw new Error(
50
+ `[llms-txt-server] Could not read llms.txt at ${filePath}. ` +
51
+ `Underlying error: ${(cause as Error).message}`,
52
+ { cause },
53
+ );
54
+ }
55
+ }
56
+ /** @wildo_source:part:end saas.website.astro.llms-txt-server */