@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 @@
1
+ {"version":3,"file":"structured-data.renderer.js","sourceRoot":"","sources":["../../../../src/astro/structured-data.renderer.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,MAAM,0BAA0B,GAAG,oBAA6B,CAAC;AAmBjE,MAAM,UAAU,wBAAwB,CAAC,IAAqC;IAC5E,MAAM,SAAS,GAAmC,EAAE,CAAC;IAErD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpC,IAAI,CAAC,OAAO,CAAC,iBAAiB;gBAAE,SAAS;YACzC,MAAM,KAAK,GAAgC,OAAO,CAAC,iBAAiB,EAAE,CAAC;YACvE,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;QACtB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACxC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEtC;;;;;OAKG;IACH,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,OAAO,GAAmC,EAAE,CAAC;IACnD,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAClD,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,OAAO,KAAK,SAAS;YAAE,SAAS;QACpC,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;YAC3B,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,SAAS;YAC3B,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACf,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,CAAC,OAAO,EAAE,CAAC;IAElB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;QAC9B,MAAM,WAAW,GACf,OAAO,CAAC,UAAU,CAAC,KAAK,SAAS;YAC/B,CAAC,CAAC,EAAE,UAAU,EAAE,0BAA0B,EAAE,GAAG,OAAO,EAAE;YACxD,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,sCAAsC,IAAI,WAAW,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,WAAW,CAClB,IAAoC,EACpC,KAAkC;IAElC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO;IAClD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS;gBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO;IACT,CAAC;IACD;;;;;OAKG;IACH,IAAI,CAAC,IAAI,CAAC,KAAqC,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,qBAAqB,CAAC,OAAgC;IAC7D,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AAC1D,CAAC;AACD,yEAAyE","sourcesContent":["import type {\n WebsiteStructuredDataPayload,\n WebsiteStructuredDataReturn,\n} from '../schemas/structured-data/website-structured-data.shared.schemas';\nimport type { WebsiteSectionDefinition } from '../schemas/sections/website-section-definition.shared.schemas';\n\n/**\n * @wildo_source:part:start saas.website.astro.structured-data-renderer facet:layer:astro facet:family:website\n *\n * `renderPageStructuredData()` — collects every section's\n * `getStructuredData()` return + any explicit page-level payloads,\n * normalizes them, dedupes by `@id`, and emits one\n * `<script type=\"application/ld+json\">` block per surviving payload.\n *\n * **Why one `<script>` per payload (NOT a single `@graph` array)**:\n *\n * - Google's structured-data testing tool, Bing webmaster tools, and\n * the Schema.org reference parser all accept multiple sibling\n * `<script type=\"application/ld+json\">` blocks. Emitting them as\n * separate blocks lets each payload be diff-friendly in the\n * page source AND lets per-payload removal happen on a future\n * AEO opt-out without churning the whole graph.\n * - `@graph` is technically valid but harder to author — every\n * payload would need to share `@context` with the wrapper, and\n * per-section authors would have to coordinate to merge their\n * payloads into one shared graph. The \"one script per payload\"\n * pattern keeps section authoring fully local.\n *\n * **Why dedupe by `@id` (last-wins)**: a page may have a section\n * emit a `Product` payload AND another section emit a `Review`\n * referencing the same `Product` via `@id`. Without dedupe, the\n * `Product` would emit twice (once standalone, once referenced).\n * Last-wins because the section's own emission is closer to the\n * truth than a reference; the dedupe is best-effort and harmless\n * if `@id` is absent (the payload always emits).\n *\n * **`@context` injection**: when a payload omits `@context`, the\n * renderer injects `'https://schema.org'`. Consumers wanting a\n * custom vocabulary supply `@context` themselves and the renderer\n * passes it through unchanged.\n *\n * **HTML-safe serialization**: JSON-LD payloads are serialized via\n * `JSON.stringify` with the closing `</` escaped to `<\\/` (and the\n * `<!` opener escaped to `<\\!`) to prevent the payload from\n * accidentally closing the surrounding `<script>` tag if the\n * payload contains the string `</script>` (e.g. a blog post about\n * structured data). This is the canonical safe-serialization\n * pattern (see https://html.spec.whatwg.org/#restrictions-for-contents-of-script-elements).\n */\n\nconst SCHEMA_ORG_DEFAULT_CONTEXT = 'https://schema.org' as const;\n\nexport interface RenderPageStructuredDataOptions {\n /**\n * Section definitions whose `getStructuredData()` (when present)\n * contributes payloads to the page. Sections without\n * `getStructuredData` are skipped silently.\n */\n sections?: readonly Pick<WebsiteSectionDefinition, 'sectionRef' | 'getStructuredData'>[];\n /**\n * Optional page-level payloads contributed by the page-shell\n * itself (typical: `BreadcrumbList`, `Organization`, `WebSite`).\n * Composed AFTER section payloads in the emission order so a\n * section's `@id`-keyed payload wins over a page-level one with\n * the same `@id` (last-wins; see preamble).\n */\n pagePayloads?: readonly WebsiteStructuredDataPayload[];\n}\n\nexport function renderPageStructuredData(opts: RenderPageStructuredDataOptions): string {\n const collected: WebsiteStructuredDataPayload[] = [];\n\n if (opts.sections) {\n for (const section of opts.sections) {\n if (!section.getStructuredData) continue;\n const value: WebsiteStructuredDataReturn = section.getStructuredData();\n collectInto(collected, value);\n }\n }\n\n if (opts.pagePayloads) {\n for (const payload of opts.pagePayloads) {\n collected.push(payload);\n }\n }\n\n if (collected.length === 0) return '';\n\n /**\n * Dedupe by `@id` (last-wins). Payloads without `@id` always\n * survive. We walk LAST-to-FIRST so the LAST occurrence of an\n * `@id` is the kept one (later writes overwrite earlier ones\n * naturally with this loop direction + reverse at the end).\n */\n const seen = new Set<string>();\n const deduped: WebsiteStructuredDataPayload[] = [];\n for (let i = collected.length - 1; i >= 0; i -= 1) {\n const payload = collected[i];\n if (payload === undefined) continue;\n const id = payload['@id'];\n if (typeof id === 'string') {\n if (seen.has(id)) continue;\n seen.add(id);\n }\n deduped.push(payload);\n }\n deduped.reverse();\n\n const lines: string[] = [];\n for (const payload of deduped) {\n const withContext: Record<string, unknown> =\n payload['@context'] === undefined\n ? { '@context': SCHEMA_ORG_DEFAULT_CONTEXT, ...payload }\n : { ...payload };\n const json = serializeForScriptTag(withContext);\n lines.push(`<script type=\"application/ld+json\">${json}</script>`);\n }\n return lines.join('\\n');\n}\n\n/**\n * Normalize a `getStructuredData()` return value into a flat array\n * the renderer iterates. Handles the four legal shapes\n * (`null` / `undefined` / single payload / readonly array).\n *\n * Mutates the `into` array in place — the function is private to\n * this module and the in-place mutation avoids one allocation per\n * section (sections compose linearly, the cost adds up on long\n * pages).\n */\nfunction collectInto(\n into: WebsiteStructuredDataPayload[],\n value: WebsiteStructuredDataReturn,\n): void {\n if (value === null || value === undefined) return;\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== null && item !== undefined) into.push(item);\n }\n return;\n }\n /**\n * Single-payload shape — narrow the readonly-array branch above\n * by elimination. `Array.isArray` returns `false` for readonly\n * arrays in some TS configs but TRUE at runtime; this branch\n * runs only for non-array `WebsiteStructuredDataPayload`.\n */\n into.push(value as WebsiteStructuredDataPayload);\n}\n\n/**\n * Serialize a payload for emission inside a `<script>` tag.\n *\n * `JSON.stringify` alone is NOT safe inside `<script>` tags:\n *\n * - A payload field containing the literal string `</script>`\n * (rare but possible on a blog post about HTML / structured\n * data) would close the script tag prematurely and inject the\n * remainder as page HTML.\n * - A payload field containing `<!--` similarly opens an HTML\n * comment that breaks the surrounding parsing context.\n *\n * Both are mitigated by escaping `<` to `\\u003c` inside the JSON\n * payload — the JSON parser treats `\\u003c` as identical to `<`,\n * so the consumed payload is unchanged, but the HTML parser sees\n * no `<` characters and never gets confused.\n */\nfunction serializeForScriptTag(payload: Record<string, unknown>): string {\n return JSON.stringify(payload).replace(/</g, '\\\\u003c');\n}\n/** @wildo_source:part:end saas.website.astro.structured-data-renderer */\n"]}
@@ -0,0 +1,93 @@
1
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
+ import type { WebsiteDesignTokens } from '../schemas/design-tokens/website-design-tokens.shared.schemas';
3
+ import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
4
+ import type { LabelPack } from '../schemas/validators/label-pack.validator';
5
+ import type { WebsiteSiteContext } from './website-site-context';
6
+ /**
7
+ * @wildo_source:part:start saas.website.astro.head-renderer facet:layer:astro facet:family:website
8
+ *
9
+ * Head-block renderers — pure string builders the consumer's per-page
10
+ * `.astro` file injects via `<Fragment set:html={...}/>` /
11
+ * `<style set:html={...}></style>`. Two functions:
12
+ *
13
+ * - `renderWebsitePageHeadHtml(opts)` — emits the complete SEO
14
+ * `<head>` block: `<title>`, meta description, full Open Graph
15
+ * card (`og:title` / `og:description` / `og:type` / `og:url` /
16
+ * `og:site_name` / `og:locale` / `og:locale:alternate` / optional
17
+ * `og:image`), Twitter card mirrors, `<link rel="canonical">`,
18
+ * and one `<link rel="alternate" hreflang>` per declared locale
19
+ * (plus an `x-default` for Google's i18n discoverability). Every
20
+ * URL is ABSOLUTE (composed from `websiteConfig.publicOrigin` +
21
+ * the page's `routePath`) — relative hreflang/canonical
22
+ * annotations are not honored consistently across crawlers
23
+ * (Phase 6 audit findings H-B / H-C / M-A).
24
+ * - `renderWebsiteDesignTokensCss(tokens)` — flattens the design-token
25
+ * bundle into `--website-*` custom properties scoped to `:root`.
26
+ * Server-static — zero island JS.
27
+ *
28
+ * **Why pure string builders** (rather than React components or Astro
29
+ * components):
30
+ *
31
+ * 1. **Avoids the `.astro` distribution problem.** A `.astro` shell
32
+ * cannot be re-exported from a TS subpath barrel and `tsc` does
33
+ * not copy `.astro` files to `dist/`. Pure TS sidesteps both.
34
+ * 2. **Avoids the React-island boundary.** Rendering head content
35
+ * via React would require either (a) a server-only React subtree
36
+ * (extra moving part) or (b) hydrating the `<head>` on the client
37
+ * (waste of bundle size for static-build content).
38
+ * 3. **Vitest-friendly.** Pure functions returning strings are
39
+ * trivially asserted against without an Astro test harness.
40
+ *
41
+ * **HTML escaping**: every consumer-supplied string (title,
42
+ * description, OG image URL, hreflang values) is escaped with
43
+ * `escapeHtmlAttribute` / `escapeHtmlText` before interpolation into
44
+ * the emitted markup. Marketing copy frequently contains characters
45
+ * (quotes, ampersands, the rare `<`) that would break the surrounding
46
+ * tag if interpolated raw — escaping is non-negotiable for a public
47
+ * surface.
48
+ *
49
+ * **Failure modes** — `renderWebsitePageHeadHtml` throws when:
50
+ * - `manifest.metaLabelKeys.titleKey` is absent from the pack
51
+ * - `manifest.metaLabelKeys.descriptionKey` is absent from the pack
52
+ * Both throw with the locale + key + actionable next step (`Run
53
+ * \`wildo website audit\` or add the entry to <pack-path>.json`).
54
+ */
55
+ export interface RenderWebsitePageHeadHtmlOptions {
56
+ manifest: WebsitePageManifest;
57
+ /**
58
+ * Composed `WebsiteSiteContext` — the renderer reads
59
+ * `websiteConfig.{defaultLocale, locales, publicOrigin}` for the
60
+ * URL/locale surface and `rootConfig.seo.{siteName, defaultOgImageUrl}`
61
+ * for the SEO branding fallback.
62
+ */
63
+ siteContext: WebsiteSiteContext;
64
+ locale: AvailableLanguage;
65
+ labelPack: LabelPack;
66
+ }
67
+ export declare function renderWebsitePageHeadHtml(opts: RenderWebsitePageHeadHtmlOptions): string;
68
+ /**
69
+ * Flatten `WebsiteDesignTokens` to a `:root { … }` block of CSS custom
70
+ * properties. Naming convention: `--website-<group>-<token>` for nested
71
+ * groups (`colors.primary` → `--website-colors-primary`); top-level
72
+ * string-valued tokens (none today, future-proofed) emit as
73
+ * `--website-<token>`.
74
+ *
75
+ * **Why scoped to `:root`** rather than `<WebsitePageLayout>`'s root
76
+ * div: marketing pages frequently embed third-party widgets (live-chat,
77
+ * analytics overlays) that paint outside the React tree. Scoping
78
+ * tokens to `:root` makes them visible to those widgets too. The
79
+ * design-token surface is small enough that the cost of a broader
80
+ * scope (token-name collisions with consumer-authored `:root` rules)
81
+ * is bounded by the `--website-` prefix.
82
+ */
83
+ export declare function renderWebsiteDesignTokensCss(tokens: WebsiteDesignTokens): string;
84
+ /**
85
+ * @wildo_source:part:end saas.website.astro.head-renderer
86
+ *
87
+ * HTML escape helpers moved to `./internal/html-escape.ts` (Phase 7)
88
+ * so the new `blog-post-head.renderer.ts` sibling can share the same
89
+ * escape contract instead of redefining it. See that module's preamble
90
+ * for the framework-mindset rationale (one escape contract per engine,
91
+ * not one per renderer).
92
+ */
93
+ //# sourceMappingURL=website-page-head.renderer.d.ts.map
@@ -0,0 +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,CAmKxF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAehF;AAED;;;;;;;;GAQG"}
@@ -0,0 +1,184 @@
1
+ import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils.js';
2
+ import { escapeHtmlAttribute, escapeHtmlText } from './internal/html-escape.js';
3
+ export function renderWebsitePageHeadHtml(opts) {
4
+ const { manifest, siteContext, locale, labelPack } = opts;
5
+ const { rootConfig, websiteConfig } = siteContext;
6
+ const titleKey = manifest.metaLabelKeys.titleKey;
7
+ const descriptionKey = manifest.metaLabelKeys.descriptionKey;
8
+ const title = labelPack[titleKey];
9
+ const description = labelPack[descriptionKey];
10
+ if (title === undefined) {
11
+ throw new Error(`[renderWebsitePageHeadHtml] Label pack for locale "${locale}" is missing ` +
12
+ `the page title key "${titleKey}" (page ref="${manifest.ref}"). ` +
13
+ `Run \`wildo website audit\` or add the entry to your <locale>.json pack.`);
14
+ }
15
+ if (description === undefined) {
16
+ throw new Error(`[renderWebsitePageHeadHtml] Label pack for locale "${locale}" is missing ` +
17
+ `the page description key "${descriptionKey}" (page ref="${manifest.ref}"). ` +
18
+ `Run \`wildo website audit\` or add the entry to your <locale>.json pack.`);
19
+ }
20
+ /**
21
+ * OG image precedence: per-page `manifest.metaLabelKeys.ogImageUrl`
22
+ * wins; falls back to the site-level `seo.defaultOgImageUrl` so
23
+ * marketing pages without a hero image still produce a richer
24
+ * social-card (Phase 6 audit fix M-A). When neither is present,
25
+ * the Twitter card kind degrades to the smaller `summary` and the
26
+ * `og:image` / `twitter:image` lines are omitted entirely.
27
+ */
28
+ const ogImageUrl = manifest.metaLabelKeys.ogImageUrl ?? rootConfig.seo.defaultOgImageUrl;
29
+ const titleAttr = escapeHtmlAttribute(title);
30
+ const descriptionAttr = escapeHtmlAttribute(description);
31
+ const titleText = escapeHtmlText(title);
32
+ /**
33
+ * Absolute URL builder. `prefixDefaultLocale: false` means the
34
+ * default locale lives at the locale-neutral path; every other
35
+ * locale sits under `/<locale>/<routePath>`. `x-default` always
36
+ * resolves to the default-locale path.
37
+ *
38
+ * Every URL emitted here is FULLY-QUALIFIED (composed against
39
+ * `websiteConfig.publicOrigin`) — Google's hreflang spec requires
40
+ * absolute URLs, and `<link rel="canonical">` relies on absolute
41
+ * URLs to survive cross-host fetches (preview deployments, CDN
42
+ * edges). See `WildoMarketingWebsiteConfigSchema.publicOrigin`
43
+ * JSDoc for the full rationale (incl. trailing-slash rejection).
44
+ */
45
+ const routePath = manifest.routePath;
46
+ const siteOrigin = websiteConfig.publicOrigin;
47
+ const defaultLocale = websiteConfig.defaultLocale;
48
+ const currentLocaleStr = String(locale);
49
+ /**
50
+ * Compose the absolute URL for `(locale, routePath)` by delegating
51
+ * the locale-prefix decision to the shared `buildLocalizedRoutePath`
52
+ * helper — same routing rule that powers `WebsiteInternalLink`. This
53
+ * guarantees that hreflang/canonical URLs and the chrome's internal
54
+ * links can never disagree about how a locale is prefixed.
55
+ */
56
+ function buildAbsoluteUrl(forLocale) {
57
+ const path = buildLocalizedRoutePath({
58
+ locale: forLocale,
59
+ defaultLocale,
60
+ routePath,
61
+ });
62
+ return `${siteOrigin}${path}`;
63
+ }
64
+ const canonicalUrl = buildAbsoluteUrl(locale);
65
+ const hreflangLines = [];
66
+ for (const entry of websiteConfig.locales) {
67
+ const localeStr = String(entry.locale);
68
+ hreflangLines.push(`<link rel="alternate" hreflang="${escapeHtmlAttribute(localeStr)}" href="${escapeHtmlAttribute(buildAbsoluteUrl(entry.locale))}" />`);
69
+ }
70
+ hreflangLines.push(`<link rel="alternate" hreflang="x-default" href="${escapeHtmlAttribute(buildAbsoluteUrl(defaultLocale))}" />`);
71
+ /**
72
+ * `og:image` + `twitter:image` are emitted only when an image URL
73
+ * is available (page-level override OR site-level fallback).
74
+ * Twitter card type also flips between `summary` (no image) and
75
+ * `summary_large_image` (image present).
76
+ */
77
+ const ogImageLines = [];
78
+ if (ogImageUrl !== undefined) {
79
+ const ogImageAttr = escapeHtmlAttribute(ogImageUrl);
80
+ ogImageLines.push(`<meta property="og:image" content="${ogImageAttr}" />`, `<meta name="twitter:image" content="${ogImageAttr}" />`);
81
+ }
82
+ const twitterCardKind = ogImageUrl !== undefined ? 'summary_large_image' : 'summary';
83
+ /**
84
+ * `og:locale:alternate` — one per non-current declared locale. Open
85
+ * Graph spec: the `og:locale` field carries the page's primary
86
+ * locale; alternates declare the other locales the page is
87
+ * available in. Emitted in declaration order.
88
+ *
89
+ * Locale format: we emit the bare language code (`en`, `fr`) per
90
+ * the AvailableLanguage atom shape. Open Graph technically
91
+ * recommends `language_TERRITORY` (e.g. `en_US`), but Facebook /
92
+ * LinkedIn / Slack accept the bare language code in practice. The
93
+ * future fix when the platform grows territory-tagged locales is to
94
+ * map at this site (no consumer change required).
95
+ */
96
+ const ogLocaleAlternateLines = [];
97
+ for (const entry of websiteConfig.locales) {
98
+ const localeStr = String(entry.locale);
99
+ if (localeStr === currentLocaleStr)
100
+ continue;
101
+ ogLocaleAlternateLines.push(`<meta property="og:locale:alternate" content="${escapeHtmlAttribute(localeStr)}" />`);
102
+ }
103
+ const siteNameAttr = escapeHtmlAttribute(rootConfig.seo.siteName);
104
+ const canonicalAttr = escapeHtmlAttribute(canonicalUrl);
105
+ const ogLocaleAttr = escapeHtmlAttribute(currentLocaleStr);
106
+ /**
107
+ * AI-training opt-out (D-AG3) — `<meta name="robots" content="noai, noimageai">`
108
+ * is emitted ONLY when the page manifest opts in via
109
+ * `seo.noaiOptOut: true`. Default is OPT-IN to AI training (the
110
+ * marketing-site default). This emission pairs with the sibling
111
+ * `_headers` entry produced by `renderWebsitePageNoaiHeaders()`
112
+ * for crawlers that don't honor in-document meta.
113
+ *
114
+ * Format: `noai` (block AI training on text) and `noimageai`
115
+ * (block AI training on images) are the two robots directives the
116
+ * major AI crawlers (GPTBot, ClaudeBot, Google-Extended, etc.)
117
+ * recognise. Emitting them as a single comma-separated `content`
118
+ * value is the canonical robots-meta shape.
119
+ */
120
+ const noaiMetaLines = [];
121
+ if (manifest.seo?.noaiOptOut === true) {
122
+ noaiMetaLines.push(`<meta name="robots" content="noai, noimageai" />`);
123
+ }
124
+ return [
125
+ `<title>${titleText}</title>`,
126
+ `<meta name="description" content="${descriptionAttr}" />`,
127
+ `<link rel="canonical" href="${canonicalAttr}" />`,
128
+ `<meta property="og:title" content="${titleAttr}" />`,
129
+ `<meta property="og:description" content="${descriptionAttr}" />`,
130
+ `<meta property="og:type" content="website" />`,
131
+ `<meta property="og:url" content="${canonicalAttr}" />`,
132
+ `<meta property="og:site_name" content="${siteNameAttr}" />`,
133
+ `<meta property="og:locale" content="${ogLocaleAttr}" />`,
134
+ ...ogLocaleAlternateLines,
135
+ ...ogImageLines,
136
+ `<meta name="twitter:card" content="${twitterCardKind}" />`,
137
+ `<meta name="twitter:title" content="${titleAttr}" />`,
138
+ `<meta name="twitter:description" content="${descriptionAttr}" />`,
139
+ ...noaiMetaLines,
140
+ ...hreflangLines,
141
+ ].join('\n');
142
+ }
143
+ /**
144
+ * Flatten `WebsiteDesignTokens` to a `:root { … }` block of CSS custom
145
+ * properties. Naming convention: `--website-<group>-<token>` for nested
146
+ * groups (`colors.primary` → `--website-colors-primary`); top-level
147
+ * string-valued tokens (none today, future-proofed) emit as
148
+ * `--website-<token>`.
149
+ *
150
+ * **Why scoped to `:root`** rather than `<WebsitePageLayout>`'s root
151
+ * div: marketing pages frequently embed third-party widgets (live-chat,
152
+ * analytics overlays) that paint outside the React tree. Scoping
153
+ * tokens to `:root` makes them visible to those widgets too. The
154
+ * design-token surface is small enough that the cost of a broader
155
+ * scope (token-name collisions with consumer-authored `:root` rules)
156
+ * is bounded by the `--website-` prefix.
157
+ */
158
+ export function renderWebsiteDesignTokensCss(tokens) {
159
+ const lines = [':root {'];
160
+ for (const [groupName, groupValue] of Object.entries(tokens)) {
161
+ if (groupValue !== null && typeof groupValue === 'object' && !Array.isArray(groupValue)) {
162
+ for (const [tokenName, tokenValue] of Object.entries(groupValue)) {
163
+ if (typeof tokenValue === 'string') {
164
+ lines.push(` --website-${groupName}-${tokenName}: ${tokenValue};`);
165
+ }
166
+ }
167
+ }
168
+ else if (typeof groupValue === 'string') {
169
+ lines.push(` --website-${groupName}: ${groupValue};`);
170
+ }
171
+ }
172
+ lines.push('}');
173
+ return lines.join('\n');
174
+ }
175
+ /**
176
+ * @wildo_source:part:end saas.website.astro.head-renderer
177
+ *
178
+ * HTML escape helpers moved to `./internal/html-escape.ts` (Phase 7)
179
+ * so the new `blog-post-head.renderer.ts` sibling can share the same
180
+ * escape contract instead of redefining it. See that module's preamble
181
+ * for the framework-mindset rationale (one escape contract per engine,
182
+ * not one per renderer).
183
+ */
184
+ //# sourceMappingURL=website-page-head.renderer.js.map
@@ -0,0 +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 * AI-training opt-out (D-AG3) — `<meta name=\"robots\" content=\"noai, noimageai\">`\n * is emitted ONLY when the page manifest opts in via\n * `seo.noaiOptOut: true`. Default is OPT-IN to AI training (the\n * marketing-site default). This emission pairs with the sibling\n * `_headers` entry produced by `renderWebsitePageNoaiHeaders()`\n * for crawlers that don't honor in-document meta.\n *\n * Format: `noai` (block AI training on text) and `noimageai`\n * (block AI training on images) are the two robots directives the\n * major AI crawlers (GPTBot, ClaudeBot, Google-Extended, etc.)\n * recognise. Emitting them as a single comma-separated `content`\n * value is the canonical robots-meta shape.\n */\n const noaiMetaLines: string[] = [];\n if (manifest.seo?.noaiOptOut === true) {\n noaiMetaLines.push(`<meta name=\"robots\" content=\"noai, noimageai\" />`);\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"]}
@@ -0,0 +1,112 @@
1
+ import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
+ import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
3
+ import type { LabelPack } from '../schemas/validators/label-pack.validator';
4
+ import type { WebsiteSiteContext } from './website-site-context';
5
+ /**
6
+ * @wildo_source:part:start saas.website.astro.runtime-helper facet:layer:astro facet:family:website
7
+ *
8
+ * `loadWebsitePageRuntime(opts)` — the per-request entry point a
9
+ * consumer's `src/pages/<page>.astro` calls inside its frontmatter to
10
+ * resolve the active locale and load the matching label pack. Returns
11
+ * the two primitive values (`locale`, `labelPack`) that travel as
12
+ * Astro-island props to `WebsitePageBridgeIsland` (see
13
+ * `bridge-runtime.tsx`).
14
+ *
15
+ * **Why a single helper** (instead of two — locale resolution + pack
16
+ * load — invoked separately in every page's frontmatter): the locale
17
+ * fallback rule (`Astro.currentLocale ?? websiteConfig.defaultLocale`)
18
+ * MUST stay aligned with the `prefixDefaultLocale: false` convention
19
+ * encoded by `buildAstroI18nConfig`. Centralizing the rule here means
20
+ * a future change (e.g. honoring an `Accept-Language` header before
21
+ * the static default-locale fallback) lands in one place rather than
22
+ * being duplicated across every consumer page's frontmatter.
23
+ *
24
+ * **Why this lives in a `.ts` helper, NOT in a `.astro` shell**: a shipped
25
+ * `WebsitePageBridge.astro` cannot cross-host the page/header/footer
26
+ * `ComponentType` references through the React-island serialization
27
+ * boundary (Astro serializes island props via `devalue`-style JSON;
28
+ * function refs do not survive). Keeping the per-request work in a
29
+ * pure TS helper means the consumer authors their own thin per-page
30
+ * `.astro` file that statically imports each page's bridge island —
31
+ * the component reference is then a JSX tag in the consumer's
32
+ * `.astro` template, which Astro's compiler resolves at build time
33
+ * (no prop serialization).
34
+ *
35
+ * **Failure modes** (all surfaced as thrown errors with actionable
36
+ * messages):
37
+ * - `astroLocale` is provided but is not a member of
38
+ * `siteContext.websiteConfig.locales`: throws — Astro's i18n
39
+ * routing should never present a locale outside the declared set;
40
+ * if it does, `astro.config.mjs` is out of sync with
41
+ * `WildoMarketingWebsiteConfig`.
42
+ * - Label pack file missing / malformed: surfaced by
43
+ * `loadLabelPack` (filesystem error message + locale + path).
44
+ *
45
+ * @param opts.manifest The page manifest the consumer is
46
+ * rendering. Read by `pickLabelsForPage`
47
+ * to filter the loaded label pack down to
48
+ * the keys this page actually consumes
49
+ * (the page's own `website.<ref>.*` slice
50
+ * plus the shared `website.chrome.*`
51
+ * slice). Without this filter every page
52
+ * would serialize the whole site's pack
53
+ * into its `<astro-island props=…>`
54
+ * attribute — see
55
+ * `pick-labels-for-page.ts` for the full
56
+ * rationale.
57
+ * @param opts.siteContext The composed `WebsiteSiteContext`. Read
58
+ * for `websiteConfig.{defaultLocale,
59
+ * locales}` (locale resolution) and
60
+ * `rootConfig.skipToMainContentLabelKey`
61
+ * (chrome key carried into the per-page
62
+ * pack slice).
63
+ * @param opts.labelPacksDir Directory holding `<locale>.json` packs.
64
+ * Resolved relative to the cwd Astro runs
65
+ * in (typically the project root).
66
+ * @param opts.astroLocale Whatever `Astro.currentLocale` returned
67
+ * at the consumer's call site.
68
+ * `string | undefined` — Astro returns
69
+ * `undefined` for the un-prefixed
70
+ * default-locale path.
71
+ */
72
+ export interface LoadWebsitePageRuntimeOptions {
73
+ /** The page manifest the consumer is rendering (carried for forward-compat). */
74
+ manifest: WebsitePageManifest;
75
+ /** The site's composed `WebsiteSiteContext` — single source of truth. */
76
+ siteContext: WebsiteSiteContext;
77
+ /** Directory holding `<locale>.json` label-pack files. */
78
+ labelPacksDir: string;
79
+ /** The value of `Astro.currentLocale` at the call site. */
80
+ astroLocale: string | undefined;
81
+ }
82
+ /**
83
+ * Result of `loadWebsitePageRuntime` — the two primitive values that
84
+ * travel as Astro-island props to the page's bridge island.
85
+ *
86
+ * Both values are JSON-serializable so they cleanly cross Astro's
87
+ * client-island hydration boundary. Function refs (page component,
88
+ * header / footer component) are NOT carried here on purpose; they
89
+ * are closed over by the per-page `createWebsitePageBridgeIsland(...)`
90
+ * call so they never need to cross a serialization boundary. See the
91
+ * file-level JSDoc above and `bridge-runtime.tsx` for the redesign
92
+ * rationale.
93
+ */
94
+ export interface WebsitePageRuntime {
95
+ /** The resolved locale (Astro's value with default-locale fallback). */
96
+ locale: AvailableLanguage;
97
+ /**
98
+ * The site's default locale (mirrors `websiteConfig.defaultLocale`).
99
+ * Returned alongside `locale` so the consumer can pass both to the
100
+ * bridge island in one symmetric block (`{ locale, defaultLocale }`)
101
+ * — the island then publishes both into `WebsiteRuntimeContext`,
102
+ * which `WebsiteInternalLink` reads to compute locale-prefixed paths.
103
+ * Re-exporting it here avoids forcing every
104
+ * consumer page to import the website config separately.
105
+ */
106
+ defaultLocale: AvailableLanguage;
107
+ /** The loaded + shape-validated label pack for the active locale. */
108
+ labelPack: LabelPack;
109
+ }
110
+ export declare function loadWebsitePageRuntime(opts: LoadWebsitePageRuntimeOptions): WebsitePageRuntime;
111
+ /** @wildo_source:part:end saas.website.astro.runtime-helper */
112
+ //# sourceMappingURL=website-page-runtime.helper.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,60 @@
1
+ import { loadLabelPack } from './label-pack-loader.js';
2
+ import { pickLabelsForPage } from './pick-labels-for-page.js';
3
+ export function loadWebsitePageRuntime(opts) {
4
+ const { manifest, siteContext, labelPacksDir, astroLocale } = opts;
5
+ const { rootConfig, websiteConfig } = siteContext;
6
+ /**
7
+ * Locale resolution — the `prefixDefaultLocale: false` convention
8
+ * means Astro serves the default locale at the un-prefixed path
9
+ * (`/`, `/pricing`, …) and reports `undefined` from
10
+ * `Astro.currentLocale` for those requests. Coalesce here to the
11
+ * website config's default rather than letting `undefined` leak
12
+ * into the label-pack lookup.
13
+ */
14
+ const resolvedLocale = astroLocale === undefined
15
+ ? websiteConfig.defaultLocale
16
+ : assertKnownLocale(astroLocale, websiteConfig);
17
+ const fullPack = loadLabelPack(labelPacksDir, resolvedLocale);
18
+ /**
19
+ * Filter to the keys this page can ever read at runtime — page-own
20
+ * slice + shared chrome slice. The full pack is loaded once (one
21
+ * fs read per request), then sliced; the slimmed result is what
22
+ * crosses the SSR → client serialization boundary inside
23
+ * `<astro-island props=…>`. Sites with O(100) keys per page on
24
+ * O(10) pages save material payload on every HTML response — the
25
+ * untrimmed default would have served every page the entire site's
26
+ * pack on every locale.
27
+ */
28
+ const labelPack = pickLabelsForPage(fullPack, manifest, [
29
+ rootConfig.skipToMainContentLabelKey,
30
+ ]);
31
+ return {
32
+ locale: resolvedLocale,
33
+ defaultLocale: websiteConfig.defaultLocale,
34
+ labelPack,
35
+ };
36
+ }
37
+ /**
38
+ * Defensive: Astro's i18n routing should only ever present a locale
39
+ * declared on `WildoMarketingWebsiteConfig.locales`. If it does not,
40
+ * the consumer's `astro.config.mjs` is out of sync with the website
41
+ * config — surface that loudly here rather than letting an unknown
42
+ * locale trigger a `[label-pack-loader] Could not read pack` error
43
+ * that would imply a content-authoring fault (missing JSON file)
44
+ * rather than a configuration fault (Astro routing mismatch).
45
+ */
46
+ function assertKnownLocale(astroLocale, websiteConfig) {
47
+ for (const entry of websiteConfig.locales) {
48
+ if (entry.locale === astroLocale) {
49
+ return entry.locale;
50
+ }
51
+ }
52
+ const declared = websiteConfig.locales.map((entry) => entry.locale);
53
+ throw new Error(`[loadWebsitePageRuntime] Astro presented locale "${astroLocale}" but ` +
54
+ `WildoMarketingWebsiteConfig.locales declares only [${declared.join(', ')}]. ` +
55
+ `Either add "${astroLocale}" to wildo.website.config.ts's locales or update ` +
56
+ `astro.config.mjs to consume buildAstroI18nConfig(WEBSITE_SITE_CONTEXT) so ` +
57
+ `both sides stay in sync.`);
58
+ }
59
+ /** @wildo_source:part:end saas.website.astro.runtime-helper */
60
+ //# sourceMappingURL=website-page-runtime.helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"website-page-runtime.helper.js","sourceRoot":"","sources":["../../../../src/astro/website-page-runtime.helper.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AA8G3D,MAAM,UAAU,sBAAsB,CACpC,IAAmC;IAEnC,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACnE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD;;;;;;;OAOG;IACH,MAAM,cAAc,GAClB,WAAW,KAAK,SAAS;QACvB,CAAC,CAAC,aAAa,CAAC,aAAa;QAC7B,CAAC,CAAC,iBAAiB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAEpD,MAAM,QAAQ,GAAG,aAAa,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC;IAC9D;;;;;;;;;OASG;IACH,MAAM,SAAS,GAAG,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE;QACtD,UAAU,CAAC,yBAAyB;KACrC,CAAC,CAAC;IAEH,OAAO;QACL,MAAM,EAAE,cAAc;QACtB,aAAa,EAAE,aAAa,CAAC,aAAa;QAC1C,SAAS;KACV,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CACxB,WAAmB,EACnB,aAA0C;IAE1C,KAAK,MAAM,KAAK,IAAI,aAAa,CAAC,OAAO,EAAE,CAAC;QAC1C,IAAK,KAAK,CAAC,MAA4B,KAAK,WAAW,EAAE,CAAC;YACxD,OAAO,KAAK,CAAC,MAAM,CAAC;QACtB,CAAC;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,CACxC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAA2B,CAC7C,CAAC;IACF,MAAM,IAAI,KAAK,CACb,oDAAoD,WAAW,QAAQ;QACrE,sDAAsD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;QAC9E,eAAe,WAAW,mDAAmD;QAC7E,4EAA4E;QAC5E,0BAA0B,CAC7B,CAAC;AACJ,CAAC;AACD,+DAA+D","sourcesContent":["import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport type { WildoMarketingWebsiteConfig } from '../config/wildo-website-config.schemas';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\nimport type { LabelPack } from '../schemas/validators/label-pack.validator';\n\nimport { loadLabelPack } from './label-pack-loader';\nimport { pickLabelsForPage } from './pick-labels-for-page';\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.runtime-helper facet:layer:astro facet:family:website\n *\n * `loadWebsitePageRuntime(opts)` — the per-request entry point a\n * consumer's `src/pages/<page>.astro` calls inside its frontmatter to\n * resolve the active locale and load the matching label pack. Returns\n * the two primitive values (`locale`, `labelPack`) that travel as\n * Astro-island props to `WebsitePageBridgeIsland` (see\n * `bridge-runtime.tsx`).\n *\n * **Why a single helper** (instead of two — locale resolution + pack\n * load — invoked separately in every page's frontmatter): the locale\n * fallback rule (`Astro.currentLocale ?? websiteConfig.defaultLocale`)\n * MUST stay aligned with the `prefixDefaultLocale: false` convention\n * encoded by `buildAstroI18nConfig`. Centralizing the rule here means\n * a future change (e.g. honoring an `Accept-Language` header before\n * the static default-locale fallback) lands in one place rather than\n * being duplicated across every consumer page's frontmatter.\n *\n * **Why this lives in a `.ts` helper, NOT in a `.astro` shell**: a shipped\n * `WebsitePageBridge.astro` cannot cross-host the page/header/footer\n * `ComponentType` references through the React-island serialization\n * boundary (Astro serializes island props via `devalue`-style JSON;\n * function refs do not survive). Keeping the per-request work in a\n * pure TS helper means the consumer authors their own thin per-page\n * `.astro` file that statically imports each page's bridge island —\n * the component reference is then a JSX tag in the consumer's\n * `.astro` template, which Astro's compiler resolves at build time\n * (no prop serialization).\n *\n * **Failure modes** (all surfaced as thrown errors with actionable\n * messages):\n * - `astroLocale` is provided but is not a member of\n * `siteContext.websiteConfig.locales`: throws — Astro's i18n\n * routing should never present a locale outside the declared set;\n * if it does, `astro.config.mjs` is out of sync with\n * `WildoMarketingWebsiteConfig`.\n * - Label pack file missing / malformed: surfaced by\n * `loadLabelPack` (filesystem error message + locale + path).\n *\n * @param opts.manifest The page manifest the consumer is\n * rendering. Read by `pickLabelsForPage`\n * to filter the loaded label pack down to\n * the keys this page actually consumes\n * (the page's own `website.<ref>.*` slice\n * plus the shared `website.chrome.*`\n * slice). Without this filter every page\n * would serialize the whole site's pack\n * into its `<astro-island props=…>`\n * attribute — see\n * `pick-labels-for-page.ts` for the full\n * rationale.\n * @param opts.siteContext The composed `WebsiteSiteContext`. Read\n * for `websiteConfig.{defaultLocale,\n * locales}` (locale resolution) and\n * `rootConfig.skipToMainContentLabelKey`\n * (chrome key carried into the per-page\n * pack slice).\n * @param opts.labelPacksDir Directory holding `<locale>.json` packs.\n * Resolved relative to the cwd Astro runs\n * in (typically the project root).\n * @param opts.astroLocale Whatever `Astro.currentLocale` returned\n * at the consumer's call site.\n * `string | undefined` — Astro returns\n * `undefined` for the un-prefixed\n * default-locale path.\n */\nexport interface LoadWebsitePageRuntimeOptions {\n /** The page manifest the consumer is rendering (carried for forward-compat). */\n manifest: WebsitePageManifest;\n /** The site's composed `WebsiteSiteContext` — single source of truth. */\n siteContext: WebsiteSiteContext;\n /** Directory holding `<locale>.json` label-pack files. */\n labelPacksDir: string;\n /** The value of `Astro.currentLocale` at the call site. */\n astroLocale: string | undefined;\n}\n\n/**\n * Result of `loadWebsitePageRuntime` — the two primitive values that\n * travel as Astro-island props to the page's bridge island.\n *\n * Both values are JSON-serializable so they cleanly cross Astro's\n * client-island hydration boundary. Function refs (page component,\n * header / footer component) are NOT carried here on purpose; they\n * are closed over by the per-page `createWebsitePageBridgeIsland(...)`\n * call so they never need to cross a serialization boundary. See the\n * file-level JSDoc above and `bridge-runtime.tsx` for the redesign\n * rationale.\n */\nexport interface WebsitePageRuntime {\n /** The resolved locale (Astro's value with default-locale fallback). */\n locale: AvailableLanguage;\n /**\n * The site's default locale (mirrors `websiteConfig.defaultLocale`).\n * Returned alongside `locale` so the consumer can pass both to the\n * bridge island in one symmetric block (`{ locale, defaultLocale }`)\n * — the island then publishes both into `WebsiteRuntimeContext`,\n * which `WebsiteInternalLink` reads to compute locale-prefixed paths.\n * Re-exporting it here avoids forcing every\n * consumer page to import the website config separately.\n */\n defaultLocale: AvailableLanguage;\n /** The loaded + shape-validated label pack for the active locale. */\n labelPack: LabelPack;\n}\n\nexport function loadWebsitePageRuntime(\n opts: LoadWebsitePageRuntimeOptions,\n): WebsitePageRuntime {\n const { manifest, siteContext, labelPacksDir, astroLocale } = opts;\n const { rootConfig, websiteConfig } = siteContext;\n\n /**\n * Locale resolution — the `prefixDefaultLocale: false` convention\n * means Astro serves the default locale at the un-prefixed path\n * (`/`, `/pricing`, …) and reports `undefined` from\n * `Astro.currentLocale` for those requests. Coalesce here to the\n * website config's default rather than letting `undefined` leak\n * into the label-pack lookup.\n */\n const resolvedLocale: AvailableLanguage =\n astroLocale === undefined\n ? websiteConfig.defaultLocale\n : assertKnownLocale(astroLocale, websiteConfig);\n\n const fullPack = loadLabelPack(labelPacksDir, resolvedLocale);\n /**\n * Filter to the keys this page can ever read at runtime — page-own\n * slice + shared chrome slice. The full pack is loaded once (one\n * fs read per request), then sliced; the slimmed result is what\n * crosses the SSR → client serialization boundary inside\n * `<astro-island props=…>`. Sites with O(100) keys per page on\n * O(10) pages save material payload on every HTML response — the\n * untrimmed default would have served every page the entire site's\n * pack on every locale.\n */\n const labelPack = pickLabelsForPage(fullPack, manifest, [\n rootConfig.skipToMainContentLabelKey,\n ]);\n\n return {\n locale: resolvedLocale,\n defaultLocale: websiteConfig.defaultLocale,\n labelPack,\n };\n}\n\n/**\n * Defensive: Astro's i18n routing should only ever present a locale\n * declared on `WildoMarketingWebsiteConfig.locales`. If it does not,\n * the consumer's `astro.config.mjs` is out of sync with the website\n * config — surface that loudly here rather than letting an unknown\n * locale trigger a `[label-pack-loader] Could not read pack` error\n * that would imply a content-authoring fault (missing JSON file)\n * rather than a configuration fault (Astro routing mismatch).\n */\nfunction assertKnownLocale(\n astroLocale: string,\n websiteConfig: WildoMarketingWebsiteConfig,\n): AvailableLanguage {\n for (const entry of websiteConfig.locales) {\n if ((entry.locale as unknown as string) === astroLocale) {\n return entry.locale;\n }\n }\n const declared = websiteConfig.locales.map(\n (entry) => entry.locale as unknown as string,\n );\n throw new Error(\n `[loadWebsitePageRuntime] Astro presented locale \"${astroLocale}\" but ` +\n `WildoMarketingWebsiteConfig.locales declares only [${declared.join(', ')}]. ` +\n `Either add \"${astroLocale}\" to wildo.website.config.ts's locales or update ` +\n `astro.config.mjs to consume buildAstroI18nConfig(WEBSITE_SITE_CONTEXT) so ` +\n `both sides stay in sync.`,\n );\n}\n/** @wildo_source:part:end saas.website.astro.runtime-helper */\n"]}