@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,543 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ reconcileStructuredDataAgainstCatalog,
5
+ type StructuredDataCatalogEntry,
6
+ type StructuredDataReconciliationFinding,
7
+ } from '../structured-data/structured-data-reconciliation.shared';
8
+
9
+ function createCatalogEntry(
10
+ entry: Partial<StructuredDataCatalogEntry> & Pick<StructuredDataCatalogEntry, '@type'>,
11
+ ): StructuredDataCatalogEntry {
12
+ return {
13
+ requiredFields: [],
14
+ optionalFields: [],
15
+ deprecatedFields: [],
16
+ recommendOptionals: false,
17
+ ...entry,
18
+ };
19
+ }
20
+
21
+ function findFinding(
22
+ findings: readonly StructuredDataReconciliationFinding[],
23
+ kind: StructuredDataReconciliationFinding['kind'],
24
+ ): StructuredDataReconciliationFinding | undefined {
25
+ return findings.find((finding) => finding.kind === kind);
26
+ }
27
+
28
+ describe('reconcileStructuredDataAgainstCatalog', () => {
29
+ it('reports duplicate catalog entries as catalog-override findings and still validates with last-wins semantics', () => {
30
+ const result = reconcileStructuredDataAgainstCatalog({
31
+ catalog: [
32
+ createCatalogEntry({
33
+ '@type': 'Product',
34
+ requiredFields: ['name'],
35
+ }),
36
+ createCatalogEntry({
37
+ '@type': 'Product',
38
+ requiredFields: ['sku'],
39
+ }),
40
+ ],
41
+ pages: [
42
+ {
43
+ ref: 'pricing',
44
+ routePath: '/pricing',
45
+ sectionRefs: ['pricing-card'],
46
+ },
47
+ ],
48
+ sections: [
49
+ {
50
+ sectionRef: 'pricing-card',
51
+ getStructuredData: () => ({
52
+ '@type': 'Product',
53
+ sku: 'pro',
54
+ }),
55
+ },
56
+ ],
57
+ });
58
+
59
+ expect(findFinding(result.findings, 'catalog-override')).toMatchObject({
60
+ kind: 'catalog-override',
61
+ entityType: 'Product',
62
+ count: 2,
63
+ });
64
+ expect(
65
+ result.findings.some(
66
+ (finding) => finding.kind === 'spec-violation' && finding.code === 'missing-required-field',
67
+ ),
68
+ ).toBe(false);
69
+ });
70
+
71
+ it('reports catalog-miss findings when a section emits an unknown @type', () => {
72
+ const result = reconcileStructuredDataAgainstCatalog({
73
+ catalog: [createCatalogEntry({ '@type': 'Product' })],
74
+ pages: [
75
+ {
76
+ ref: 'pricing',
77
+ routePath: '/pricing',
78
+ sectionRefs: ['pricing-card'],
79
+ },
80
+ ],
81
+ sections: [
82
+ {
83
+ sectionRef: 'pricing-card',
84
+ getStructuredData: () => ({
85
+ '@type': 'SoftwareApplication',
86
+ name: 'Wonder Todos',
87
+ }),
88
+ },
89
+ ],
90
+ });
91
+
92
+ expect(findFinding(result.findings, 'catalog-miss')).toMatchObject({
93
+ kind: 'catalog-miss',
94
+ pageRef: 'pricing',
95
+ routePath: '/pricing',
96
+ sectionRef: 'pricing-card',
97
+ entityType: 'SoftwareApplication',
98
+ });
99
+ });
100
+
101
+ it('validates page-level structured-data payloads alongside section payloads', () => {
102
+ const result = reconcileStructuredDataAgainstCatalog({
103
+ catalog: [
104
+ createCatalogEntry({
105
+ '@type': 'BlogPosting',
106
+ requiredFields: ['headline'],
107
+ }),
108
+ ],
109
+ pages: [
110
+ {
111
+ ref: 'blog-post',
112
+ routePath: '/blog/example',
113
+ sectionRefs: [],
114
+ },
115
+ ],
116
+ sections: [],
117
+ pagePayloads: [
118
+ {
119
+ pageRef: 'blog-post',
120
+ routePath: '/blog/example',
121
+ sourceRef: 'page:blog-post:example',
122
+ payloads: {
123
+ '@type': 'BlogPosting',
124
+ headline: 'Example post',
125
+ },
126
+ },
127
+ ],
128
+ });
129
+
130
+ expect(result.findings).toEqual([]);
131
+ expect(result.pages).toEqual([
132
+ {
133
+ pageRef: 'blog-post',
134
+ routePath: '/blog/example',
135
+ payloadCount: 1,
136
+ findingCount: 0,
137
+ },
138
+ ]);
139
+ });
140
+
141
+ it('isolates page-level payload validation by concrete routePath for templated pages', () => {
142
+ const result = reconcileStructuredDataAgainstCatalog({
143
+ catalog: [
144
+ createCatalogEntry({
145
+ '@type': 'Review',
146
+ requiredFields: ['itemReviewed'],
147
+ }),
148
+ createCatalogEntry({
149
+ '@type': 'Product',
150
+ requiredFields: ['name'],
151
+ }),
152
+ ],
153
+ pages: [
154
+ {
155
+ ref: 'blog-post',
156
+ routePath: '/blog/[slug]',
157
+ sectionRefs: [],
158
+ },
159
+ ],
160
+ sections: [],
161
+ pagePayloads: [
162
+ {
163
+ pageRef: 'blog-post',
164
+ routePath: '/blog/a',
165
+ sourceRef: 'page:blog-post:a',
166
+ payloads: {
167
+ '@type': 'Review',
168
+ itemReviewed: {
169
+ '@id': 'https://example.test/blog/b#product',
170
+ },
171
+ },
172
+ },
173
+ {
174
+ pageRef: 'blog-post',
175
+ routePath: '/blog/b',
176
+ sourceRef: 'page:blog-post:b',
177
+ payloads: {
178
+ '@type': 'Product',
179
+ '@id': 'https://example.test/blog/b#product',
180
+ name: 'Post B product',
181
+ },
182
+ },
183
+ ],
184
+ });
185
+
186
+ expect(result.findings).toEqual(
187
+ expect.arrayContaining([
188
+ expect.objectContaining({
189
+ kind: 'cross-entity-reference-drift',
190
+ routePath: '/blog/a',
191
+ missingReferenceId: 'https://example.test/blog/b#product',
192
+ }),
193
+ ]),
194
+ );
195
+ expect(result.pages.map((page) => page.routePath)).toEqual(['/blog/a', '/blog/b']);
196
+ });
197
+
198
+ it('merges templated section payloads into each concrete page-level route', () => {
199
+ const result = reconcileStructuredDataAgainstCatalog({
200
+ catalog: [
201
+ createCatalogEntry({
202
+ '@type': 'Review',
203
+ requiredFields: ['itemReviewed'],
204
+ }),
205
+ createCatalogEntry({
206
+ '@type': 'Product',
207
+ requiredFields: ['name'],
208
+ }),
209
+ ],
210
+ pages: [
211
+ {
212
+ ref: 'blog-post',
213
+ routePath: '/blog/[slug]',
214
+ sectionRefs: ['blog-product'],
215
+ },
216
+ ],
217
+ sections: [
218
+ {
219
+ sectionRef: 'blog-product',
220
+ getStructuredData: () => ({
221
+ '@type': 'Product',
222
+ '@id': 'https://example.test/blog/a#product',
223
+ name: 'Post A product',
224
+ }),
225
+ },
226
+ ],
227
+ pagePayloads: [
228
+ {
229
+ pageRef: 'blog-post',
230
+ routePath: '/blog/a',
231
+ sourceRef: 'page:blog-post:a',
232
+ payloads: {
233
+ '@type': 'Review',
234
+ itemReviewed: {
235
+ '@id': 'https://example.test/blog/a#product',
236
+ },
237
+ },
238
+ },
239
+ ],
240
+ });
241
+
242
+ expect(result.findings).toEqual([]);
243
+ expect(result.pages).toEqual([
244
+ {
245
+ pageRef: 'blog-post',
246
+ routePath: '/blog/a',
247
+ payloadCount: 2,
248
+ findingCount: 0,
249
+ },
250
+ ]);
251
+ });
252
+
253
+ it('keeps templated section payloads when page-level payload routePath is omitted', () => {
254
+ const result = reconcileStructuredDataAgainstCatalog({
255
+ catalog: [
256
+ createCatalogEntry({
257
+ '@type': 'Review',
258
+ requiredFields: ['itemReviewed'],
259
+ }),
260
+ createCatalogEntry({
261
+ '@type': 'Product',
262
+ requiredFields: ['name'],
263
+ }),
264
+ ],
265
+ pages: [
266
+ {
267
+ ref: 'blog-post',
268
+ routePath: '/blog/[slug]',
269
+ sectionRefs: ['blog-product'],
270
+ },
271
+ ],
272
+ sections: [
273
+ {
274
+ sectionRef: 'blog-product',
275
+ getStructuredData: () => ({
276
+ '@type': 'Product',
277
+ '@id': 'https://example.test/blog/template#product',
278
+ name: 'Template product',
279
+ }),
280
+ },
281
+ ],
282
+ pagePayloads: [
283
+ {
284
+ pageRef: 'blog-post',
285
+ sourceRef: 'page:blog-post:template',
286
+ payloads: {
287
+ '@type': 'Review',
288
+ itemReviewed: {
289
+ '@id': 'https://example.test/blog/template#product',
290
+ },
291
+ },
292
+ },
293
+ ],
294
+ });
295
+
296
+ expect(result.findings).toEqual([]);
297
+ expect(result.pages).toEqual([
298
+ {
299
+ pageRef: 'blog-post',
300
+ routePath: '/blog/[slug]',
301
+ payloadCount: 2,
302
+ findingCount: 0,
303
+ },
304
+ ]);
305
+ });
306
+
307
+ it('does not validate templated section payloads twice when collection findings also exist', () => {
308
+ const result = reconcileStructuredDataAgainstCatalog({
309
+ catalog: [
310
+ createCatalogEntry({
311
+ '@type': 'Product',
312
+ requiredFields: ['name'],
313
+ }),
314
+ ],
315
+ pages: [
316
+ {
317
+ ref: 'blog-post',
318
+ routePath: '/blog/[slug]',
319
+ sectionRefs: ['missing-section', 'blog-product'],
320
+ },
321
+ ],
322
+ sections: [
323
+ {
324
+ sectionRef: 'blog-product',
325
+ getStructuredData: () => ({
326
+ '@type': 'Product',
327
+ '@id': 'https://example.test/blog/a#product',
328
+ }),
329
+ },
330
+ ],
331
+ pagePayloads: [
332
+ {
333
+ pageRef: 'blog-post',
334
+ routePath: '/blog/a',
335
+ sourceRef: 'page:blog-post:a',
336
+ payloads: {
337
+ '@type': 'Product',
338
+ '@id': 'https://example.test/blog/a#page-product',
339
+ name: 'Page product',
340
+ },
341
+ },
342
+ ],
343
+ });
344
+
345
+ const missingNameFindings = result.findings.filter(
346
+ (finding) =>
347
+ finding.kind === 'spec-violation'
348
+ && finding.code === 'missing-required-field'
349
+ && finding.field === 'name',
350
+ );
351
+ expect(missingNameFindings).toHaveLength(1);
352
+ expect(result.pages).toEqual([
353
+ {
354
+ pageRef: 'blog-post',
355
+ routePath: '/blog/[slug]',
356
+ payloadCount: 0,
357
+ findingCount: 1,
358
+ },
359
+ {
360
+ pageRef: 'blog-post',
361
+ routePath: '/blog/a',
362
+ payloadCount: 2,
363
+ findingCount: 1,
364
+ },
365
+ ]);
366
+ });
367
+
368
+ it('reports page-level payloads that reference no supplied page manifest', () => {
369
+ const result = reconcileStructuredDataAgainstCatalog({
370
+ catalog: [createCatalogEntry({ '@type': 'BlogPosting' })],
371
+ pages: [],
372
+ sections: [],
373
+ pagePayloads: [
374
+ {
375
+ pageRef: 'missing-page',
376
+ routePath: '/missing',
377
+ sourceRef: 'page:missing',
378
+ payloads: {
379
+ '@type': 'BlogPosting',
380
+ },
381
+ },
382
+ ],
383
+ });
384
+
385
+ expect(result.findings).toEqual([
386
+ expect.objectContaining({
387
+ kind: 'spec-violation',
388
+ code: 'orphan-page-payload',
389
+ pageRef: 'missing-page',
390
+ routePath: '/missing',
391
+ sectionRef: 'page:missing',
392
+ }),
393
+ ]);
394
+ });
395
+
396
+ it('reports missing required fields as spec-violation findings', () => {
397
+ const result = reconcileStructuredDataAgainstCatalog({
398
+ catalog: [
399
+ createCatalogEntry({
400
+ '@type': 'Product',
401
+ requiredFields: ['name', 'offers'],
402
+ }),
403
+ ],
404
+ pages: [
405
+ {
406
+ ref: 'pricing',
407
+ routePath: '/pricing',
408
+ sectionRefs: ['pricing-card'],
409
+ },
410
+ ],
411
+ sections: [
412
+ {
413
+ sectionRef: 'pricing-card',
414
+ getStructuredData: () => ({
415
+ '@type': 'Product',
416
+ name: 'Wonder Todos Pro',
417
+ }),
418
+ },
419
+ ],
420
+ });
421
+
422
+ expect(result.findings).toEqual(
423
+ expect.arrayContaining([
424
+ expect.objectContaining({
425
+ kind: 'spec-violation',
426
+ code: 'missing-required-field',
427
+ field: 'offers',
428
+ entityType: 'Product',
429
+ }),
430
+ ]),
431
+ );
432
+ });
433
+
434
+ it('reports deprecated-field usage when emitted payloads still include deprecated catalog fields', () => {
435
+ const result = reconcileStructuredDataAgainstCatalog({
436
+ catalog: [
437
+ createCatalogEntry({
438
+ '@type': 'Article',
439
+ deprecatedFields: [
440
+ {
441
+ field: 'dateCreated',
442
+ replacedBy: 'datePublished',
443
+ deprecatedSince: '2026-04-17',
444
+ },
445
+ ],
446
+ }),
447
+ ],
448
+ pages: [
449
+ {
450
+ ref: 'blog',
451
+ routePath: '/blog/example',
452
+ sectionRefs: ['blog-body'],
453
+ },
454
+ ],
455
+ sections: [
456
+ {
457
+ sectionRef: 'blog-body',
458
+ getStructuredData: () => ({
459
+ '@type': 'Article',
460
+ dateCreated: '2026-04-17',
461
+ }),
462
+ },
463
+ ],
464
+ });
465
+
466
+ expect(findFinding(result.findings, 'deprecated-field')).toMatchObject({
467
+ kind: 'deprecated-field',
468
+ entityType: 'Article',
469
+ field: 'dateCreated',
470
+ replacedBy: 'datePublished',
471
+ deprecatedSince: '2026-04-17',
472
+ });
473
+ });
474
+
475
+ it('reports cross-entity reference drift when payloads point at missing @id targets', () => {
476
+ const result = reconcileStructuredDataAgainstCatalog({
477
+ catalog: [
478
+ createCatalogEntry({
479
+ '@type': 'Review',
480
+ requiredFields: ['itemReviewed'],
481
+ }),
482
+ ],
483
+ pages: [
484
+ {
485
+ ref: 'reviews',
486
+ routePath: '/reviews',
487
+ sectionRefs: ['review-card'],
488
+ },
489
+ ],
490
+ sections: [
491
+ {
492
+ sectionRef: 'review-card',
493
+ getStructuredData: () => ({
494
+ '@type': 'Review',
495
+ itemReviewed: {
496
+ '@id': 'https://example.com/#product-missing',
497
+ },
498
+ }),
499
+ },
500
+ ],
501
+ });
502
+
503
+ expect(findFinding(result.findings, 'cross-entity-reference-drift')).toMatchObject({
504
+ kind: 'cross-entity-reference-drift',
505
+ entityType: 'Review',
506
+ missingReferenceId: 'https://example.com/#product-missing',
507
+ });
508
+ });
509
+
510
+ it('fails closed on missing section registrations and invalid getStructuredData payload shapes', () => {
511
+ const result = reconcileStructuredDataAgainstCatalog({
512
+ catalog: [createCatalogEntry({ '@type': 'Product' })],
513
+ pages: [
514
+ {
515
+ ref: 'pricing',
516
+ routePath: '/pricing',
517
+ sectionRefs: ['missing-section', 'invalid-section'],
518
+ },
519
+ ],
520
+ sections: [
521
+ {
522
+ sectionRef: 'invalid-section',
523
+ getStructuredData: () => 'not-an-object',
524
+ },
525
+ ],
526
+ });
527
+
528
+ expect(result.findings).toEqual(
529
+ expect.arrayContaining([
530
+ expect.objectContaining({
531
+ kind: 'spec-violation',
532
+ code: 'missing-section-definition',
533
+ sectionRef: 'missing-section',
534
+ }),
535
+ expect.objectContaining({
536
+ kind: 'spec-violation',
537
+ code: 'invalid-payload-shape',
538
+ sectionRef: 'invalid-section',
539
+ }),
540
+ ]),
541
+ );
542
+ });
543
+ });
@@ -0,0 +1,111 @@
1
+ import { z } from 'zod';
2
+
3
+ import { AvailableLanguage } from '@wildo-ai/saas-models/public-runtime';
4
+
5
+ /**
6
+ * @wildo_source:part:start saas.website.blog.frontmatter facet:layer:shared facet:family:website
7
+ *
8
+ * `BlogPostFrontmatter` — the YAML frontmatter contract every blog
9
+ * post MDX file under `examples/<app>/website/src/content/blog/<locale>/`
10
+ * MUST satisfy.
11
+ *
12
+ * **Q16=B context**: blogs are authored as full per-locale MDX files
13
+ * (one MDX per (slug, locale) pair), NOT as label-pack-keyed content.
14
+ * The frontmatter therefore carries strings directly (`title`,
15
+ * `summary`) rather than label keys — there is no shared
16
+ * label-pack-driven authoring surface for blog content. This is
17
+ * intentional: blog copy is long-form and locale-divergent; forcing
18
+ * it through a structural label-pack would either inflate the pack
19
+ * or fragment the prose.
20
+ *
21
+ * **Field rationale**:
22
+ *
23
+ * - `title` — REQUIRED. Renders into `<title>`, `og:title`, and the
24
+ * in-page H1. Authored as a direct string, deliberately.
25
+ * - `summary` — REQUIRED. Renders into `<meta name="description">`,
26
+ * `og:description`, and listing-card descriptions. Direct string.
27
+ * - `slug` — REQUIRED. URL slug (lowercase kebab-case). Brand prevents
28
+ * accidental swap with `WebsitePageRef` (same shape, different role).
29
+ * - `locale` — REQUIRED. The post's locale. Drives the URL prefix
30
+ * (`/<locale>/blog/<slug>`) and `<html lang>`. Cross-locale
31
+ * correspondence is established by sharing the `slug` across
32
+ * per-locale MDX files (e.g. `en/why-context.md` and
33
+ * `fr/why-context.md` are the same post in two locales).
34
+ * - `publishedAt` — REQUIRED. ISO-8601 date-time string. Renders into
35
+ * `og:article:published_time` and the listing-card date label.
36
+ * Stored as string (not `z.coerce.date`) because YAML frontmatter
37
+ * parsers preserve the original string and the SEO emitters need
38
+ * the original ISO formatting (no Date.toISOString() round-trip
39
+ * loss).
40
+ * - `authorRef` — REQUIRED. Identifier of the author resolved by the
41
+ * consumer's author registry (typically a `src/content/authors/<ref>.ts`
42
+ * file). Brand keeps it distinct from page/section refs.
43
+ * - `tags` — REQUIRED, possibly empty. Lowercase kebab-case tag
44
+ * identifiers. Drives `og:article:tag` and the listing-page facets.
45
+ * - `coverImageUrl` — OPTIONAL. CDN URL for the blog post hero image
46
+ * and `og:image`.
47
+ *
48
+ * **Why a separate file from the page-manifest**: a blog post is NOT
49
+ * a `WebsitePageManifest` (no React `pageComponent` to mount — the
50
+ * MDX file IS the content). The Astro adapter emits one MDX route
51
+ * per post via `content collections`, with `BlogPostFrontmatter`
52
+ * driving the route generation and the SEO emission.
53
+ */
54
+ const _BlogPostSlugSchema = z
55
+ .string()
56
+ .min(1, { error: 'BlogPostSlug must not be empty' })
57
+ .max(96, { error: 'BlogPostSlug must be at most 96 characters' })
58
+ .regex(
59
+ /^[a-z][a-z0-9-]*$/,
60
+ { error: 'BlogPostSlug must be lowercase kebab-case starting with a letter' },
61
+ )
62
+ .brand<'BlogPostSlug'>();
63
+
64
+ export const BlogPostSlugSchema: typeof _BlogPostSlugSchema = _BlogPostSlugSchema;
65
+
66
+ export type BlogPostSlug = z.infer<typeof _BlogPostSlugSchema>;
67
+
68
+ const _BlogAuthorRefSchema = z
69
+ .string()
70
+ .min(1, { error: 'BlogAuthorRef must not be empty' })
71
+ .max(64, { error: 'BlogAuthorRef must be at most 64 characters' })
72
+ .regex(
73
+ /^[a-z][a-z0-9-]*$/,
74
+ { error: 'BlogAuthorRef must be lowercase kebab-case starting with a letter' },
75
+ )
76
+ .brand<'BlogAuthorRef'>();
77
+
78
+ export const BlogAuthorRefSchema: typeof _BlogAuthorRefSchema = _BlogAuthorRefSchema;
79
+
80
+ export type BlogAuthorRef = z.infer<typeof _BlogAuthorRefSchema>;
81
+
82
+ const _BlogPostTagSchema = z
83
+ .string()
84
+ .min(1, { error: 'BlogPostTag must not be empty' })
85
+ .max(48, { error: 'BlogPostTag must be at most 48 characters' })
86
+ .regex(
87
+ /^[a-z][a-z0-9-]*$/,
88
+ { error: 'BlogPostTag must be lowercase kebab-case starting with a letter' },
89
+ )
90
+ .brand<'BlogPostTag'>();
91
+
92
+ export const BlogPostTagSchema: typeof _BlogPostTagSchema = _BlogPostTagSchema;
93
+
94
+ export type BlogPostTag = z.infer<typeof _BlogPostTagSchema>;
95
+
96
+ const _BlogPostFrontmatterSchema = z.object({
97
+ title: z.string().min(1, { error: 'title must not be empty' }).max(200, { error: 'title must be at most 200 characters' }),
98
+ summary: z.string().min(1, { error: 'summary must not be empty' }).max(400, { error: 'summary must be at most 400 characters' }),
99
+ slug: _BlogPostSlugSchema,
100
+ locale: z.enum(AvailableLanguage),
101
+ publishedAt: z.iso.datetime({ error: 'publishedAt must be an ISO-8601 datetime string' }),
102
+ authorRef: _BlogAuthorRefSchema,
103
+ tags: z.array(_BlogPostTagSchema),
104
+ coverImageUrl: z.url({ error: 'coverImageUrl must be a valid URL' }).optional(),
105
+ });
106
+
107
+ export const BlogPostFrontmatterSchema: typeof _BlogPostFrontmatterSchema =
108
+ _BlogPostFrontmatterSchema;
109
+
110
+ export type BlogPostFrontmatter = z.infer<typeof _BlogPostFrontmatterSchema>;
111
+ /** @wildo_source:part:end saas.website.blog.frontmatter */