@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,228 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
4
+
5
+ import { WildoMarketingWebsiteConfigSchema } from '../../config/wildo-website-config.schemas';
6
+ import { WebsiteLabelKeySchema } from '../../schemas/label-keys/website-label-key.schemas';
7
+ import {
8
+ WebsitePageManifestSchema,
9
+ type WebsitePageManifest,
10
+ } from '../../schemas/manifests/website-page-manifest.shared.schemas';
11
+ import {
12
+ WebsiteRootConfigSchema,
13
+ type WebsiteRootConfig,
14
+ type WebsiteRobotsConfig,
15
+ type WebsiteSitemapConfig,
16
+ } from '../../schemas/manifests/website-root-config.shared.schemas';
17
+ import { WebsitePageRefSchema } from '../../schemas/refs/page-ref.schemas';
18
+ import { WebsiteSectionRefSchema } from '../../schemas/refs/section-ref.schemas';
19
+ import { renderRobotsTxt } from '../robots-renderer';
20
+ import { composeWebsiteSiteContext, type WebsiteSiteContext } from '../website-site-context';
21
+
22
+ const NoopComponent = () => null;
23
+
24
+ const COLORS = {
25
+ background: '#fff',
26
+ foreground: '#111',
27
+ primary: '#0070f3',
28
+ primaryForeground: '#fff',
29
+ accent: '#ff5e5b',
30
+ accentForeground: '#fff',
31
+ surface: '#fafafa',
32
+ surfaceForeground: '#111',
33
+ border: '#e5e5e5',
34
+ muted: '#f5f5f5',
35
+ mutedForeground: '#737373',
36
+ };
37
+ const RADII = {
38
+ none: '0',
39
+ sm: '0.125rem',
40
+ md: '0.375rem',
41
+ lg: '0.75rem',
42
+ xl: '1.5rem',
43
+ full: '9999px',
44
+ };
45
+ const DESIGN_TOKENS = { colors: COLORS, radii: RADII };
46
+
47
+ const buildManifest = (ref: string, routePath: string): WebsitePageManifest =>
48
+ WebsitePageManifestSchema.parse({
49
+ ref: WebsitePageRefSchema.parse(ref),
50
+ routePath,
51
+ pageComponent: NoopComponent,
52
+ sectionRefs: [WebsiteSectionRefSchema.parse('hero')],
53
+ metaLabelKeys: {
54
+ titleKey: WebsiteLabelKeySchema.parse(`website.${ref}.meta.title`),
55
+ descriptionKey: WebsiteLabelKeySchema.parse(`website.${ref}.meta.description`),
56
+ },
57
+ }) as WebsitePageManifest;
58
+
59
+ interface BuildSiteContextOverrides {
60
+ robots?: WebsiteRobotsConfig | undefined;
61
+ sitemap?: WebsiteSitemapConfig | undefined;
62
+ publicOrigin?: string;
63
+ }
64
+
65
+ const buildSiteContext = (overrides: BuildSiteContextOverrides = {}): WebsiteSiteContext => {
66
+ const rootConfig = WebsiteRootConfigSchema.parse({
67
+ pageManifests: [buildManifest('landing', '/')],
68
+ designTokens: DESIGN_TOKENS,
69
+ navigation: { headerComponent: NoopComponent, footerComponent: NoopComponent },
70
+ seo: {
71
+ siteName: 'Example Site',
72
+ ...(overrides.robots !== undefined ? { robots: overrides.robots } : {}),
73
+ ...(overrides.sitemap !== undefined ? { sitemap: overrides.sitemap } : {}),
74
+ },
75
+ skipToMainContentLabelKey: WebsiteLabelKeySchema.parse(
76
+ 'website.chrome.layout.skip-to-main-content',
77
+ ),
78
+ }) as WebsiteRootConfig;
79
+
80
+ const websiteConfig = WildoMarketingWebsiteConfigSchema.parse({
81
+ publicOrigin: overrides.publicOrigin ?? 'https://example.com',
82
+ appOrigin: 'https://app.example.com',
83
+ defaultLocale: AvailableLanguage.EN,
84
+ locales: [{ locale: AvailableLanguage.EN, path: 'en' }],
85
+ });
86
+
87
+ return composeWebsiteSiteContext({ rootConfig, websiteConfig });
88
+ };
89
+
90
+ describe('renderRobotsTxt (Phase 8 close-out — saas-website-seo-sitemap.md)', () => {
91
+ describe('default policy (no robots block authored)', () => {
92
+ it('emits a permissive User-agent: * block + Sitemap: footer', () => {
93
+ const txt = renderRobotsTxt(buildSiteContext());
94
+ expect(txt).toBe(
95
+ 'User-agent: *\nDisallow:\n\nSitemap: https://example.com/sitemap-index.xml\n',
96
+ );
97
+ });
98
+
99
+ it('strips a trailing slash on the public origin (defense in depth — schema already refuses it)', () => {
100
+ /**
101
+ * The schema-level refinement already rejects trailing slashes,
102
+ * so we exercise the renderer's defense-in-depth path by
103
+ * bypassing the parser via a hand-built site context.
104
+ */
105
+ const handBuiltContext: WebsiteSiteContext = {
106
+ rootConfig: buildSiteContext().rootConfig,
107
+ websiteConfig: {
108
+ ...buildSiteContext().websiteConfig,
109
+ publicOrigin: 'https://example.com/',
110
+ },
111
+ };
112
+ const txt = renderRobotsTxt(handBuiltContext);
113
+ expect(txt).toContain('Sitemap: https://example.com/sitemap-index.xml');
114
+ expect(txt).not.toContain('//sitemap-index.xml');
115
+ });
116
+ });
117
+
118
+ describe('policy = noindex (staging / private surfaces)', () => {
119
+ it('emits Disallow: / for the catch-all User-agent', () => {
120
+ const txt = renderRobotsTxt(buildSiteContext({ robots: { policy: 'noindex' } }));
121
+ expect(txt).toContain('User-agent: *\nDisallow: /');
122
+ });
123
+
124
+ it('STILL emits the Sitemap: footer when noindex (crawler-ignoring-noindex defense)', () => {
125
+ const txt = renderRobotsTxt(buildSiteContext({ robots: { policy: 'noindex' } }));
126
+ expect(txt).toContain('Sitemap: https://example.com/sitemap-index.xml');
127
+ });
128
+ });
129
+
130
+ describe('disallow path-prefix list', () => {
131
+ it('emits one Disallow line per entry under the catch-all block', () => {
132
+ const txt = renderRobotsTxt(
133
+ buildSiteContext({
134
+ robots: { policy: 'index', disallow: ['/internal/', '/preview/'] },
135
+ }),
136
+ );
137
+ expect(txt).toContain('User-agent: *\nDisallow: /internal/\nDisallow: /preview/');
138
+ /**
139
+ * When per-path disallow entries are present, the spec-canonical
140
+ * empty `Disallow:` line is NOT emitted (the per-path entries
141
+ * carry the policy intent).
142
+ */
143
+ expect(txt).not.toMatch(/User-agent: \*\nDisallow:\n/);
144
+ });
145
+ });
146
+
147
+ describe('allowedCrawlers (deny-by-default direction)', () => {
148
+ it('emits a per-bot Allow: / block for each named crawler', () => {
149
+ const txt = renderRobotsTxt(
150
+ buildSiteContext({
151
+ robots: { policy: 'index', allowedCrawlers: ['Googlebot', 'Bingbot'] },
152
+ }),
153
+ );
154
+ expect(txt).toContain('User-agent: Googlebot\nAllow: /');
155
+ expect(txt).toContain('User-agent: Bingbot\nAllow: /');
156
+ });
157
+
158
+ it('closes the rest of the crawler universe with Disallow: / on the catch-all', () => {
159
+ const txt = renderRobotsTxt(
160
+ buildSiteContext({
161
+ robots: { policy: 'index', allowedCrawlers: ['Googlebot'] },
162
+ }),
163
+ );
164
+ expect(txt).toContain('User-agent: *\nDisallow: /');
165
+ });
166
+ });
167
+
168
+ describe('disallowedCrawlers (allow-by-default direction)', () => {
169
+ it('emits a per-bot Disallow: / block for each named crawler', () => {
170
+ const txt = renderRobotsTxt(
171
+ buildSiteContext({
172
+ robots: { policy: 'index', disallowedCrawlers: ['GPTBot', 'CCBot'] },
173
+ }),
174
+ );
175
+ expect(txt).toContain('User-agent: GPTBot\nDisallow: /');
176
+ expect(txt).toContain('User-agent: CCBot\nDisallow: /');
177
+ });
178
+
179
+ it('keeps the catch-all permissive (Disallow:) when only disallowedCrawlers are set', () => {
180
+ const txt = renderRobotsTxt(
181
+ buildSiteContext({
182
+ robots: { policy: 'index', disallowedCrawlers: ['GPTBot'] },
183
+ }),
184
+ );
185
+ expect(txt).toContain('User-agent: *\nDisallow:\n');
186
+ });
187
+ });
188
+
189
+ describe('sitemap.enabled', () => {
190
+ it('omits the Sitemap: footer when sitemap.enabled = false (staging suppression)', () => {
191
+ const txt = renderRobotsTxt(buildSiteContext({ sitemap: { enabled: false } }));
192
+ expect(txt).not.toContain('Sitemap:');
193
+ });
194
+
195
+ it('emits the Sitemap: footer when sitemap is omitted (default = enabled)', () => {
196
+ const txt = renderRobotsTxt(buildSiteContext());
197
+ expect(txt).toContain('Sitemap: https://example.com/sitemap-index.xml');
198
+ });
199
+ });
200
+
201
+ describe('failure modes', () => {
202
+ it('throws when publicOrigin is missing (configuration error, not silent fallback)', () => {
203
+ const handBuiltContext = {
204
+ rootConfig: buildSiteContext().rootConfig,
205
+ websiteConfig: {
206
+ ...buildSiteContext().websiteConfig,
207
+ publicOrigin: '',
208
+ },
209
+ } as WebsiteSiteContext;
210
+ expect(() => renderRobotsTxt(handBuiltContext)).toThrow(/publicOrigin/);
211
+ });
212
+ });
213
+
214
+ describe('output shape', () => {
215
+ it('always ends with a trailing newline (spec-conformant)', () => {
216
+ expect(renderRobotsTxt(buildSiteContext())).toMatch(/\n$/);
217
+ });
218
+
219
+ it('separates blocks with one blank line', () => {
220
+ const txt = renderRobotsTxt(
221
+ buildSiteContext({
222
+ robots: { policy: 'index', allowedCrawlers: ['Googlebot'] },
223
+ }),
224
+ );
225
+ expect(txt.split('\n\n').length).toBeGreaterThanOrEqual(3);
226
+ });
227
+ });
228
+ });
@@ -0,0 +1,230 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
4
+
5
+ import { WildoMarketingWebsiteConfigSchema } from '../../config/wildo-website-config.schemas';
6
+ import { WebsiteLabelKeySchema } from '../../schemas/label-keys/website-label-key.schemas';
7
+ import {
8
+ WebsitePageManifestSchema,
9
+ type WebsitePageManifest,
10
+ } from '../../schemas/manifests/website-page-manifest.shared.schemas';
11
+ import {
12
+ WebsiteRootConfigSchema,
13
+ type WebsiteRootConfig,
14
+ } from '../../schemas/manifests/website-root-config.shared.schemas';
15
+ import { WebsitePageRefSchema } from '../../schemas/refs/page-ref.schemas';
16
+ import { WebsiteSectionRefSchema } from '../../schemas/refs/section-ref.schemas';
17
+ import { enumerateMarketingPageSitemapUrls } from '../sitemap-coverage';
18
+ import { composeWebsiteSiteContext, type WebsiteSiteContext } from '../website-site-context';
19
+
20
+ const NoopComponent = () => null;
21
+
22
+ const COLORS = {
23
+ background: '#fff',
24
+ foreground: '#111',
25
+ primary: '#0070f3',
26
+ primaryForeground: '#fff',
27
+ accent: '#ff5e5b',
28
+ accentForeground: '#fff',
29
+ surface: '#fafafa',
30
+ surfaceForeground: '#111',
31
+ border: '#e5e5e5',
32
+ muted: '#f5f5f5',
33
+ mutedForeground: '#737373',
34
+ };
35
+ const RADII = {
36
+ none: '0',
37
+ sm: '0.125rem',
38
+ md: '0.375rem',
39
+ lg: '0.75rem',
40
+ xl: '1.5rem',
41
+ full: '9999px',
42
+ };
43
+ const DESIGN_TOKENS = { colors: COLORS, radii: RADII };
44
+
45
+ const buildManifest = (ref: string, routePath: string): WebsitePageManifest =>
46
+ WebsitePageManifestSchema.parse({
47
+ ref: WebsitePageRefSchema.parse(ref),
48
+ routePath,
49
+ pageComponent: NoopComponent,
50
+ sectionRefs: [WebsiteSectionRefSchema.parse('hero')],
51
+ metaLabelKeys: {
52
+ titleKey: WebsiteLabelKeySchema.parse(`website.${ref}.meta.title`),
53
+ descriptionKey: WebsiteLabelKeySchema.parse(`website.${ref}.meta.description`),
54
+ },
55
+ }) as WebsitePageManifest;
56
+
57
+ interface BuildContextOpts {
58
+ manifests?: WebsitePageManifest[];
59
+ locales?: AvailableLanguage[];
60
+ defaultLocale?: AvailableLanguage;
61
+ publicOrigin?: string;
62
+ }
63
+
64
+ const buildSiteContext = (opts: BuildContextOpts = {}): WebsiteSiteContext => {
65
+ const manifests = opts.manifests ?? [
66
+ buildManifest('landing', '/'),
67
+ buildManifest('pricing', '/pricing'),
68
+ buildManifest('contact', '/contact'),
69
+ ];
70
+ const locales = opts.locales ?? [AvailableLanguage.EN, AvailableLanguage.FR];
71
+ const defaultLocale = opts.defaultLocale ?? AvailableLanguage.EN;
72
+
73
+ const rootConfig = WebsiteRootConfigSchema.parse({
74
+ pageManifests: manifests,
75
+ designTokens: DESIGN_TOKENS,
76
+ navigation: { headerComponent: NoopComponent, footerComponent: NoopComponent },
77
+ seo: { siteName: 'Example' },
78
+ skipToMainContentLabelKey: WebsiteLabelKeySchema.parse(
79
+ 'website.chrome.layout.skip-to-main-content',
80
+ ),
81
+ }) as WebsiteRootConfig;
82
+
83
+ const websiteConfig = WildoMarketingWebsiteConfigSchema.parse({
84
+ publicOrigin: opts.publicOrigin ?? 'https://example.com',
85
+ appOrigin: 'https://app.example.com',
86
+ defaultLocale,
87
+ locales: locales.map((locale) => ({ locale, path: String(locale) })),
88
+ });
89
+
90
+ return composeWebsiteSiteContext({ rootConfig, websiteConfig });
91
+ };
92
+
93
+ describe('enumerateMarketingPageSitemapUrls (Phase 8 close-out — sitemap-coverage enforcement)', () => {
94
+ describe('coverage invariant — every (page × locale) appears as a URL entry', () => {
95
+ it('emits N pages × M locales URLs for a multi-page multi-locale config', () => {
96
+ const ctx = buildSiteContext({
97
+ manifests: [
98
+ buildManifest('landing', '/'),
99
+ buildManifest('pricing', '/pricing'),
100
+ buildManifest('contact', '/contact'),
101
+ ],
102
+ locales: [AvailableLanguage.EN, AvailableLanguage.FR],
103
+ });
104
+ const entries = enumerateMarketingPageSitemapUrls(ctx);
105
+ expect(entries).toHaveLength(3 * 2);
106
+ });
107
+
108
+ it('preserves the manifest order × locale order in the output', () => {
109
+ const ctx = buildSiteContext({
110
+ manifests: [buildManifest('landing', '/'), buildManifest('pricing', '/pricing')],
111
+ locales: [AvailableLanguage.EN, AvailableLanguage.FR],
112
+ });
113
+ const entries = enumerateMarketingPageSitemapUrls(ctx);
114
+ expect(entries.map((e) => `${e.pageRef}/${String(e.locale)}`)).toEqual([
115
+ 'landing/en',
116
+ 'landing/fr',
117
+ 'pricing/en',
118
+ 'pricing/fr',
119
+ ]);
120
+ });
121
+ });
122
+
123
+ describe('URL composition (matches buildLocalizedRoutePath)', () => {
124
+ it('mounts the default locale at the un-prefixed path', () => {
125
+ const ctx = buildSiteContext({
126
+ manifests: [buildManifest('landing', '/'), buildManifest('pricing', '/pricing')],
127
+ locales: [AvailableLanguage.EN, AvailableLanguage.FR],
128
+ defaultLocale: AvailableLanguage.EN,
129
+ });
130
+ const entries = enumerateMarketingPageSitemapUrls(ctx);
131
+ const enLanding = entries.find((e) => e.pageRef === 'landing' && e.locale === AvailableLanguage.EN);
132
+ const enPricing = entries.find((e) => e.pageRef === 'pricing' && e.locale === AvailableLanguage.EN);
133
+ expect(enLanding?.url).toBe('https://example.com/');
134
+ expect(enPricing?.url).toBe('https://example.com/pricing');
135
+ });
136
+
137
+ it('prefixes non-default locales with /<locale>/', () => {
138
+ const ctx = buildSiteContext({
139
+ manifests: [buildManifest('landing', '/'), buildManifest('pricing', '/pricing')],
140
+ locales: [AvailableLanguage.EN, AvailableLanguage.FR],
141
+ defaultLocale: AvailableLanguage.EN,
142
+ });
143
+ const entries = enumerateMarketingPageSitemapUrls(ctx);
144
+ const frLanding = entries.find((e) => e.pageRef === 'landing' && e.locale === AvailableLanguage.FR);
145
+ const frPricing = entries.find((e) => e.pageRef === 'pricing' && e.locale === AvailableLanguage.FR);
146
+ /**
147
+ * `/` → `/fr` (no trailing slash for the locale-prefixed root).
148
+ */
149
+ expect(frLanding?.url).toBe('https://example.com/fr');
150
+ expect(frPricing?.url).toBe('https://example.com/fr/pricing');
151
+ });
152
+ });
153
+
154
+ describe('hreflang alternates (single-sitemap-with-hreflang shape)', () => {
155
+ it('includes one alternate per locale + an x-default pointing at the default-locale URL', () => {
156
+ const ctx = buildSiteContext({
157
+ manifests: [buildManifest('pricing', '/pricing')],
158
+ locales: [AvailableLanguage.EN, AvailableLanguage.FR],
159
+ defaultLocale: AvailableLanguage.EN,
160
+ });
161
+ const entries = enumerateMarketingPageSitemapUrls(ctx);
162
+ const enEntry = entries.find((e) => e.locale === AvailableLanguage.EN);
163
+ expect(enEntry?.alternates).toEqual([
164
+ { hreflang: 'en', url: 'https://example.com/pricing' },
165
+ { hreflang: 'fr', url: 'https://example.com/fr/pricing' },
166
+ { hreflang: 'x-default', url: 'https://example.com/pricing' },
167
+ ]);
168
+ });
169
+
170
+ it('every entry for a given page shares the same alternate URL set', () => {
171
+ const ctx = buildSiteContext({
172
+ manifests: [buildManifest('pricing', '/pricing')],
173
+ locales: [AvailableLanguage.EN, AvailableLanguage.FR],
174
+ });
175
+ const entries = enumerateMarketingPageSitemapUrls(ctx);
176
+ const [a, b] = entries;
177
+ expect(a?.alternates.map((x) => x.url)).toEqual(b?.alternates.map((x) => x.url));
178
+ });
179
+ });
180
+
181
+ describe('failure modes', () => {
182
+ it('throws when publicOrigin is missing (configuration error)', () => {
183
+ const ctx = buildSiteContext();
184
+ const broken: WebsiteSiteContext = {
185
+ rootConfig: ctx.rootConfig,
186
+ websiteConfig: { ...ctx.websiteConfig, publicOrigin: '' },
187
+ };
188
+ expect(() => enumerateMarketingPageSitemapUrls(broken)).toThrow(/publicOrigin/);
189
+ });
190
+ });
191
+
192
+ describe('coverage REGRESSION guard — the test that fails when a route silently drops out', () => {
193
+ it('listing the full URL set against expected (page × locale) tuples — every route must appear exactly once', () => {
194
+ const manifests = [
195
+ buildManifest('landing', '/'),
196
+ buildManifest('features', '/features'),
197
+ buildManifest('pricing', '/pricing'),
198
+ buildManifest('contact', '/contact'),
199
+ buildManifest('not-found', '/404'),
200
+ ];
201
+ const locales = [AvailableLanguage.EN, AvailableLanguage.FR];
202
+ const ctx = buildSiteContext({ manifests, locales, defaultLocale: AvailableLanguage.EN });
203
+
204
+ const expected = new Set<string>();
205
+ for (const m of manifests) {
206
+ for (const locale of locales) {
207
+ expected.add(`${m.ref as unknown as string}|${String(locale)}`);
208
+ }
209
+ }
210
+
211
+ const actual = new Set(
212
+ enumerateMarketingPageSitemapUrls(ctx).map((e) => `${e.pageRef}|${String(e.locale)}`),
213
+ );
214
+
215
+ expect(actual).toEqual(expected);
216
+ /**
217
+ * Negative: removing any single manifest from the input must
218
+ * shrink the output by `locales.length` entries — direct
219
+ * proof that the helper is the enforcement seam (no other
220
+ * stage can silently re-add a dropped page).
221
+ */
222
+ const ctxMissingPricing = buildSiteContext({
223
+ manifests: manifests.filter((m) => (m.ref as unknown as string) !== 'pricing'),
224
+ locales,
225
+ });
226
+ const after = enumerateMarketingPageSitemapUrls(ctxMissingPricing);
227
+ expect(after).toHaveLength(actual.size - locales.length);
228
+ });
229
+ });
230
+ });
@@ -0,0 +1,172 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { WebsiteSectionDefinition } from '../../schemas/sections/website-section-definition.shared.schemas';
4
+ import type {
5
+ WebsiteStructuredDataPayload,
6
+ WebsiteStructuredDataReturn,
7
+ } from '../../schemas/structured-data/website-structured-data.shared.schemas';
8
+ import { renderPageStructuredData } from '../structured-data.renderer';
9
+
10
+ /**
11
+ * Tiny helper that fabricates a section-shaped object — the renderer
12
+ * only reads `getStructuredData` (and `sectionRef` for type
13
+ * compatibility), so we can hand-roll a partial that satisfies
14
+ * `Pick<WebsiteSectionDefinition, 'sectionRef' | 'getStructuredData'>`
15
+ * without dragging the full section schema through every test.
16
+ */
17
+ function makeSection(
18
+ sectionRef: string,
19
+ getStructuredData?: () => WebsiteStructuredDataReturn,
20
+ ): Pick<WebsiteSectionDefinition, 'sectionRef' | 'getStructuredData'> {
21
+ return {
22
+ sectionRef: sectionRef as unknown as WebsiteSectionDefinition['sectionRef'],
23
+ ...(getStructuredData ? { getStructuredData } : {}),
24
+ };
25
+ }
26
+
27
+ describe('renderPageStructuredData', () => {
28
+ it('returns empty string when no sections, no page payloads', () => {
29
+ expect(renderPageStructuredData({})).toBe('');
30
+ });
31
+
32
+ it('returns empty string when every section returns null/undefined/[]', () => {
33
+ const sections = [
34
+ makeSection('a', () => null),
35
+ makeSection('b', () => undefined),
36
+ makeSection('c', () => []),
37
+ ];
38
+ expect(renderPageStructuredData({ sections })).toBe('');
39
+ });
40
+
41
+ it('skips sections without `getStructuredData`', () => {
42
+ const sections = [makeSection('a'), makeSection('b')];
43
+ expect(renderPageStructuredData({ sections })).toBe('');
44
+ });
45
+
46
+ it('emits one `<script>` per single payload, with `@context` injected when omitted', () => {
47
+ const payload: WebsiteStructuredDataPayload = {
48
+ '@type': 'Article',
49
+ headline: 'Hello',
50
+ };
51
+ const result = renderPageStructuredData({
52
+ sections: [makeSection('hero', () => payload)],
53
+ });
54
+ expect(result).toContain('<script type="application/ld+json">');
55
+ expect(result).toContain('</script>');
56
+ expect(result).toContain('"@context":"https://schema.org"');
57
+ expect(result).toContain('"@type":"Article"');
58
+ expect(result).toContain('"headline":"Hello"');
59
+ });
60
+
61
+ it('preserves a consumer-supplied `@context` (does not overwrite)', () => {
62
+ const payload: WebsiteStructuredDataPayload = {
63
+ '@type': 'Article',
64
+ '@context': 'https://example.com/custom-context',
65
+ };
66
+ const result = renderPageStructuredData({
67
+ sections: [makeSection('hero', () => payload)],
68
+ });
69
+ expect(result).toContain('"@context":"https://example.com/custom-context"');
70
+ expect(result).not.toContain('"@context":"https://schema.org"');
71
+ });
72
+
73
+ it('emits separate `<script>` blocks for an array return (Product + AggregateOffer pattern)', () => {
74
+ const sections = [
75
+ makeSection('pricing', () => [
76
+ { '@type': 'Product', name: 'Plan' } satisfies WebsiteStructuredDataPayload,
77
+ { '@type': 'AggregateOffer', lowPrice: '9' } satisfies WebsiteStructuredDataPayload,
78
+ ]),
79
+ ];
80
+ const result = renderPageStructuredData({ sections });
81
+ const blockCount = result.match(/<script type="application\/ld\+json">/g)?.length ?? 0;
82
+ expect(blockCount).toBe(2);
83
+ expect(result).toContain('"@type":"Product"');
84
+ expect(result).toContain('"@type":"AggregateOffer"');
85
+ });
86
+
87
+ it('appends page-level payloads after section payloads', () => {
88
+ const sections = [
89
+ makeSection('hero', () => ({ '@type': 'Article', headline: 'A' }) as WebsiteStructuredDataPayload),
90
+ ];
91
+ const pagePayloads: WebsiteStructuredDataPayload[] = [
92
+ { '@type': 'BreadcrumbList', itemListElement: [] },
93
+ ];
94
+ const result = renderPageStructuredData({ sections, pagePayloads });
95
+ const articleIdx = result.indexOf('"@type":"Article"');
96
+ const breadcrumbIdx = result.indexOf('"@type":"BreadcrumbList"');
97
+ expect(articleIdx).toBeGreaterThan(-1);
98
+ expect(breadcrumbIdx).toBeGreaterThan(-1);
99
+ expect(articleIdx).toBeLessThan(breadcrumbIdx);
100
+ });
101
+
102
+ it('dedupes payloads sharing the same `@id` (last-wins)', () => {
103
+ const sections = [
104
+ makeSection('hero', () => ({
105
+ '@type': 'Product',
106
+ '@id': 'https://example.com/#product',
107
+ name: 'OldName',
108
+ }) as WebsiteStructuredDataPayload),
109
+ makeSection('cta', () => ({
110
+ '@type': 'Product',
111
+ '@id': 'https://example.com/#product',
112
+ name: 'NewName',
113
+ }) as WebsiteStructuredDataPayload),
114
+ ];
115
+ const result = renderPageStructuredData({ sections });
116
+ const blockCount = result.match(/<script type="application\/ld\+json">/g)?.length ?? 0;
117
+ expect(blockCount).toBe(1);
118
+ expect(result).toContain('"name":"NewName"');
119
+ expect(result).not.toContain('"name":"OldName"');
120
+ });
121
+
122
+ it('does NOT dedupe payloads without `@id` (every payload always emits)', () => {
123
+ const sections = [
124
+ makeSection('a', () => ({ '@type': 'Article', headline: 'A' }) as WebsiteStructuredDataPayload),
125
+ makeSection('b', () => ({ '@type': 'Article', headline: 'B' }) as WebsiteStructuredDataPayload),
126
+ ];
127
+ const result = renderPageStructuredData({ sections });
128
+ const blockCount = result.match(/<script type="application\/ld\+json">/g)?.length ?? 0;
129
+ expect(blockCount).toBe(2);
130
+ });
131
+
132
+ it('escapes `<` to `\\u003c` so a payload containing `</script>` cannot break the surrounding tag', () => {
133
+ const sections = [
134
+ makeSection(
135
+ 'evil',
136
+ () =>
137
+ ({
138
+ '@type': 'Article',
139
+ description: 'Malicious </script><img src=x onerror=alert(1)>',
140
+ }) as WebsiteStructuredDataPayload,
141
+ ),
142
+ ];
143
+ const result = renderPageStructuredData({ sections });
144
+ /**
145
+ * The serialized JSON inside the `<script>` block must NOT
146
+ * contain a literal `</` sequence — that would let the HTML
147
+ * parser see `</script>` and prematurely close the script
148
+ * element. The renderer escapes every `<` to `\u003c`.
149
+ */
150
+ const scriptOpenIdx = result.indexOf('<script type="application/ld+json">');
151
+ const scriptCloseIdx = result.indexOf('</script>', scriptOpenIdx);
152
+ const innerJson = result.slice(
153
+ scriptOpenIdx + '<script type="application/ld+json">'.length,
154
+ scriptCloseIdx,
155
+ );
156
+ expect(innerJson).not.toContain('</');
157
+ /**
158
+ * The renderer escapes `<` (the only character that can open
159
+ * an HTML comment / close a tag inside a `<script>` body); `>`
160
+ * is left alone because it cannot start a parsing escape on
161
+ * its own. So `</script>` becomes `\u003c/script>`.
162
+ */
163
+ expect(innerJson).toContain('\\u003c/script>');
164
+ /**
165
+ * Re-parse the escaped JSON to confirm the consumer sees the
166
+ * original payload unchanged (escape is HTML-only, not a JSON
167
+ * mutation).
168
+ */
169
+ const reparsed = JSON.parse(innerJson) as Record<string, unknown>;
170
+ expect(reparsed.description).toBe('Malicious </script><img src=x onerror=alert(1)>');
171
+ });
172
+ });