@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,85 @@
1
+ import type { WebsiteLabelKey } from '../schemas/label-keys/website-label-key.schemas';
2
+ import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
3
+ import type { LabelPack } from '../schemas/validators/label-pack.validator';
4
+
5
+ /**
6
+ * @wildo_source:part:start saas.website.astro.pick-labels-for-page facet:layer:astro facet:family:website
7
+ *
8
+ * `pickLabelsForPage(pack, manifest)` — return the subset of a loaded
9
+ * label pack that the given page actually consumes at render time.
10
+ *
11
+ * **Why this exists**: the per-page bridge island carries its
12
+ * `labelPack` prop across the SSR → client hydration boundary as
13
+ * serialized JSON inside the `<astro-island props=...>` attribute.
14
+ * Shipping the entire site's pack on every page (every locale × every
15
+ * page = every visitor pays the full pack) inflates the static HTML
16
+ * payload by ~70% on a typical marketing surface where each page reads
17
+ * only its own slice plus the shared chrome slice.
18
+ *
19
+ * **Filter rule** (matches the runtime contract enforced by
20
+ * `useWebsiteLabel` / `useWebsiteLabelByKey`):
21
+ * - Keep keys starting with `website.<manifest.ref>.` — every key a
22
+ * `useWebsiteLabel(slot)` call inside any of this page's sections
23
+ * can ever resolve to (the hook composes
24
+ * `website.<pageRef>.<sectionRef>.<slot>`).
25
+ * - Keep keys starting with `website.chrome.` — the synthetic
26
+ * namespace site-wide chrome (header / footer) reads via
27
+ * `useWebsiteLabelByKey(absoluteKey)`. The chrome contract is
28
+ * declared on `WebsiteRootConfig.chromeLabelKeys` and validated
29
+ * by the label-pack validator.
30
+ *
31
+ * **What gets dropped**: every `website.<otherPage>.…` key. A page
32
+ * cannot legitimately read another page's labels through the hook
33
+ * surface — the hook would either throw (`useWebsiteLabel`) or
34
+ * surface a missing-key diagnostic (`useWebsiteLabelByKey`) — so
35
+ * dropping those keys is observably safe.
36
+ *
37
+ * **Why prefix-based** (vs. walking `expectedKeysBySectionRef` for an
38
+ * exact key list): prefix matching is O(N) over `Object.entries(pack)`
39
+ * with no extra inputs threaded through `loadWebsitePageRuntime`. An
40
+ * exact-list filter would need to know the section-key map at request
41
+ * time, forcing every consumer's `.astro` page to import the
42
+ * collected map — extra config-surface for a tighter filter that
43
+ * saves an additional handful of bytes (the section keys for the
44
+ * page would already match the prefix).
45
+ *
46
+ * **Pure**: no I/O, no observable side-effects. Returns a fresh
47
+ * object — the input `pack` reference is not mutated.
48
+ *
49
+ * @param pack The full loaded label pack for the active locale.
50
+ * @param manifest The page manifest being rendered (drives the
51
+ * `website.<manifest.ref>.*` page-own prefix).
52
+ * @param explicitKeys OPTIONAL allow-list of additional fully-composed
53
+ * label keys to keep regardless of prefix. Used by
54
+ * the framework to guarantee that
55
+ * `WebsiteRootConfig.skipToMainContentLabelKey`
56
+ * reaches the bridge even if a consumer chose to
57
+ * namespace it outside `website.chrome.*` (the
58
+ * framework MUST be able to render the skip link
59
+ * on every page in every locale — Phase 6 audit
60
+ * fix M-α).
61
+ */
62
+ export function pickLabelsForPage(
63
+ pack: LabelPack,
64
+ manifest: WebsitePageManifest,
65
+ explicitKeys: readonly WebsiteLabelKey[] = [],
66
+ ): LabelPack {
67
+ const pagePrefix = `website.${manifest.ref as unknown as string}.`;
68
+ const chromePrefix = 'website.chrome.';
69
+ const explicitKeySet = new Set<string>(
70
+ explicitKeys.map((key) => key as unknown as string),
71
+ );
72
+
73
+ const filtered: LabelPack = {};
74
+ for (const [key, value] of Object.entries(pack)) {
75
+ if (
76
+ key.startsWith(pagePrefix) ||
77
+ key.startsWith(chromePrefix) ||
78
+ explicitKeySet.has(key)
79
+ ) {
80
+ filtered[key] = value;
81
+ }
82
+ }
83
+ return filtered;
84
+ }
85
+ /** @wildo_source:part:end saas.website.astro.pick-labels-for-page */
@@ -0,0 +1,171 @@
1
+ import type {
2
+ WebsiteRobotsConfig,
3
+ } from '../schemas/manifests/website-root-config.shared.schemas';
4
+
5
+ import type { WebsiteSiteContext } from './website-site-context';
6
+
7
+ /**
8
+ * @wildo_source:part:start saas.website.astro.robots-renderer facet:layer:astro facet:family:website
9
+ *
10
+ * `renderRobotsTxt(siteContext)` — pure string builder for the
11
+ * marketing site's `robots.txt` body. The consumer wires a thin
12
+ * `src/pages/robots.txt.ts` Astro endpoint that imports this helper
13
+ * + `WEBSITE_SITE_CONTEXT` and returns its output as a `Response`
14
+ * with `Content-Type: text/plain` (see `website-seo-aeo-geo` SKILL).
15
+ *
16
+ * # Why a pure string builder (not an Astro component)
17
+ *
18
+ * Same rationale as `renderWebsitePageHeadHtml`:
19
+ * 1. Pure TypeScript ships through the `@wildo-ai/saas-website/astro`
20
+ * subpath barrel (Astro components do not).
21
+ * 2. Vitest-friendly — every policy permutation is asserted
22
+ * against the returned string without an Astro test harness.
23
+ * 3. The Astro endpoint stays a 3-line shim the consumer can
24
+ * audit at a glance.
25
+ *
26
+ * # Output shape
27
+ *
28
+ * The emitted text follows the de-facto `robots.txt` ordering
29
+ * (RFC 9309 §2 + Google's published guidance):
30
+ *
31
+ * 1. Per-bot blocks for `allowedCrawlers` (deny-by-default
32
+ * direction) — each named bot gets its own `User-agent: <bot>`
33
+ * + `Allow: /` block.
34
+ * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default
35
+ * direction) — each named bot gets its own `User-agent: <bot>`
36
+ * + `Disallow: /` block.
37
+ * 3. Catch-all `User-agent: *` block carrying the site-wide
38
+ * `policy` (`Disallow: /` if `policy === 'noindex'` OR if
39
+ * `allowedCrawlers` is set, otherwise nothing) and any
40
+ * `disallow[]` path-prefix entries.
41
+ * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when
42
+ * `seo.sitemap?.enabled !== false` (defaults to `true`).
43
+ * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of
44
+ * whether the site has one or many sitemap files (the index
45
+ * is always-present per the plugin's contract), so this is
46
+ * the stable URL to advertise.
47
+ *
48
+ * # Mutual-exclusion invariant
49
+ *
50
+ * `WebsiteRobotsConfigSchema.superRefine` already guarantees
51
+ * `allowedCrawlers` and `disallowedCrawlers` are NOT both set, so
52
+ * step 1 and step 2 above are mutually exclusive at the input
53
+ * layer. The string builder is defensive (handles both being unset)
54
+ * but does NOT re-validate the invariant.
55
+ *
56
+ * # Why we always emit `Sitemap:` footer when `enabled !== false`
57
+ *
58
+ * The `Sitemap:` directive is independent from any `User-agent`
59
+ * block — it is a top-level signal to any crawler that the site
60
+ * publishes a sitemap. Even when the catch-all policy is
61
+ * `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line
62
+ * stays useful: a crawler that ignores `noindex` (Bing was famous
63
+ * for this) at least has a single canonical URL list to consult,
64
+ * and human readers of `robots.txt` see the publication contract
65
+ * intact.
66
+ *
67
+ * # Failure modes
68
+ *
69
+ * - `siteContext.websiteConfig.publicOrigin` undefined → throws.
70
+ * The marketing-site origin is the SINGLE source of truth for
71
+ * absolute URL composition (see `WildoMarketingWebsiteConfig`
72
+ * JSDoc); a missing origin is a configuration error, not a
73
+ * silent fallback opportunity.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // examples/<app>/website/src/pages/robots.txt.ts
78
+ * import type { APIRoute } from 'astro';
79
+ * import { renderRobotsTxt } from '@wildo-ai/saas-website/astro';
80
+ * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';
81
+ *
82
+ * export const GET: APIRoute = () =>
83
+ * new Response(renderRobotsTxt(WEBSITE_SITE_CONTEXT), {
84
+ * headers: { 'Content-Type': 'text/plain; charset=utf-8' },
85
+ * });
86
+ * ```
87
+ */
88
+ export function renderRobotsTxt(siteContext: WebsiteSiteContext): string {
89
+ const { rootConfig, websiteConfig } = siteContext;
90
+
91
+ if (!websiteConfig.publicOrigin) {
92
+ throw new Error(
93
+ 'renderRobotsTxt: WildoMarketingWebsiteConfig.publicOrigin is required to compose the absolute Sitemap: URL. Author it in your service\'s `wildo.website.config.ts`.',
94
+ );
95
+ }
96
+
97
+ const robots: WebsiteRobotsConfig | undefined = rootConfig.seo.robots;
98
+ const policy: 'index' | 'noindex' = robots?.policy ?? 'index';
99
+ const allowedCrawlers = robots?.allowedCrawlers ?? [];
100
+ const disallowedCrawlers = robots?.disallowedCrawlers ?? [];
101
+ const disallow = robots?.disallow ?? [];
102
+
103
+ const blocks: string[] = [];
104
+
105
+ /**
106
+ * Block 1 — per-bot `Allow: /` for the deny-by-default direction.
107
+ * Empty when `allowedCrawlers` is unset (the dominant case).
108
+ */
109
+ for (const bot of allowedCrawlers) {
110
+ blocks.push(`User-agent: ${bot}\nAllow: /`);
111
+ }
112
+
113
+ /**
114
+ * Block 2 — per-bot `Disallow: /` for the allow-by-default
115
+ * direction. Empty when `disallowedCrawlers` is unset (the
116
+ * dominant case for a public marketing site).
117
+ */
118
+ for (const bot of disallowedCrawlers) {
119
+ blocks.push(`User-agent: ${bot}\nDisallow: /`);
120
+ }
121
+
122
+ /**
123
+ * Block 3 — catch-all `User-agent: *`. Two policy-driven shapes:
124
+ * - `noindex` → `Disallow: /` (the site-wide closure).
125
+ * - `index` (default) → `Disallow:` is the spec-canonical "allow
126
+ * everything" line; combined with any per-path `Disallow: <path>`
127
+ * entries from `disallow[]`. When `allowedCrawlers` is set, the
128
+ * catch-all also closes the rest of the crawler universe with
129
+ * `Disallow: /` so the named bots are the ONLY ones permitted.
130
+ */
131
+ const catchAllLines: string[] = ['User-agent: *'];
132
+ if (policy === 'noindex' || allowedCrawlers.length > 0) {
133
+ catchAllLines.push('Disallow: /');
134
+ } else if (disallow.length === 0) {
135
+ /**
136
+ * Spec-canonical "allow everything" — `Disallow:` with no path.
137
+ * Some crawlers treat the absence of any `Disallow:` line as
138
+ * undefined behavior; an explicit empty Disallow is the safe
139
+ * shape.
140
+ */
141
+ catchAllLines.push('Disallow:');
142
+ }
143
+ for (const path of disallow) {
144
+ catchAllLines.push(`Disallow: ${path}`);
145
+ }
146
+ blocks.push(catchAllLines.join('\n'));
147
+
148
+ /**
149
+ * Block 4 — `Sitemap:` footer, advertised whenever sitemap
150
+ * emission is enabled (the marketing-site default). See file
151
+ * preamble for why we always advertise `sitemap-index.xml` as
152
+ * the stable URL.
153
+ */
154
+ const sitemapEnabled = rootConfig.seo.sitemap?.enabled ?? true;
155
+ if (sitemapEnabled) {
156
+ /**
157
+ * Trim trailing slash on the origin to avoid emitting
158
+ * `https://example.com//sitemap-index.xml`. `URL` constructor
159
+ * is overkill for a single concat; the regex is enough.
160
+ */
161
+ const origin = websiteConfig.publicOrigin.replace(/\/+$/, '');
162
+ blocks.push(`Sitemap: ${origin}/sitemap-index.xml`);
163
+ }
164
+
165
+ /**
166
+ * Spec-conformant trailing newline. Some crawler parsers tolerate
167
+ * its absence; emitting it is universally safe.
168
+ */
169
+ return `${blocks.join('\n\n')}\n`;
170
+ }
171
+ /** @wildo_source:part:end saas.website.astro.robots-renderer */
@@ -0,0 +1,186 @@
1
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
+
3
+ import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';
4
+ import type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';
5
+
6
+ import type { WebsiteSiteContext } from './website-site-context';
7
+
8
+ /**
9
+ * @wildo_source:part:start saas.website.astro.sitemap-coverage facet:layer:astro facet:family:website
10
+ *
11
+ * `enumerateMarketingPageSitemapUrls(siteContext)` — pure helper that
12
+ * returns every (page-manifest × locale) absolute URL the marketing
13
+ * site's `sitemap.xml` MUST advertise, paired with its hreflang
14
+ * alternates.
15
+ *
16
+ * # Why this helper exists (vs leaning entirely on `@astrojs/sitemap`)
17
+ *
18
+ * `@astrojs/sitemap` (the official Astro plugin) auto-discovers every
19
+ * page Astro emits and emits a `sitemap-index.xml` + sub-sitemaps at
20
+ * build time. That covers the dominant case (every page in
21
+ * `src/pages/**` ships in the sitemap). What it does NOT cover by
22
+ * itself:
23
+ *
24
+ * 1. **Coverage enforcement at unit-test time.** The plugin runs
25
+ * during `astro build`; vitest cannot exercise it without
26
+ * booting the full Astro pipeline. This helper is the testable
27
+ * surface — a regression that drops a manifest-registered route
28
+ * from the expected URL set fails fast in vitest, well before
29
+ * the `astro build` step would surface it (and even then only
30
+ * via end-to-end inspection of the emitted XML).
31
+ * 2. **Programmatic page registration.** Future framework-owned
32
+ * features that synthesize routes outside Astro's
33
+ * file-based router (e.g. data-driven landing variants) need an
34
+ * explicit URL list to pass into `@astrojs/sitemap`'s
35
+ * `customPages` config. This helper is the canonical place to
36
+ * compute that list — consumers spread the result into the
37
+ * plugin config without re-deriving the locale × route logic.
38
+ * 3. **Cross-check.** The same site context that drives the head
39
+ * renderer drives this enumeration; a divergence between
40
+ * "what the head renderer thinks the canonical URL set is" and
41
+ * "what the sitemap publishes" is a silent drift surface this
42
+ * helper makes auditable.
43
+ *
44
+ * # Output shape
45
+ *
46
+ * Each entry carries:
47
+ * - `url` — the absolute canonical URL (no trailing slash, matches
48
+ * `<link rel="canonical">` output of `renderWebsitePageHeadHtml`).
49
+ * - `pageRef` — the page-manifest `ref` (debug + grouping aid).
50
+ * - `routePath` — the manifest-declared `routePath` (debug + grouping aid).
51
+ * - `locale` — the URL's language.
52
+ * - `alternates` — every other locale's URL for the SAME page,
53
+ * emitted as `xhtml:link rel="alternate" hreflang` entries inside
54
+ * the sitemap (single-sitemap-with-hreflang shape, per Google's
55
+ * recommended sitemap format).
56
+ *
57
+ * # Routing convention (matches `buildLocalizedRoutePath`)
58
+ *
59
+ * Default locale mounts at the un-prefixed path (`/`, `/pricing`);
60
+ * other locales sit under `/<locale>/…`. The helper delegates to
61
+ * `buildLocalizedRoutePath` so a future change to the routing
62
+ * convention lands in one file rather than two.
63
+ *
64
+ * # What this helper does NOT enumerate
65
+ *
66
+ * - **Blog post slugs.** Blog posts share ONE page-manifest ref
67
+ * (`BLOG_POST_PAGE_REF`) but materialize as N URLs — one per
68
+ * `(slug, locale)` MDX file. The MDX file system is the
69
+ * source-of-truth for blog URLs, NOT the page manifest. Astro's
70
+ * file-based router auto-discovers each blog `[slug].astro` route
71
+ * and `@astrojs/sitemap` auto-publishes them. A future helper
72
+ * (`enumerateBlogPostSitemapUrls(...)`) can address the same
73
+ * coverage-enforcement gap for the blog surface specifically.
74
+ * - **Static `.astro` pages without a page manifest.** If a consumer
75
+ * authors a page directly in `src/pages/` without registering a
76
+ * manifest in `WebsiteRootConfig.pageManifests`, this helper will
77
+ * not list it (and `renderWebsitePageHeadHtml` cannot render its
78
+ * `<head>` either). The framework convention is "every public
79
+ * page has a manifest" — pages without manifests are a label-pack
80
+ * discipline regression and surface separately via
81
+ * `assertLabelPacksValid`.
82
+ */
83
+ export interface MarketingPageSitemapEntry {
84
+ /** Absolute canonical URL, no trailing slash. */
85
+ readonly url: string;
86
+ /** The page-manifest `ref` (branded string in the source schema). */
87
+ readonly pageRef: string;
88
+ /** The manifest-declared `routePath` (always starts with `/`). */
89
+ readonly routePath: string;
90
+ /** The URL's language. */
91
+ readonly locale: AvailableLanguage;
92
+ /**
93
+ * Every OTHER locale's URL for the same page, suitable for emitting
94
+ * `xhtml:link rel="alternate" hreflang` entries inside the sitemap.
95
+ * Includes an `x-default` synthesized from the default-locale URL
96
+ * (Google's i18n discoverability convention; matches the head
97
+ * renderer's hreflang output).
98
+ */
99
+ readonly alternates: readonly { readonly hreflang: string; readonly url: string }[];
100
+ }
101
+
102
+ export function enumerateMarketingPageSitemapUrls(
103
+ siteContext: WebsiteSiteContext,
104
+ ): readonly MarketingPageSitemapEntry[] {
105
+ const { rootConfig, websiteConfig } = siteContext;
106
+
107
+ if (!websiteConfig.publicOrigin) {
108
+ throw new Error(
109
+ 'enumerateMarketingPageSitemapUrls: WildoMarketingWebsiteConfig.publicOrigin is required to compose absolute sitemap URLs. Author it in your service\'s `wildo.website.config.ts`.',
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Trim trailing slash defensively (the schema-level refinement
115
+ * already rejects them, but the helper is consumed in synthetic
116
+ * test contexts that bypass the parser).
117
+ */
118
+ const origin = websiteConfig.publicOrigin.replace(/\/+$/, '');
119
+ const defaultLocale = websiteConfig.defaultLocale;
120
+ const locales: readonly AvailableLanguage[] = websiteConfig.locales.map(
121
+ (entry: { locale: AvailableLanguage }) => entry.locale,
122
+ );
123
+
124
+ const entries: MarketingPageSitemapEntry[] = [];
125
+
126
+ for (const manifest of rootConfig.pageManifests as readonly WebsitePageManifest[]) {
127
+ const routePath = manifest.routePath as unknown as string;
128
+ const pageRef = manifest.ref as unknown as string;
129
+
130
+ /**
131
+ * Pre-compute every locale's URL for this page once — each entry
132
+ * needs the full set as its `alternates` (with itself excluded
133
+ * later if/when the consumer wants the strict
134
+ * "alternates-don't-include-self" shape; today we keep the full
135
+ * set + add `x-default` for parity with the head renderer's
136
+ * hreflang output).
137
+ */
138
+ const urlsByLocale = new Map<AvailableLanguage, string>();
139
+ for (const locale of locales) {
140
+ const localizedPath = buildLocalizedRoutePath({ locale, defaultLocale, routePath });
141
+ urlsByLocale.set(locale, `${origin}${localizedPath}`);
142
+ }
143
+
144
+ const defaultLocaleUrl = urlsByLocale.get(defaultLocale);
145
+ if (defaultLocaleUrl === undefined) {
146
+ throw new Error(
147
+ `enumerateMarketingPageSitemapUrls: defaultLocale "${String(defaultLocale)}" is missing from WildoMarketingWebsiteConfig.locales (page "${pageRef}"). This is a configuration invariant — locales[] must include the default locale.`,
148
+ );
149
+ }
150
+
151
+ for (const locale of locales) {
152
+ const url = urlsByLocale.get(locale);
153
+ if (url === undefined) {
154
+ throw new Error(
155
+ `enumerateMarketingPageSitemapUrls: missing computed URL for locale "${String(locale)}" on page "${pageRef}" (internal invariant violation).`,
156
+ );
157
+ }
158
+
159
+ const alternates: { hreflang: string; url: string }[] = [];
160
+ for (const altLocale of locales) {
161
+ const altUrl = urlsByLocale.get(altLocale);
162
+ if (altUrl !== undefined) {
163
+ alternates.push({ hreflang: String(altLocale), url: altUrl });
164
+ }
165
+ }
166
+ /**
167
+ * `x-default` — Google's hreflang convention for "the URL to
168
+ * serve when no locale signal is available". The marketing-site
169
+ * convention (default locale at the un-prefixed path) makes the
170
+ * default-locale URL the right `x-default` target.
171
+ */
172
+ alternates.push({ hreflang: 'x-default', url: defaultLocaleUrl });
173
+
174
+ entries.push({
175
+ url,
176
+ pageRef,
177
+ routePath,
178
+ locale,
179
+ alternates,
180
+ });
181
+ }
182
+ }
183
+
184
+ return entries;
185
+ }
186
+ /** @wildo_source:part:end saas.website.astro.sitemap-coverage */
@@ -0,0 +1,171 @@
1
+ import type {
2
+ WebsiteStructuredDataPayload,
3
+ WebsiteStructuredDataReturn,
4
+ } from '../schemas/structured-data/website-structured-data.shared.schemas';
5
+ import type { WebsiteSectionDefinition } from '../schemas/sections/website-section-definition.shared.schemas';
6
+
7
+ /**
8
+ * @wildo_source:part:start saas.website.astro.structured-data-renderer facet:layer:astro facet:family:website
9
+ *
10
+ * `renderPageStructuredData()` — collects every section's
11
+ * `getStructuredData()` return + any explicit page-level payloads,
12
+ * normalizes them, dedupes by `@id`, and emits one
13
+ * `<script type="application/ld+json">` block per surviving payload.
14
+ *
15
+ * **Why one `<script>` per payload (NOT a single `@graph` array)**:
16
+ *
17
+ * - Google's structured-data testing tool, Bing webmaster tools, and
18
+ * the Schema.org reference parser all accept multiple sibling
19
+ * `<script type="application/ld+json">` blocks. Emitting them as
20
+ * separate blocks lets each payload be diff-friendly in the
21
+ * page source AND lets per-payload removal happen on a future
22
+ * AEO opt-out without churning the whole graph.
23
+ * - `@graph` is technically valid but harder to author — every
24
+ * payload would need to share `@context` with the wrapper, and
25
+ * per-section authors would have to coordinate to merge their
26
+ * payloads into one shared graph. The "one script per payload"
27
+ * pattern keeps section authoring fully local.
28
+ *
29
+ * **Why dedupe by `@id` (last-wins)**: a page may have a section
30
+ * emit a `Product` payload AND another section emit a `Review`
31
+ * referencing the same `Product` via `@id`. Without dedupe, the
32
+ * `Product` would emit twice (once standalone, once referenced).
33
+ * Last-wins because the section's own emission is closer to the
34
+ * truth than a reference; the dedupe is best-effort and harmless
35
+ * if `@id` is absent (the payload always emits).
36
+ *
37
+ * **`@context` injection**: when a payload omits `@context`, the
38
+ * renderer injects `'https://schema.org'`. Consumers wanting a
39
+ * custom vocabulary supply `@context` themselves and the renderer
40
+ * passes it through unchanged.
41
+ *
42
+ * **HTML-safe serialization**: JSON-LD payloads are serialized via
43
+ * `JSON.stringify` with the closing `</` escaped to `<\/` (and the
44
+ * `<!` opener escaped to `<\!`) to prevent the payload from
45
+ * accidentally closing the surrounding `<script>` tag if the
46
+ * payload contains the string `</script>` (e.g. a blog post about
47
+ * structured data). This is the canonical safe-serialization
48
+ * pattern (see https://html.spec.whatwg.org/#restrictions-for-contents-of-script-elements).
49
+ */
50
+
51
+ const SCHEMA_ORG_DEFAULT_CONTEXT = 'https://schema.org' as const;
52
+
53
+ export interface RenderPageStructuredDataOptions {
54
+ /**
55
+ * Section definitions whose `getStructuredData()` (when present)
56
+ * contributes payloads to the page. Sections without
57
+ * `getStructuredData` are skipped silently.
58
+ */
59
+ sections?: readonly Pick<WebsiteSectionDefinition, 'sectionRef' | 'getStructuredData'>[];
60
+ /**
61
+ * Optional page-level payloads contributed by the page-shell
62
+ * itself (typical: `BreadcrumbList`, `Organization`, `WebSite`).
63
+ * Composed AFTER section payloads in the emission order so a
64
+ * section's `@id`-keyed payload wins over a page-level one with
65
+ * the same `@id` (last-wins; see preamble).
66
+ */
67
+ pagePayloads?: readonly WebsiteStructuredDataPayload[];
68
+ }
69
+
70
+ export function renderPageStructuredData(opts: RenderPageStructuredDataOptions): string {
71
+ const collected: WebsiteStructuredDataPayload[] = [];
72
+
73
+ if (opts.sections) {
74
+ for (const section of opts.sections) {
75
+ if (!section.getStructuredData) continue;
76
+ const value: WebsiteStructuredDataReturn = section.getStructuredData();
77
+ collectInto(collected, value);
78
+ }
79
+ }
80
+
81
+ if (opts.pagePayloads) {
82
+ for (const payload of opts.pagePayloads) {
83
+ collected.push(payload);
84
+ }
85
+ }
86
+
87
+ if (collected.length === 0) return '';
88
+
89
+ /**
90
+ * Dedupe by `@id` (last-wins). Payloads without `@id` always
91
+ * survive. We walk LAST-to-FIRST so the LAST occurrence of an
92
+ * `@id` is the kept one (later writes overwrite earlier ones
93
+ * naturally with this loop direction + reverse at the end).
94
+ */
95
+ const seen = new Set<string>();
96
+ const deduped: WebsiteStructuredDataPayload[] = [];
97
+ for (let i = collected.length - 1; i >= 0; i -= 1) {
98
+ const payload = collected[i];
99
+ if (payload === undefined) continue;
100
+ const id = payload['@id'];
101
+ if (typeof id === 'string') {
102
+ if (seen.has(id)) continue;
103
+ seen.add(id);
104
+ }
105
+ deduped.push(payload);
106
+ }
107
+ deduped.reverse();
108
+
109
+ const lines: string[] = [];
110
+ for (const payload of deduped) {
111
+ const withContext: Record<string, unknown> =
112
+ payload['@context'] === undefined
113
+ ? { '@context': SCHEMA_ORG_DEFAULT_CONTEXT, ...payload }
114
+ : { ...payload };
115
+ const json = serializeForScriptTag(withContext);
116
+ lines.push(`<script type="application/ld+json">${json}</script>`);
117
+ }
118
+ return lines.join('\n');
119
+ }
120
+
121
+ /**
122
+ * Normalize a `getStructuredData()` return value into a flat array
123
+ * the renderer iterates. Handles the four legal shapes
124
+ * (`null` / `undefined` / single payload / readonly array).
125
+ *
126
+ * Mutates the `into` array in place — the function is private to
127
+ * this module and the in-place mutation avoids one allocation per
128
+ * section (sections compose linearly, the cost adds up on long
129
+ * pages).
130
+ */
131
+ function collectInto(
132
+ into: WebsiteStructuredDataPayload[],
133
+ value: WebsiteStructuredDataReturn,
134
+ ): void {
135
+ if (value === null || value === undefined) return;
136
+ if (Array.isArray(value)) {
137
+ for (const item of value) {
138
+ if (item !== null && item !== undefined) into.push(item);
139
+ }
140
+ return;
141
+ }
142
+ /**
143
+ * Single-payload shape — narrow the readonly-array branch above
144
+ * by elimination. `Array.isArray` returns `false` for readonly
145
+ * arrays in some TS configs but TRUE at runtime; this branch
146
+ * runs only for non-array `WebsiteStructuredDataPayload`.
147
+ */
148
+ into.push(value as WebsiteStructuredDataPayload);
149
+ }
150
+
151
+ /**
152
+ * Serialize a payload for emission inside a `<script>` tag.
153
+ *
154
+ * `JSON.stringify` alone is NOT safe inside `<script>` tags:
155
+ *
156
+ * - A payload field containing the literal string `</script>`
157
+ * (rare but possible on a blog post about HTML / structured
158
+ * data) would close the script tag prematurely and inject the
159
+ * remainder as page HTML.
160
+ * - A payload field containing `<!--` similarly opens an HTML
161
+ * comment that breaks the surrounding parsing context.
162
+ *
163
+ * Both are mitigated by escaping `<` to `\u003c` inside the JSON
164
+ * payload — the JSON parser treats `\u003c` as identical to `<`,
165
+ * so the consumed payload is unchanged, but the HTML parser sees
166
+ * no `<` characters and never gets confused.
167
+ */
168
+ function serializeForScriptTag(payload: Record<string, unknown>): string {
169
+ return JSON.stringify(payload).replace(/</g, '\\u003c');
170
+ }
171
+ /** @wildo_source:part:end saas.website.astro.structured-data-renderer */