@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 { WebsiteSiteContext } from './website-site-context';
2
+ /**
3
+ * @wildo_source:part:start saas.website.astro.robots-renderer facet:layer:astro facet:family:website
4
+ *
5
+ * `renderRobotsTxt(siteContext)` — pure string builder for the
6
+ * marketing site's `robots.txt` body. The consumer wires a thin
7
+ * `src/pages/robots.txt.ts` Astro endpoint that imports this helper
8
+ * + `WEBSITE_SITE_CONTEXT` and returns its output as a `Response`
9
+ * with `Content-Type: text/plain` (see `website-seo-aeo-geo` SKILL).
10
+ *
11
+ * # Why a pure string builder (not an Astro component)
12
+ *
13
+ * Same rationale as `renderWebsitePageHeadHtml`:
14
+ * 1. Pure TypeScript ships through the `@wildo-ai/saas-website/astro`
15
+ * subpath barrel (Astro components do not).
16
+ * 2. Vitest-friendly — every policy permutation is asserted
17
+ * against the returned string without an Astro test harness.
18
+ * 3. The Astro endpoint stays a 3-line shim the consumer can
19
+ * audit at a glance.
20
+ *
21
+ * # Output shape
22
+ *
23
+ * The emitted text follows the de-facto `robots.txt` ordering
24
+ * (RFC 9309 §2 + Google's published guidance):
25
+ *
26
+ * 1. Per-bot blocks for `allowedCrawlers` (deny-by-default
27
+ * direction) — each named bot gets its own `User-agent: <bot>`
28
+ * + `Allow: /` block.
29
+ * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default
30
+ * direction) — each named bot gets its own `User-agent: <bot>`
31
+ * + `Disallow: /` block.
32
+ * 3. Catch-all `User-agent: *` block carrying the site-wide
33
+ * `policy` (`Disallow: /` if `policy === 'noindex'` OR if
34
+ * `allowedCrawlers` is set, otherwise nothing) and any
35
+ * `disallow[]` path-prefix entries.
36
+ * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when
37
+ * `seo.sitemap?.enabled !== false` (defaults to `true`).
38
+ * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of
39
+ * whether the site has one or many sitemap files (the index
40
+ * is always-present per the plugin's contract), so this is
41
+ * the stable URL to advertise.
42
+ *
43
+ * # Mutual-exclusion invariant
44
+ *
45
+ * `WebsiteRobotsConfigSchema.superRefine` already guarantees
46
+ * `allowedCrawlers` and `disallowedCrawlers` are NOT both set, so
47
+ * step 1 and step 2 above are mutually exclusive at the input
48
+ * layer. The string builder is defensive (handles both being unset)
49
+ * but does NOT re-validate the invariant.
50
+ *
51
+ * # Why we always emit `Sitemap:` footer when `enabled !== false`
52
+ *
53
+ * The `Sitemap:` directive is independent from any `User-agent`
54
+ * block — it is a top-level signal to any crawler that the site
55
+ * publishes a sitemap. Even when the catch-all policy is
56
+ * `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line
57
+ * stays useful: a crawler that ignores `noindex` (Bing was famous
58
+ * for this) at least has a single canonical URL list to consult,
59
+ * and human readers of `robots.txt` see the publication contract
60
+ * intact.
61
+ *
62
+ * # Failure modes
63
+ *
64
+ * - `siteContext.websiteConfig.publicOrigin` undefined → throws.
65
+ * The marketing-site origin is the SINGLE source of truth for
66
+ * absolute URL composition (see `WildoMarketingWebsiteConfig`
67
+ * JSDoc); a missing origin is a configuration error, not a
68
+ * silent fallback opportunity.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * // examples/<app>/website/src/pages/robots.txt.ts
73
+ * import type { APIRoute } from 'astro';
74
+ * import { renderRobotsTxt } from '@wildo-ai/saas-website/astro';
75
+ * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';
76
+ *
77
+ * export const GET: APIRoute = () =>
78
+ * new Response(renderRobotsTxt(WEBSITE_SITE_CONTEXT), {
79
+ * headers: { 'Content-Type': 'text/plain; charset=utf-8' },
80
+ * });
81
+ * ```
82
+ */
83
+ export declare function renderRobotsTxt(siteContext: WebsiteSiteContext): string;
84
+ /** @wildo_source:part:end saas.website.astro.robots-renderer */
85
+ //# sourceMappingURL=robots-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots-renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/robots-renderer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,kBAAkB,GAAG,MAAM,CAkFvE;AACD,gEAAgE"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @wildo_source:part:start saas.website.astro.robots-renderer facet:layer:astro facet:family:website
3
+ *
4
+ * `renderRobotsTxt(siteContext)` — pure string builder for the
5
+ * marketing site's `robots.txt` body. The consumer wires a thin
6
+ * `src/pages/robots.txt.ts` Astro endpoint that imports this helper
7
+ * + `WEBSITE_SITE_CONTEXT` and returns its output as a `Response`
8
+ * with `Content-Type: text/plain` (see `website-seo-aeo-geo` SKILL).
9
+ *
10
+ * # Why a pure string builder (not an Astro component)
11
+ *
12
+ * Same rationale as `renderWebsitePageHeadHtml`:
13
+ * 1. Pure TypeScript ships through the `@wildo-ai/saas-website/astro`
14
+ * subpath barrel (Astro components do not).
15
+ * 2. Vitest-friendly — every policy permutation is asserted
16
+ * against the returned string without an Astro test harness.
17
+ * 3. The Astro endpoint stays a 3-line shim the consumer can
18
+ * audit at a glance.
19
+ *
20
+ * # Output shape
21
+ *
22
+ * The emitted text follows the de-facto `robots.txt` ordering
23
+ * (RFC 9309 §2 + Google's published guidance):
24
+ *
25
+ * 1. Per-bot blocks for `allowedCrawlers` (deny-by-default
26
+ * direction) — each named bot gets its own `User-agent: <bot>`
27
+ * + `Allow: /` block.
28
+ * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default
29
+ * direction) — each named bot gets its own `User-agent: <bot>`
30
+ * + `Disallow: /` block.
31
+ * 3. Catch-all `User-agent: *` block carrying the site-wide
32
+ * `policy` (`Disallow: /` if `policy === 'noindex'` OR if
33
+ * `allowedCrawlers` is set, otherwise nothing) and any
34
+ * `disallow[]` path-prefix entries.
35
+ * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when
36
+ * `seo.sitemap?.enabled !== false` (defaults to `true`).
37
+ * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of
38
+ * whether the site has one or many sitemap files (the index
39
+ * is always-present per the plugin's contract), so this is
40
+ * the stable URL to advertise.
41
+ *
42
+ * # Mutual-exclusion invariant
43
+ *
44
+ * `WebsiteRobotsConfigSchema.superRefine` already guarantees
45
+ * `allowedCrawlers` and `disallowedCrawlers` are NOT both set, so
46
+ * step 1 and step 2 above are mutually exclusive at the input
47
+ * layer. The string builder is defensive (handles both being unset)
48
+ * but does NOT re-validate the invariant.
49
+ *
50
+ * # Why we always emit `Sitemap:` footer when `enabled !== false`
51
+ *
52
+ * The `Sitemap:` directive is independent from any `User-agent`
53
+ * block — it is a top-level signal to any crawler that the site
54
+ * publishes a sitemap. Even when the catch-all policy is
55
+ * `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line
56
+ * stays useful: a crawler that ignores `noindex` (Bing was famous
57
+ * for this) at least has a single canonical URL list to consult,
58
+ * and human readers of `robots.txt` see the publication contract
59
+ * intact.
60
+ *
61
+ * # Failure modes
62
+ *
63
+ * - `siteContext.websiteConfig.publicOrigin` undefined → throws.
64
+ * The marketing-site origin is the SINGLE source of truth for
65
+ * absolute URL composition (see `WildoMarketingWebsiteConfig`
66
+ * JSDoc); a missing origin is a configuration error, not a
67
+ * silent fallback opportunity.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * // examples/<app>/website/src/pages/robots.txt.ts
72
+ * import type { APIRoute } from 'astro';
73
+ * import { renderRobotsTxt } from '@wildo-ai/saas-website/astro';
74
+ * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';
75
+ *
76
+ * export const GET: APIRoute = () =>
77
+ * new Response(renderRobotsTxt(WEBSITE_SITE_CONTEXT), {
78
+ * headers: { 'Content-Type': 'text/plain; charset=utf-8' },
79
+ * });
80
+ * ```
81
+ */
82
+ export function renderRobotsTxt(siteContext) {
83
+ const { rootConfig, websiteConfig } = siteContext;
84
+ if (!websiteConfig.publicOrigin) {
85
+ throw new Error('renderRobotsTxt: WildoMarketingWebsiteConfig.publicOrigin is required to compose the absolute Sitemap: URL. Author it in your service\'s `wildo.website.config.ts`.');
86
+ }
87
+ const robots = rootConfig.seo.robots;
88
+ const policy = robots?.policy ?? 'index';
89
+ const allowedCrawlers = robots?.allowedCrawlers ?? [];
90
+ const disallowedCrawlers = robots?.disallowedCrawlers ?? [];
91
+ const disallow = robots?.disallow ?? [];
92
+ const blocks = [];
93
+ /**
94
+ * Block 1 — per-bot `Allow: /` for the deny-by-default direction.
95
+ * Empty when `allowedCrawlers` is unset (the dominant case).
96
+ */
97
+ for (const bot of allowedCrawlers) {
98
+ blocks.push(`User-agent: ${bot}\nAllow: /`);
99
+ }
100
+ /**
101
+ * Block 2 — per-bot `Disallow: /` for the allow-by-default
102
+ * direction. Empty when `disallowedCrawlers` is unset (the
103
+ * dominant case for a public marketing site).
104
+ */
105
+ for (const bot of disallowedCrawlers) {
106
+ blocks.push(`User-agent: ${bot}\nDisallow: /`);
107
+ }
108
+ /**
109
+ * Block 3 — catch-all `User-agent: *`. Two policy-driven shapes:
110
+ * - `noindex` → `Disallow: /` (the site-wide closure).
111
+ * - `index` (default) → `Disallow:` is the spec-canonical "allow
112
+ * everything" line; combined with any per-path `Disallow: <path>`
113
+ * entries from `disallow[]`. When `allowedCrawlers` is set, the
114
+ * catch-all also closes the rest of the crawler universe with
115
+ * `Disallow: /` so the named bots are the ONLY ones permitted.
116
+ */
117
+ const catchAllLines = ['User-agent: *'];
118
+ if (policy === 'noindex' || allowedCrawlers.length > 0) {
119
+ catchAllLines.push('Disallow: /');
120
+ }
121
+ else if (disallow.length === 0) {
122
+ /**
123
+ * Spec-canonical "allow everything" — `Disallow:` with no path.
124
+ * Some crawlers treat the absence of any `Disallow:` line as
125
+ * undefined behavior; an explicit empty Disallow is the safe
126
+ * shape.
127
+ */
128
+ catchAllLines.push('Disallow:');
129
+ }
130
+ for (const path of disallow) {
131
+ catchAllLines.push(`Disallow: ${path}`);
132
+ }
133
+ blocks.push(catchAllLines.join('\n'));
134
+ /**
135
+ * Block 4 — `Sitemap:` footer, advertised whenever sitemap
136
+ * emission is enabled (the marketing-site default). See file
137
+ * preamble for why we always advertise `sitemap-index.xml` as
138
+ * the stable URL.
139
+ */
140
+ const sitemapEnabled = rootConfig.seo.sitemap?.enabled ?? true;
141
+ if (sitemapEnabled) {
142
+ /**
143
+ * Trim trailing slash on the origin to avoid emitting
144
+ * `https://example.com//sitemap-index.xml`. `URL` constructor
145
+ * is overkill for a single concat; the regex is enough.
146
+ */
147
+ const origin = websiteConfig.publicOrigin.replace(/\/+$/, '');
148
+ blocks.push(`Sitemap: ${origin}/sitemap-index.xml`);
149
+ }
150
+ /**
151
+ * Spec-conformant trailing newline. Some crawler parsers tolerate
152
+ * its absence; emitting it is universally safe.
153
+ */
154
+ return `${blocks.join('\n\n')}\n`;
155
+ }
156
+ /** @wildo_source:part:end saas.website.astro.robots-renderer */
157
+ //# sourceMappingURL=robots-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots-renderer.js","sourceRoot":"","sources":["../../../../src/astro/robots-renderer.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgFG;AACH,MAAM,UAAU,eAAe,CAAC,WAA+B;IAC7D,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,qKAAqK,CACtK,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAoC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC;IACtE,MAAM,MAAM,GAAwB,MAAM,EAAE,MAAM,IAAI,OAAO,CAAC;IAC9D,MAAM,eAAe,GAAG,MAAM,EAAE,eAAe,IAAI,EAAE,CAAC;IACtD,MAAM,kBAAkB,GAAG,MAAM,EAAE,kBAAkB,IAAI,EAAE,CAAC;IAC5D,MAAM,QAAQ,GAAG,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAC;IAExC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B;;;OAGG;IACH,KAAK,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,eAAe,GAAG,YAAY,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,KAAK,MAAM,GAAG,IAAI,kBAAkB,EAAE,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC,CAAC;IACjD,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,aAAa,GAAa,CAAC,eAAe,CAAC,CAAC;IAClD,IAAI,MAAM,KAAK,SAAS,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvD,aAAa,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACpC,CAAC;SAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC;;;;;WAKG;QACH,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClC,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,aAAa,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAEtC;;;;;OAKG;IACH,MAAM,cAAc,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,IAAI,IAAI,CAAC;IAC/D,IAAI,cAAc,EAAE,CAAC;QACnB;;;;WAIG;QACH,MAAM,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC9D,MAAM,CAAC,IAAI,CAAC,YAAY,MAAM,oBAAoB,CAAC,CAAC;IACtD,CAAC;IAED;;;OAGG;IACH,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;AACpC,CAAC;AACD,gEAAgE","sourcesContent":["import type {\n WebsiteRobotsConfig,\n} from '../schemas/manifests/website-root-config.shared.schemas';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.robots-renderer facet:layer:astro facet:family:website\n *\n * `renderRobotsTxt(siteContext)` — pure string builder for the\n * marketing site's `robots.txt` body. The consumer wires a thin\n * `src/pages/robots.txt.ts` Astro endpoint that imports this helper\n * + `WEBSITE_SITE_CONTEXT` and returns its output as a `Response`\n * with `Content-Type: text/plain` (see `website-seo-aeo-geo` SKILL).\n *\n * # Why a pure string builder (not an Astro component)\n *\n * Same rationale as `renderWebsitePageHeadHtml`:\n * 1. Pure TypeScript ships through the `@wildo-ai/saas-website/astro`\n * subpath barrel (Astro components do not).\n * 2. Vitest-friendly — every policy permutation is asserted\n * against the returned string without an Astro test harness.\n * 3. The Astro endpoint stays a 3-line shim the consumer can\n * audit at a glance.\n *\n * # Output shape\n *\n * The emitted text follows the de-facto `robots.txt` ordering\n * (RFC 9309 §2 + Google's published guidance):\n *\n * 1. Per-bot blocks for `allowedCrawlers` (deny-by-default\n * direction) — each named bot gets its own `User-agent: <bot>`\n * + `Allow: /` block.\n * 2. Per-bot blocks for `disallowedCrawlers` (allow-by-default\n * direction) — each named bot gets its own `User-agent: <bot>`\n * + `Disallow: /` block.\n * 3. Catch-all `User-agent: *` block carrying the site-wide\n * `policy` (`Disallow: /` if `policy === 'noindex'` OR if\n * `allowedCrawlers` is set, otherwise nothing) and any\n * `disallow[]` path-prefix entries.\n * 4. `Sitemap: <publicOrigin>/sitemap-index.xml` footer when\n * `seo.sitemap?.enabled !== false` (defaults to `true`).\n * `@astrojs/sitemap` emits `sitemap-index.xml` regardless of\n * whether the site has one or many sitemap files (the index\n * is always-present per the plugin's contract), so this is\n * the stable URL to advertise.\n *\n * # Mutual-exclusion invariant\n *\n * `WebsiteRobotsConfigSchema.superRefine` already guarantees\n * `allowedCrawlers` and `disallowedCrawlers` are NOT both set, so\n * step 1 and step 2 above are mutually exclusive at the input\n * layer. The string builder is defensive (handles both being unset)\n * but does NOT re-validate the invariant.\n *\n * # Why we always emit `Sitemap:` footer when `enabled !== false`\n *\n * The `Sitemap:` directive is independent from any `User-agent`\n * block — it is a top-level signal to any crawler that the site\n * publishes a sitemap. Even when the catch-all policy is\n * `Disallow: /` (e.g. `policy === 'noindex'`), the `Sitemap:` line\n * stays useful: a crawler that ignores `noindex` (Bing was famous\n * for this) at least has a single canonical URL list to consult,\n * and human readers of `robots.txt` see the publication contract\n * intact.\n *\n * # Failure modes\n *\n * - `siteContext.websiteConfig.publicOrigin` undefined → throws.\n * The marketing-site origin is the SINGLE source of truth for\n * absolute URL composition (see `WildoMarketingWebsiteConfig`\n * JSDoc); a missing origin is a configuration error, not a\n * silent fallback opportunity.\n *\n * @example\n * ```ts\n * // examples/<app>/website/src/pages/robots.txt.ts\n * import type { APIRoute } from 'astro';\n * import { renderRobotsTxt } from '@wildo-ai/saas-website/astro';\n * import { WEBSITE_SITE_CONTEXT } from '@/website-site-context';\n *\n * export const GET: APIRoute = () =>\n * new Response(renderRobotsTxt(WEBSITE_SITE_CONTEXT), {\n * headers: { 'Content-Type': 'text/plain; charset=utf-8' },\n * });\n * ```\n */\nexport function renderRobotsTxt(siteContext: WebsiteSiteContext): string {\n const { rootConfig, websiteConfig } = siteContext;\n\n if (!websiteConfig.publicOrigin) {\n throw new Error(\n 'renderRobotsTxt: WildoMarketingWebsiteConfig.publicOrigin is required to compose the absolute Sitemap: URL. Author it in your service\\'s `wildo.website.config.ts`.',\n );\n }\n\n const robots: WebsiteRobotsConfig | undefined = rootConfig.seo.robots;\n const policy: 'index' | 'noindex' = robots?.policy ?? 'index';\n const allowedCrawlers = robots?.allowedCrawlers ?? [];\n const disallowedCrawlers = robots?.disallowedCrawlers ?? [];\n const disallow = robots?.disallow ?? [];\n\n const blocks: string[] = [];\n\n /**\n * Block 1 — per-bot `Allow: /` for the deny-by-default direction.\n * Empty when `allowedCrawlers` is unset (the dominant case).\n */\n for (const bot of allowedCrawlers) {\n blocks.push(`User-agent: ${bot}\\nAllow: /`);\n }\n\n /**\n * Block 2 — per-bot `Disallow: /` for the allow-by-default\n * direction. Empty when `disallowedCrawlers` is unset (the\n * dominant case for a public marketing site).\n */\n for (const bot of disallowedCrawlers) {\n blocks.push(`User-agent: ${bot}\\nDisallow: /`);\n }\n\n /**\n * Block 3 — catch-all `User-agent: *`. Two policy-driven shapes:\n * - `noindex` → `Disallow: /` (the site-wide closure).\n * - `index` (default) → `Disallow:` is the spec-canonical \"allow\n * everything\" line; combined with any per-path `Disallow: <path>`\n * entries from `disallow[]`. When `allowedCrawlers` is set, the\n * catch-all also closes the rest of the crawler universe with\n * `Disallow: /` so the named bots are the ONLY ones permitted.\n */\n const catchAllLines: string[] = ['User-agent: *'];\n if (policy === 'noindex' || allowedCrawlers.length > 0) {\n catchAllLines.push('Disallow: /');\n } else if (disallow.length === 0) {\n /**\n * Spec-canonical \"allow everything\" — `Disallow:` with no path.\n * Some crawlers treat the absence of any `Disallow:` line as\n * undefined behavior; an explicit empty Disallow is the safe\n * shape.\n */\n catchAllLines.push('Disallow:');\n }\n for (const path of disallow) {\n catchAllLines.push(`Disallow: ${path}`);\n }\n blocks.push(catchAllLines.join('\\n'));\n\n /**\n * Block 4 — `Sitemap:` footer, advertised whenever sitemap\n * emission is enabled (the marketing-site default). See file\n * preamble for why we always advertise `sitemap-index.xml` as\n * the stable URL.\n */\n const sitemapEnabled = rootConfig.seo.sitemap?.enabled ?? true;\n if (sitemapEnabled) {\n /**\n * Trim trailing slash on the origin to avoid emitting\n * `https://example.com//sitemap-index.xml`. `URL` constructor\n * is overkill for a single concat; the regex is enough.\n */\n const origin = websiteConfig.publicOrigin.replace(/\\/+$/, '');\n blocks.push(`Sitemap: ${origin}/sitemap-index.xml`);\n }\n\n /**\n * Spec-conformant trailing newline. Some crawler parsers tolerate\n * its absence; emitting it is universally safe.\n */\n return `${blocks.join('\\n\\n')}\\n`;\n}\n/** @wildo_source:part:end saas.website.astro.robots-renderer */\n"]}
@@ -0,0 +1,101 @@
1
+ import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
2
+ import type { WebsiteSiteContext } from './website-site-context';
3
+ /**
4
+ * @wildo_source:part:start saas.website.astro.sitemap-coverage facet:layer:astro facet:family:website
5
+ *
6
+ * `enumerateMarketingPageSitemapUrls(siteContext)` — pure helper that
7
+ * returns every (page-manifest × locale) absolute URL the marketing
8
+ * site's `sitemap.xml` MUST advertise, paired with its hreflang
9
+ * alternates.
10
+ *
11
+ * # Why this helper exists (vs leaning entirely on `@astrojs/sitemap`)
12
+ *
13
+ * `@astrojs/sitemap` (the official Astro plugin) auto-discovers every
14
+ * page Astro emits and emits a `sitemap-index.xml` + sub-sitemaps at
15
+ * build time. That covers the dominant case (every page in
16
+ * `src/pages/**` ships in the sitemap). What it does NOT cover by
17
+ * itself:
18
+ *
19
+ * 1. **Coverage enforcement at unit-test time.** The plugin runs
20
+ * during `astro build`; vitest cannot exercise it without
21
+ * booting the full Astro pipeline. This helper is the testable
22
+ * surface — a regression that drops a manifest-registered route
23
+ * from the expected URL set fails fast in vitest, well before
24
+ * the `astro build` step would surface it (and even then only
25
+ * via end-to-end inspection of the emitted XML).
26
+ * 2. **Programmatic page registration.** Future framework-owned
27
+ * features that synthesize routes outside Astro's
28
+ * file-based router (e.g. data-driven landing variants) need an
29
+ * explicit URL list to pass into `@astrojs/sitemap`'s
30
+ * `customPages` config. This helper is the canonical place to
31
+ * compute that list — consumers spread the result into the
32
+ * plugin config without re-deriving the locale × route logic.
33
+ * 3. **Cross-check.** The same site context that drives the head
34
+ * renderer drives this enumeration; a divergence between
35
+ * "what the head renderer thinks the canonical URL set is" and
36
+ * "what the sitemap publishes" is a silent drift surface this
37
+ * helper makes auditable.
38
+ *
39
+ * # Output shape
40
+ *
41
+ * Each entry carries:
42
+ * - `url` — the absolute canonical URL (no trailing slash, matches
43
+ * `<link rel="canonical">` output of `renderWebsitePageHeadHtml`).
44
+ * - `pageRef` — the page-manifest `ref` (debug + grouping aid).
45
+ * - `routePath` — the manifest-declared `routePath` (debug + grouping aid).
46
+ * - `locale` — the URL's language.
47
+ * - `alternates` — every other locale's URL for the SAME page,
48
+ * emitted as `xhtml:link rel="alternate" hreflang` entries inside
49
+ * the sitemap (single-sitemap-with-hreflang shape, per Google's
50
+ * recommended sitemap format).
51
+ *
52
+ * # Routing convention (matches `buildLocalizedRoutePath`)
53
+ *
54
+ * Default locale mounts at the un-prefixed path (`/`, `/pricing`);
55
+ * other locales sit under `/<locale>/…`. The helper delegates to
56
+ * `buildLocalizedRoutePath` so a future change to the routing
57
+ * convention lands in one file rather than two.
58
+ *
59
+ * # What this helper does NOT enumerate
60
+ *
61
+ * - **Blog post slugs.** Blog posts share ONE page-manifest ref
62
+ * (`BLOG_POST_PAGE_REF`) but materialize as N URLs — one per
63
+ * `(slug, locale)` MDX file. The MDX file system is the
64
+ * source-of-truth for blog URLs, NOT the page manifest. Astro's
65
+ * file-based router auto-discovers each blog `[slug].astro` route
66
+ * and `@astrojs/sitemap` auto-publishes them. A future helper
67
+ * (`enumerateBlogPostSitemapUrls(...)`) can address the same
68
+ * coverage-enforcement gap for the blog surface specifically.
69
+ * - **Static `.astro` pages without a page manifest.** If a consumer
70
+ * authors a page directly in `src/pages/` without registering a
71
+ * manifest in `WebsiteRootConfig.pageManifests`, this helper will
72
+ * not list it (and `renderWebsitePageHeadHtml` cannot render its
73
+ * `<head>` either). The framework convention is "every public
74
+ * page has a manifest" — pages without manifests are a label-pack
75
+ * discipline regression and surface separately via
76
+ * `assertLabelPacksValid`.
77
+ */
78
+ export interface MarketingPageSitemapEntry {
79
+ /** Absolute canonical URL, no trailing slash. */
80
+ readonly url: string;
81
+ /** The page-manifest `ref` (branded string in the source schema). */
82
+ readonly pageRef: string;
83
+ /** The manifest-declared `routePath` (always starts with `/`). */
84
+ readonly routePath: string;
85
+ /** The URL's language. */
86
+ readonly locale: AvailableLanguage;
87
+ /**
88
+ * Every OTHER locale's URL for the same page, suitable for emitting
89
+ * `xhtml:link rel="alternate" hreflang` entries inside the sitemap.
90
+ * Includes an `x-default` synthesized from the default-locale URL
91
+ * (Google's i18n discoverability convention; matches the head
92
+ * renderer's hreflang output).
93
+ */
94
+ readonly alternates: readonly {
95
+ readonly hreflang: string;
96
+ readonly url: string;
97
+ }[];
98
+ }
99
+ export declare function enumerateMarketingPageSitemapUrls(siteContext: WebsiteSiteContext): readonly MarketingPageSitemapEntry[];
100
+ /** @wildo_source:part:end saas.website.astro.sitemap-coverage */
101
+ //# sourceMappingURL=sitemap-coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-coverage.d.ts","sourceRoot":"","sources":["../../../../src/astro/sitemap-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AAK9E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0EG;AACH,MAAM,WAAW,yBAAyB;IACxC,iDAAiD;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,0BAA0B;IAC1B,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACrF;AAED,wBAAgB,iCAAiC,CAC/C,WAAW,EAAE,kBAAkB,GAC9B,SAAS,yBAAyB,EAAE,CAiFtC;AACD,iEAAiE"}
@@ -0,0 +1,67 @@
1
+ import { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils.js';
2
+ export function enumerateMarketingPageSitemapUrls(siteContext) {
3
+ const { rootConfig, websiteConfig } = siteContext;
4
+ if (!websiteConfig.publicOrigin) {
5
+ throw new Error('enumerateMarketingPageSitemapUrls: WildoMarketingWebsiteConfig.publicOrigin is required to compose absolute sitemap URLs. Author it in your service\'s `wildo.website.config.ts`.');
6
+ }
7
+ /**
8
+ * Trim trailing slash defensively (the schema-level refinement
9
+ * already rejects them, but the helper is consumed in synthetic
10
+ * test contexts that bypass the parser).
11
+ */
12
+ const origin = websiteConfig.publicOrigin.replace(/\/+$/, '');
13
+ const defaultLocale = websiteConfig.defaultLocale;
14
+ const locales = websiteConfig.locales.map((entry) => entry.locale);
15
+ const entries = [];
16
+ for (const manifest of rootConfig.pageManifests) {
17
+ const routePath = manifest.routePath;
18
+ const pageRef = manifest.ref;
19
+ /**
20
+ * Pre-compute every locale's URL for this page once — each entry
21
+ * needs the full set as its `alternates` (with itself excluded
22
+ * later if/when the consumer wants the strict
23
+ * "alternates-don't-include-self" shape; today we keep the full
24
+ * set + add `x-default` for parity with the head renderer's
25
+ * hreflang output).
26
+ */
27
+ const urlsByLocale = new Map();
28
+ for (const locale of locales) {
29
+ const localizedPath = buildLocalizedRoutePath({ locale, defaultLocale, routePath });
30
+ urlsByLocale.set(locale, `${origin}${localizedPath}`);
31
+ }
32
+ const defaultLocaleUrl = urlsByLocale.get(defaultLocale);
33
+ if (defaultLocaleUrl === undefined) {
34
+ throw new Error(`enumerateMarketingPageSitemapUrls: defaultLocale "${String(defaultLocale)}" is missing from WildoMarketingWebsiteConfig.locales (page "${pageRef}"). This is a configuration invariant — locales[] must include the default locale.`);
35
+ }
36
+ for (const locale of locales) {
37
+ const url = urlsByLocale.get(locale);
38
+ if (url === undefined) {
39
+ throw new Error(`enumerateMarketingPageSitemapUrls: missing computed URL for locale "${String(locale)}" on page "${pageRef}" (internal invariant violation).`);
40
+ }
41
+ const alternates = [];
42
+ for (const altLocale of locales) {
43
+ const altUrl = urlsByLocale.get(altLocale);
44
+ if (altUrl !== undefined) {
45
+ alternates.push({ hreflang: String(altLocale), url: altUrl });
46
+ }
47
+ }
48
+ /**
49
+ * `x-default` — Google's hreflang convention for "the URL to
50
+ * serve when no locale signal is available". The marketing-site
51
+ * convention (default locale at the un-prefixed path) makes the
52
+ * default-locale URL the right `x-default` target.
53
+ */
54
+ alternates.push({ hreflang: 'x-default', url: defaultLocaleUrl });
55
+ entries.push({
56
+ url,
57
+ pageRef,
58
+ routePath,
59
+ locale,
60
+ alternates,
61
+ });
62
+ }
63
+ }
64
+ return entries;
65
+ }
66
+ /** @wildo_source:part:end saas.website.astro.sitemap-coverage */
67
+ //# sourceMappingURL=sitemap-coverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-coverage.js","sourceRoot":"","sources":["../../../../src/astro/sitemap-coverage.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AAmGjF,MAAM,UAAU,iCAAiC,CAC/C,WAA+B;IAE/B,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,WAAW,CAAC;IAElD,IAAI,CAAC,aAAa,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACb,mLAAmL,CACpL,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,MAAM,MAAM,GAAG,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC9D,MAAM,aAAa,GAAG,aAAa,CAAC,aAAa,CAAC;IAClD,MAAM,OAAO,GAAiC,aAAa,CAAC,OAAO,CAAC,GAAG,CACrE,CAAC,KAAoC,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CACvD,CAAC;IAEF,MAAM,OAAO,GAAgC,EAAE,CAAC;IAEhD,KAAK,MAAM,QAAQ,IAAI,UAAU,CAAC,aAA+C,EAAE,CAAC;QAClF,MAAM,SAAS,GAAG,QAAQ,CAAC,SAA8B,CAAC;QAC1D,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAwB,CAAC;QAElD;;;;;;;WAOG;QACH,MAAM,YAAY,GAAG,IAAI,GAAG,EAA6B,CAAC;QAC1D,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,aAAa,GAAG,uBAAuB,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAC;YACpF,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,MAAM,GAAG,aAAa,EAAE,CAAC,CAAC;QACxD,CAAC;QAED,MAAM,gBAAgB,GAAG,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACzD,IAAI,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CACb,qDAAqD,MAAM,CAAC,aAAa,CAAC,gEAAgE,OAAO,oFAAoF,CACtO,CAAC;QACJ,CAAC;QAED,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACrC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;gBACtB,MAAM,IAAI,KAAK,CACb,uEAAuE,MAAM,CAAC,MAAM,CAAC,cAAc,OAAO,mCAAmC,CAC9I,CAAC;YACJ,CAAC;YAED,MAAM,UAAU,GAAwC,EAAE,CAAC;YAC3D,KAAK,MAAM,SAAS,IAAI,OAAO,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC3C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACzB,UAAU,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;gBAChE,CAAC;YACH,CAAC;YACD;;;;;eAKG;YACH,UAAU,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,gBAAgB,EAAE,CAAC,CAAC;YAElE,OAAO,CAAC,IAAI,CAAC;gBACX,GAAG;gBACH,OAAO;gBACP,SAAS;gBACT,MAAM;gBACN,UAAU;aACX,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AACD,iEAAiE","sourcesContent":["import type { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';\n\nimport { buildLocalizedRoutePath } from '../core/routing/internal-routing.utils';\nimport type { WebsitePageManifest } from '../schemas/manifests/website-page-manifest.shared.schemas';\n\nimport type { WebsiteSiteContext } from './website-site-context';\n\n/**\n * @wildo_source:part:start saas.website.astro.sitemap-coverage facet:layer:astro facet:family:website\n *\n * `enumerateMarketingPageSitemapUrls(siteContext)` — pure helper that\n * returns every (page-manifest × locale) absolute URL the marketing\n * site's `sitemap.xml` MUST advertise, paired with its hreflang\n * alternates.\n *\n * # Why this helper exists (vs leaning entirely on `@astrojs/sitemap`)\n *\n * `@astrojs/sitemap` (the official Astro plugin) auto-discovers every\n * page Astro emits and emits a `sitemap-index.xml` + sub-sitemaps at\n * build time. That covers the dominant case (every page in\n * `src/pages/**` ships in the sitemap). What it does NOT cover by\n * itself:\n *\n * 1. **Coverage enforcement at unit-test time.** The plugin runs\n * during `astro build`; vitest cannot exercise it without\n * booting the full Astro pipeline. This helper is the testable\n * surface — a regression that drops a manifest-registered route\n * from the expected URL set fails fast in vitest, well before\n * the `astro build` step would surface it (and even then only\n * via end-to-end inspection of the emitted XML).\n * 2. **Programmatic page registration.** Future framework-owned\n * features that synthesize routes outside Astro's\n * file-based router (e.g. data-driven landing variants) need an\n * explicit URL list to pass into `@astrojs/sitemap`'s\n * `customPages` config. This helper is the canonical place to\n * compute that list — consumers spread the result into the\n * plugin config without re-deriving the locale × route logic.\n * 3. **Cross-check.** The same site context that drives the head\n * renderer drives this enumeration; a divergence between\n * \"what the head renderer thinks the canonical URL set is\" and\n * \"what the sitemap publishes\" is a silent drift surface this\n * helper makes auditable.\n *\n * # Output shape\n *\n * Each entry carries:\n * - `url` — the absolute canonical URL (no trailing slash, matches\n * `<link rel=\"canonical\">` output of `renderWebsitePageHeadHtml`).\n * - `pageRef` — the page-manifest `ref` (debug + grouping aid).\n * - `routePath` — the manifest-declared `routePath` (debug + grouping aid).\n * - `locale` — the URL's language.\n * - `alternates` — every other locale's URL for the SAME page,\n * emitted as `xhtml:link rel=\"alternate\" hreflang` entries inside\n * the sitemap (single-sitemap-with-hreflang shape, per Google's\n * recommended sitemap format).\n *\n * # Routing convention (matches `buildLocalizedRoutePath`)\n *\n * Default locale mounts at the un-prefixed path (`/`, `/pricing`);\n * other locales sit under `/<locale>/…`. The helper delegates to\n * `buildLocalizedRoutePath` so a future change to the routing\n * convention lands in one file rather than two.\n *\n * # What this helper does NOT enumerate\n *\n * - **Blog post slugs.** Blog posts share ONE page-manifest ref\n * (`BLOG_POST_PAGE_REF`) but materialize as N URLs — one per\n * `(slug, locale)` MDX file. The MDX file system is the\n * source-of-truth for blog URLs, NOT the page manifest. Astro's\n * file-based router auto-discovers each blog `[slug].astro` route\n * and `@astrojs/sitemap` auto-publishes them. A future helper\n * (`enumerateBlogPostSitemapUrls(...)`) can address the same\n * coverage-enforcement gap for the blog surface specifically.\n * - **Static `.astro` pages without a page manifest.** If a consumer\n * authors a page directly in `src/pages/` without registering a\n * manifest in `WebsiteRootConfig.pageManifests`, this helper will\n * not list it (and `renderWebsitePageHeadHtml` cannot render its\n * `<head>` either). The framework convention is \"every public\n * page has a manifest\" — pages without manifests are a label-pack\n * discipline regression and surface separately via\n * `assertLabelPacksValid`.\n */\nexport interface MarketingPageSitemapEntry {\n /** Absolute canonical URL, no trailing slash. */\n readonly url: string;\n /** The page-manifest `ref` (branded string in the source schema). */\n readonly pageRef: string;\n /** The manifest-declared `routePath` (always starts with `/`). */\n readonly routePath: string;\n /** The URL's language. */\n readonly locale: AvailableLanguage;\n /**\n * Every OTHER locale's URL for the same page, suitable for emitting\n * `xhtml:link rel=\"alternate\" hreflang` entries inside the sitemap.\n * Includes an `x-default` synthesized from the default-locale URL\n * (Google's i18n discoverability convention; matches the head\n * renderer's hreflang output).\n */\n readonly alternates: readonly { readonly hreflang: string; readonly url: string }[];\n}\n\nexport function enumerateMarketingPageSitemapUrls(\n siteContext: WebsiteSiteContext,\n): readonly MarketingPageSitemapEntry[] {\n const { rootConfig, websiteConfig } = siteContext;\n\n if (!websiteConfig.publicOrigin) {\n throw new Error(\n 'enumerateMarketingPageSitemapUrls: WildoMarketingWebsiteConfig.publicOrigin is required to compose absolute sitemap URLs. Author it in your service\\'s `wildo.website.config.ts`.',\n );\n }\n\n /**\n * Trim trailing slash defensively (the schema-level refinement\n * already rejects them, but the helper is consumed in synthetic\n * test contexts that bypass the parser).\n */\n const origin = websiteConfig.publicOrigin.replace(/\\/+$/, '');\n const defaultLocale = websiteConfig.defaultLocale;\n const locales: readonly AvailableLanguage[] = websiteConfig.locales.map(\n (entry: { locale: AvailableLanguage }) => entry.locale,\n );\n\n const entries: MarketingPageSitemapEntry[] = [];\n\n for (const manifest of rootConfig.pageManifests as readonly WebsitePageManifest[]) {\n const routePath = manifest.routePath as unknown as string;\n const pageRef = manifest.ref as unknown as string;\n\n /**\n * Pre-compute every locale's URL for this page once — each entry\n * needs the full set as its `alternates` (with itself excluded\n * later if/when the consumer wants the strict\n * \"alternates-don't-include-self\" shape; today we keep the full\n * set + add `x-default` for parity with the head renderer's\n * hreflang output).\n */\n const urlsByLocale = new Map<AvailableLanguage, string>();\n for (const locale of locales) {\n const localizedPath = buildLocalizedRoutePath({ locale, defaultLocale, routePath });\n urlsByLocale.set(locale, `${origin}${localizedPath}`);\n }\n\n const defaultLocaleUrl = urlsByLocale.get(defaultLocale);\n if (defaultLocaleUrl === undefined) {\n throw new Error(\n `enumerateMarketingPageSitemapUrls: defaultLocale \"${String(defaultLocale)}\" is missing from WildoMarketingWebsiteConfig.locales (page \"${pageRef}\"). This is a configuration invariant — locales[] must include the default locale.`,\n );\n }\n\n for (const locale of locales) {\n const url = urlsByLocale.get(locale);\n if (url === undefined) {\n throw new Error(\n `enumerateMarketingPageSitemapUrls: missing computed URL for locale \"${String(locale)}\" on page \"${pageRef}\" (internal invariant violation).`,\n );\n }\n\n const alternates: { hreflang: string; url: string }[] = [];\n for (const altLocale of locales) {\n const altUrl = urlsByLocale.get(altLocale);\n if (altUrl !== undefined) {\n alternates.push({ hreflang: String(altLocale), url: altUrl });\n }\n }\n /**\n * `x-default` — Google's hreflang convention for \"the URL to\n * serve when no locale signal is available\". The marketing-site\n * convention (default locale at the un-prefixed path) makes the\n * default-locale URL the right `x-default` target.\n */\n alternates.push({ hreflang: 'x-default', url: defaultLocaleUrl });\n\n entries.push({\n url,\n pageRef,\n routePath,\n locale,\n alternates,\n });\n }\n }\n\n return entries;\n}\n/** @wildo_source:part:end saas.website.astro.sitemap-coverage */\n"]}
@@ -0,0 +1,21 @@
1
+ import type { WebsiteStructuredDataPayload } from '../schemas/structured-data/website-structured-data.shared.schemas';
2
+ import type { WebsiteSectionDefinition } from '../schemas/sections/website-section-definition.shared.schemas';
3
+ export interface RenderPageStructuredDataOptions {
4
+ /**
5
+ * Section definitions whose `getStructuredData()` (when present)
6
+ * contributes payloads to the page. Sections without
7
+ * `getStructuredData` are skipped silently.
8
+ */
9
+ sections?: readonly Pick<WebsiteSectionDefinition, 'sectionRef' | 'getStructuredData'>[];
10
+ /**
11
+ * Optional page-level payloads contributed by the page-shell
12
+ * itself (typical: `BreadcrumbList`, `Organization`, `WebSite`).
13
+ * Composed AFTER section payloads in the emission order so a
14
+ * section's `@id`-keyed payload wins over a page-level one with
15
+ * the same `@id` (last-wins; see preamble).
16
+ */
17
+ pagePayloads?: readonly WebsiteStructuredDataPayload[];
18
+ }
19
+ export declare function renderPageStructuredData(opts: RenderPageStructuredDataOptions): string;
20
+ /** @wildo_source:part:end saas.website.astro.structured-data-renderer */
21
+ //# sourceMappingURL=structured-data.renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-data.renderer.d.ts","sourceRoot":"","sources":["../../../../src/astro/structured-data.renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAE7B,MAAM,mEAAmE,CAAC;AAC3E,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+DAA+D,CAAC;AAgD9G,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,IAAI,CAAC,wBAAwB,EAAE,YAAY,GAAG,mBAAmB,CAAC,EAAE,CAAC;IACzF;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,SAAS,4BAA4B,EAAE,CAAC;CACxD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,+BAA+B,GAAG,MAAM,CAiDtF;AAoDD,yEAAyE"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @wildo_source:part:start saas.website.astro.structured-data-renderer facet:layer:astro facet:family:website
3
+ *
4
+ * `renderPageStructuredData()` — collects every section's
5
+ * `getStructuredData()` return + any explicit page-level payloads,
6
+ * normalizes them, dedupes by `@id`, and emits one
7
+ * `<script type="application/ld+json">` block per surviving payload.
8
+ *
9
+ * **Why one `<script>` per payload (NOT a single `@graph` array)**:
10
+ *
11
+ * - Google's structured-data testing tool, Bing webmaster tools, and
12
+ * the Schema.org reference parser all accept multiple sibling
13
+ * `<script type="application/ld+json">` blocks. Emitting them as
14
+ * separate blocks lets each payload be diff-friendly in the
15
+ * page source AND lets per-payload removal happen on a future
16
+ * AEO opt-out without churning the whole graph.
17
+ * - `@graph` is technically valid but harder to author — every
18
+ * payload would need to share `@context` with the wrapper, and
19
+ * per-section authors would have to coordinate to merge their
20
+ * payloads into one shared graph. The "one script per payload"
21
+ * pattern keeps section authoring fully local.
22
+ *
23
+ * **Why dedupe by `@id` (last-wins)**: a page may have a section
24
+ * emit a `Product` payload AND another section emit a `Review`
25
+ * referencing the same `Product` via `@id`. Without dedupe, the
26
+ * `Product` would emit twice (once standalone, once referenced).
27
+ * Last-wins because the section's own emission is closer to the
28
+ * truth than a reference; the dedupe is best-effort and harmless
29
+ * if `@id` is absent (the payload always emits).
30
+ *
31
+ * **`@context` injection**: when a payload omits `@context`, the
32
+ * renderer injects `'https://schema.org'`. Consumers wanting a
33
+ * custom vocabulary supply `@context` themselves and the renderer
34
+ * passes it through unchanged.
35
+ *
36
+ * **HTML-safe serialization**: JSON-LD payloads are serialized via
37
+ * `JSON.stringify` with the closing `</` escaped to `<\/` (and the
38
+ * `<!` opener escaped to `<\!`) to prevent the payload from
39
+ * accidentally closing the surrounding `<script>` tag if the
40
+ * payload contains the string `</script>` (e.g. a blog post about
41
+ * structured data). This is the canonical safe-serialization
42
+ * pattern (see https://html.spec.whatwg.org/#restrictions-for-contents-of-script-elements).
43
+ */
44
+ const SCHEMA_ORG_DEFAULT_CONTEXT = 'https://schema.org';
45
+ export function renderPageStructuredData(opts) {
46
+ const collected = [];
47
+ if (opts.sections) {
48
+ for (const section of opts.sections) {
49
+ if (!section.getStructuredData)
50
+ continue;
51
+ const value = section.getStructuredData();
52
+ collectInto(collected, value);
53
+ }
54
+ }
55
+ if (opts.pagePayloads) {
56
+ for (const payload of opts.pagePayloads) {
57
+ collected.push(payload);
58
+ }
59
+ }
60
+ if (collected.length === 0)
61
+ return '';
62
+ /**
63
+ * Dedupe by `@id` (last-wins). Payloads without `@id` always
64
+ * survive. We walk LAST-to-FIRST so the LAST occurrence of an
65
+ * `@id` is the kept one (later writes overwrite earlier ones
66
+ * naturally with this loop direction + reverse at the end).
67
+ */
68
+ const seen = new Set();
69
+ const deduped = [];
70
+ for (let i = collected.length - 1; i >= 0; i -= 1) {
71
+ const payload = collected[i];
72
+ if (payload === undefined)
73
+ continue;
74
+ const id = payload['@id'];
75
+ if (typeof id === 'string') {
76
+ if (seen.has(id))
77
+ continue;
78
+ seen.add(id);
79
+ }
80
+ deduped.push(payload);
81
+ }
82
+ deduped.reverse();
83
+ const lines = [];
84
+ for (const payload of deduped) {
85
+ const withContext = payload['@context'] === undefined
86
+ ? { '@context': SCHEMA_ORG_DEFAULT_CONTEXT, ...payload }
87
+ : { ...payload };
88
+ const json = serializeForScriptTag(withContext);
89
+ lines.push(`<script type="application/ld+json">${json}</script>`);
90
+ }
91
+ return lines.join('\n');
92
+ }
93
+ /**
94
+ * Normalize a `getStructuredData()` return value into a flat array
95
+ * the renderer iterates. Handles the four legal shapes
96
+ * (`null` / `undefined` / single payload / readonly array).
97
+ *
98
+ * Mutates the `into` array in place — the function is private to
99
+ * this module and the in-place mutation avoids one allocation per
100
+ * section (sections compose linearly, the cost adds up on long
101
+ * pages).
102
+ */
103
+ function collectInto(into, value) {
104
+ if (value === null || value === undefined)
105
+ return;
106
+ if (Array.isArray(value)) {
107
+ for (const item of value) {
108
+ if (item !== null && item !== undefined)
109
+ into.push(item);
110
+ }
111
+ return;
112
+ }
113
+ /**
114
+ * Single-payload shape — narrow the readonly-array branch above
115
+ * by elimination. `Array.isArray` returns `false` for readonly
116
+ * arrays in some TS configs but TRUE at runtime; this branch
117
+ * runs only for non-array `WebsiteStructuredDataPayload`.
118
+ */
119
+ into.push(value);
120
+ }
121
+ /**
122
+ * Serialize a payload for emission inside a `<script>` tag.
123
+ *
124
+ * `JSON.stringify` alone is NOT safe inside `<script>` tags:
125
+ *
126
+ * - A payload field containing the literal string `</script>`
127
+ * (rare but possible on a blog post about HTML / structured
128
+ * data) would close the script tag prematurely and inject the
129
+ * remainder as page HTML.
130
+ * - A payload field containing `<!--` similarly opens an HTML
131
+ * comment that breaks the surrounding parsing context.
132
+ *
133
+ * Both are mitigated by escaping `<` to `\u003c` inside the JSON
134
+ * payload — the JSON parser treats `\u003c` as identical to `<`,
135
+ * so the consumed payload is unchanged, but the HTML parser sees
136
+ * no `<` characters and never gets confused.
137
+ */
138
+ function serializeForScriptTag(payload) {
139
+ return JSON.stringify(payload).replace(/</g, '\\u003c');
140
+ }
141
+ /** @wildo_source:part:end saas.website.astro.structured-data-renderer */
142
+ //# sourceMappingURL=structured-data.renderer.js.map