blume 2.0.3 → 2.1.0

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 (455) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-1d7ve1dm.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-1d7ve1dm.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-2eytanqx.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-yfyb25rh.js → chunk-2f3ndkpw.js} +24 -8
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-r20tn01b.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-r20tn01b.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-35d4wj9f.js → chunk-2n4ps1b3.js} +7 -7
  12. package/dist/cli/{chunk-j85vccga.js → chunk-43zcf4gz.js} +97 -57
  13. package/dist/cli/{chunk-j85vccga.js.map → chunk-43zcf4gz.js.map} +6 -6
  14. package/dist/cli/{chunk-e04dxsz1.js → chunk-4ejxbzee.js} +1 -1
  15. package/dist/cli/{chunk-5m5nmvyq.js → chunk-4h8z9vv5.js} +233 -39
  16. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  17. package/dist/cli/{chunk-5f86nr5m.js → chunk-5cg2rzgf.js} +18 -16
  18. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-5cg2rzgf.js.map} +2 -2
  19. package/dist/cli/{chunk-tzmab476.js → chunk-5ffywsmb.js} +4 -4
  20. package/dist/cli/{chunk-2z928egk.js → chunk-5wj2qwn2.js} +5 -5
  21. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  22. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  23. package/dist/cli/{chunk-vg9r4eb9.js → chunk-713mngdq.js} +12 -5
  24. package/dist/cli/{chunk-vg9r4eb9.js.map → chunk-713mngdq.js.map} +3 -3
  25. package/dist/cli/{chunk-5xvm6tfj.js → chunk-9geymqns.js} +17 -15
  26. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-9geymqns.js.map} +2 -2
  27. package/dist/cli/{chunk-q58y5e6a.js → chunk-9xr4dgee.js} +26 -17
  28. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  29. package/dist/cli/{chunk-b07cmahc.js → chunk-amrb6hgh.js} +2 -2
  30. package/dist/cli/chunk-cm89scdv.js +443 -0
  31. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  32. package/dist/cli/{chunk-akbpwfxc.js → chunk-cmtcnh9f.js} +2 -2
  33. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  34. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  35. package/dist/cli/{chunk-crgn1q09.js → chunk-e7vvk5ya.js} +4 -4
  36. package/dist/cli/{chunk-pehfxfta.js → chunk-g2z9x01v.js} +6 -98
  37. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  38. package/dist/cli/{chunk-tkacnehg.js → chunk-gp84za0h.js} +2 -2
  39. package/dist/cli/{chunk-wrr3j9w9.js → chunk-h2a48yhd.js} +186 -63
  40. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  41. package/dist/cli/{chunk-hm3vjy5s.js → chunk-jnwj5kjj.js} +14 -9
  42. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-jnwj5kjj.js.map} +4 -4
  43. package/dist/cli/chunk-kwrg63tm.js +31 -0
  44. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  45. package/dist/cli/{chunk-2hsdwb9n.js → chunk-nd1821sk.js} +18 -16
  46. package/dist/cli/{chunk-2hsdwb9n.js.map → chunk-nd1821sk.js.map} +2 -2
  47. package/dist/cli/{chunk-6dsbexzp.js → chunk-ntv4qrt7.js} +17 -15
  48. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-ntv4qrt7.js.map} +2 -2
  49. package/dist/cli/{chunk-r9rcc4w7.js → chunk-p804f3pe.js} +7 -7
  50. package/dist/cli/{chunk-ey84smr6.js → chunk-py62fewh.js} +6 -6
  51. package/dist/cli/{chunk-ey84smr6.js.map → chunk-py62fewh.js.map} +1 -1
  52. package/dist/cli/chunk-q3wxxq39.js +126 -0
  53. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  54. package/dist/cli/{chunk-9t7a85s3.js → chunk-rekqz4qg.js} +23 -13
  55. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  56. package/dist/cli/{chunk-8ktnccpt.js → chunk-ta27feev.js} +1 -1
  57. package/dist/cli/{chunk-pv29h0wf.js → chunk-xkz5xbtv.js} +3476 -822
  58. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  59. package/dist/cli/{chunk-a58773jm.js → chunk-y4ykbark.js} +2 -2
  60. package/dist/cli/{chunk-p3v96n38.js → chunk-y5h4r2y0.js} +4 -4
  61. package/dist/cli/{chunk-acanzt5p.js → chunk-yj17vs39.js} +6 -6
  62. package/dist/cli/{chunk-p73c0m7w.js → chunk-yxmk4pec.js} +16 -14
  63. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-yxmk4pec.js.map} +2 -2
  64. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  65. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  66. package/dist/cli/{chunk-c8chx29p.js → chunk-zf748avv.js} +384 -207
  67. package/dist/cli/chunk-zf748avv.js.map +37 -0
  68. package/dist/cli/{chunk-g4hq16wv.js → chunk-zhvd6adm.js} +16 -14
  69. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-zhvd6adm.js.map} +2 -2
  70. package/dist/cli/index.js +26 -20
  71. package/dist/cli/index.js.map +5 -5
  72. package/dist/types/ai/agent-surface.d.ts +8 -7
  73. package/dist/types/ai/ai-catalog.d.ts +7 -0
  74. package/dist/types/ai/ask-context.d.ts +22 -0
  75. package/dist/types/ai/ask.d.ts +271 -58
  76. package/dist/types/ai/index.d.ts +2 -2
  77. package/dist/types/ai/site-skill.d.ts +26 -0
  78. package/dist/types/ai/skills.d.ts +16 -0
  79. package/dist/types/astro/integration.d.ts +8 -0
  80. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  81. package/dist/types/captcha/index.d.ts +28 -0
  82. package/dist/types/captcha/schema.d.ts +29 -0
  83. package/dist/types/captcha/turnstile.d.ts +28 -0
  84. package/dist/types/components/content/api-field.d.ts +59 -0
  85. package/dist/types/components/content/api-page.d.ts +95 -0
  86. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  87. package/dist/types/components/openapi/helpers.d.ts +137 -0
  88. package/dist/types/components/openapi/security.d.ts +85 -0
  89. package/dist/types/consent/ethyca.d.ts +48 -0
  90. package/dist/types/consent/index.d.ts +37 -0
  91. package/dist/types/consent/native.d.ts +28 -0
  92. package/dist/types/consent/osano.d.ts +42 -0
  93. package/dist/types/consent/schema.d.ts +110 -0
  94. package/dist/types/core/config-input.d.ts +187 -20
  95. package/dist/types/core/data.d.ts +49 -4
  96. package/dist/types/core/footer.d.ts +8 -0
  97. package/dist/types/core/i18n-ui.d.ts +86 -0
  98. package/dist/types/core/includes.d.ts +8 -2
  99. package/dist/types/core/localizable.d.ts +7 -0
  100. package/dist/types/core/page-modes.d.ts +29 -0
  101. package/dist/types/core/redirect-patterns.d.ts +144 -0
  102. package/dist/types/core/related.d.ts +40 -0
  103. package/dist/types/core/schema.d.ts +551 -56
  104. package/dist/types/core/sources/read.d.ts +5 -0
  105. package/dist/types/core/types.d.ts +1 -1
  106. package/dist/types/core/variables.d.ts +42 -0
  107. package/dist/types/deploy/node-headers.d.ts +13 -2
  108. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  109. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  110. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  111. package/dist/types/deploy/redirects.d.ts +39 -11
  112. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  113. package/dist/types/markdown/code-title.d.ts +17 -0
  114. package/dist/types/markdown/features.d.ts +3 -2
  115. package/dist/types/narration/build.d.ts +70 -0
  116. package/dist/types/narration/provider.d.ts +26 -0
  117. package/dist/types/narration/script.d.ts +154 -0
  118. package/dist/types/openapi/model.d.ts +12 -6
  119. package/dist/types/openapi/overlay.d.ts +54 -0
  120. package/dist/types/openapi/parse.d.ts +13 -0
  121. package/dist/types/openapi/references.d.ts +14 -4
  122. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  123. package/dist/types/ratelimit/index.d.ts +27 -0
  124. package/dist/types/ratelimit/memory.d.ts +36 -0
  125. package/dist/types/ratelimit/schema.d.ts +111 -0
  126. package/dist/types/ratelimit/upstash.d.ts +21 -0
  127. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  128. package/dist/types/reference/asyncapi.d.ts +10 -10
  129. package/dist/types/reference/graphql.d.ts +13 -10
  130. package/dist/types/reference/openapi.d.ts +33 -12
  131. package/dist/types/reference/options.d.ts +45 -5
  132. package/dist/types/reference/scalar.d.ts +13 -0
  133. package/dist/types/reference/schema.d.ts +60 -36
  134. package/dist/types/search/documents.d.ts +12 -1
  135. package/dist/types/search/orama-index.d.ts +4 -0
  136. package/dist/types/seo/metatags.d.ts +20 -0
  137. package/docs/02-deployment.mdx +41 -5
  138. package/docs/03-upgrading.mdx +4 -4
  139. package/docs/04-migrating.mdx +1 -1
  140. package/docs/advanced/changelog.mdx +2 -2
  141. package/docs/advanced/skills.mdx +14 -0
  142. package/docs/cli/audit.mdx +21 -1
  143. package/docs/cli/index.mdx +1 -0
  144. package/docs/configuration/analytics.mdx +3 -1
  145. package/docs/configuration/assistant.mdx +188 -36
  146. package/docs/configuration/consent.mdx +100 -0
  147. package/docs/configuration/customization.mdx +4 -4
  148. package/docs/configuration/index.mdx +63 -5
  149. package/docs/configuration/meta.ts +3 -0
  150. package/docs/configuration/narration.mdx +119 -0
  151. package/docs/configuration/rate-limiting.mdx +69 -0
  152. package/docs/configuration/search.mdx +29 -1
  153. package/docs/configuration/theming.mdx +3 -1
  154. package/docs/content/components.mdx +106 -0
  155. package/docs/content/frontmatter.mdx +76 -0
  156. package/docs/content/i18n.mdx +38 -3
  157. package/docs/content/includes.mdx +16 -0
  158. package/docs/content/meta.mdx +1 -0
  159. package/docs/content/meta.ts +1 -0
  160. package/docs/content/navigation.mdx +28 -6
  161. package/docs/content/sources/contentful.mdx +32 -0
  162. package/docs/content/sources/custom.mdx +39 -0
  163. package/docs/content/sources/github-releases.mdx +31 -0
  164. package/docs/content/sources/index.mdx +91 -0
  165. package/docs/content/sources/meta.ts +16 -0
  166. package/docs/content/sources/notion.mdx +32 -0
  167. package/docs/content/sources/obsidian.mdx +37 -0
  168. package/docs/content/sources/payload.mdx +30 -0
  169. package/docs/content/sources/remote-mdx.mdx +40 -0
  170. package/docs/content/sources/sanity.mdx +29 -0
  171. package/docs/content/sources/strapi.mdx +31 -0
  172. package/docs/content/syntax.mdx +67 -2
  173. package/docs/content/variables.mdx +41 -0
  174. package/docs/discoverability/agent-discovery.mdx +29 -2
  175. package/docs/discoverability/index.mdx +1 -1
  176. package/docs/discoverability/llms-txt.mdx +1 -1
  177. package/docs/discoverability/metadata.mdx +17 -0
  178. package/docs/references/api-pages.mdx +74 -0
  179. package/docs/references/asyncapi.mdx +1 -1
  180. package/docs/references/graphql.mdx +2 -2
  181. package/docs/references/meta.ts +1 -1
  182. package/docs/references/openapi.mdx +102 -3
  183. package/docs/references/scalar.mdx +3 -1
  184. package/package.json +36 -1
  185. package/skills/blume/SKILL.md +3 -0
  186. package/skills/blume-migrate/SKILL.md +5 -5
  187. package/skills/blume-migrate/references/docusaurus.md +3 -3
  188. package/skills/blume-migrate/references/mintlify.md +37 -19
  189. package/skills/blume-migrate/references/nextra.md +2 -2
  190. package/skills/blume-migrate/references/starlight.md +2 -1
  191. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  192. package/skills/blume-write-skill/SKILL.md +82 -0
  193. package/src/ai/agent-readability.ts +2 -1
  194. package/src/ai/agent-surface.ts +9 -7
  195. package/src/ai/ai-catalog.ts +6 -3
  196. package/src/ai/ask-context.ts +73 -20
  197. package/src/ai/ask-data.ts +6 -0
  198. package/src/ai/ask-tools.ts +142 -0
  199. package/src/ai/ask.ts +354 -76
  200. package/src/ai/component-markdown.ts +75 -0
  201. package/src/ai/cors.ts +2 -0
  202. package/src/ai/index.ts +12 -0
  203. package/src/ai/llms.ts +6 -2
  204. package/src/ai/markdown.ts +2 -1
  205. package/src/ai/mcp/data.ts +6 -0
  206. package/src/ai/openapi-components.ts +9 -3
  207. package/src/ai/site-skill.ts +274 -0
  208. package/src/ai/skills.ts +35 -2
  209. package/src/ai/write-skill.ts +97 -0
  210. package/src/astro/generate.ts +132 -18
  211. package/src/astro/index.ts +1 -0
  212. package/src/astro/integration.ts +35 -0
  213. package/src/astro/module-types.ts +6 -0
  214. package/src/astro/templates.ts +283 -30
  215. package/src/astro/variables.ts +31 -0
  216. package/src/audit/catalog.ts +19 -0
  217. package/src/audit/checks/contrast.ts +183 -0
  218. package/src/audit/checks/i18n.ts +2 -3
  219. package/src/audit/checks/indexability.ts +2 -3
  220. package/src/audit/checks/links.ts +5 -12
  221. package/src/audit/checks/sitemap.ts +2 -3
  222. package/src/audit/redirects.ts +46 -15
  223. package/src/audit/run.ts +2 -0
  224. package/src/audit/types.ts +2 -1
  225. package/src/blume-modules.d.ts +7 -0
  226. package/src/captcha/client.ts +152 -0
  227. package/src/captcha/hcaptcha.ts +36 -0
  228. package/src/captcha/index.ts +28 -0
  229. package/src/captcha/schema.ts +32 -0
  230. package/src/captcha/turnstile.ts +38 -0
  231. package/src/captcha/verify.ts +71 -0
  232. package/src/cli/command-meta.ts +5 -0
  233. package/src/cli/commands/doctor.ts +22 -16
  234. package/src/cli/commands/skill.ts +96 -0
  235. package/src/cli/index.ts +5 -0
  236. package/src/cli/required-secrets.ts +71 -29
  237. package/src/components/BlumePage.astro +2 -0
  238. package/src/components/content/ApiEndpoint.astro +18 -0
  239. package/src/components/content/ApiField.astro +79 -0
  240. package/src/components/content/ApiPlayground.astro +62 -0
  241. package/src/components/content/ApiRail.astro +19 -0
  242. package/src/components/content/Color.astro +1 -0
  243. package/src/components/content/FileTree.astro +1 -0
  244. package/src/components/content/GithubInfo.astro +1 -0
  245. package/src/components/content/Panel.astro +1 -0
  246. package/src/components/content/ParamField.astro +25 -0
  247. package/src/components/content/RequestExample.astro +17 -0
  248. package/src/components/content/ResponseExample.astro +17 -0
  249. package/src/components/content/ResponseField.astro +32 -0
  250. package/src/components/content/Step.astro +1 -0
  251. package/src/components/content/Tile.astro +1 -0
  252. package/src/components/content/Tooltip.astro +1 -0
  253. package/src/components/content/Tree.astro +1 -0
  254. package/src/components/content/TypeTable.astro +4 -1
  255. package/src/components/content/View.astro +21 -0
  256. package/src/components/content/ViewSwitcher.astro +168 -0
  257. package/src/components/content/api-field.ts +100 -0
  258. package/src/components/content/api-page.ts +224 -0
  259. package/src/components/content/view-switcher.ts +71 -0
  260. package/src/components/index.ts +2 -0
  261. package/src/components/islands/Assistant.astro +10 -1
  262. package/src/components/islands/assistant.tsx +104 -14
  263. package/src/components/islands/code-question.ts +62 -0
  264. package/src/components/islands/hooks.ts +110 -21
  265. package/src/components/islands/support-link.ts +68 -0
  266. package/src/components/layout/Analytics.astro +11 -2
  267. package/src/components/layout/Banner.astro +14 -10
  268. package/src/components/layout/Breadcrumbs.astro +6 -2
  269. package/src/components/layout/ConsentBanner.astro +62 -0
  270. package/src/components/layout/ConsentHead.astro +38 -0
  271. package/src/components/layout/Directory.astro +94 -0
  272. package/src/components/layout/DrawerSwitchers.astro +51 -0
  273. package/src/components/layout/Empty.astro +2 -2
  274. package/src/components/layout/Header.astro +122 -80
  275. package/src/components/layout/LanguageSwitcher.astro +48 -15
  276. package/src/components/layout/NarrationPlayer.astro +1085 -0
  277. package/src/components/layout/NavSelector.astro +33 -7
  278. package/src/components/layout/PageFeedback.astro +54 -49
  279. package/src/components/layout/PageLayout.astro +71 -3
  280. package/src/components/layout/ReferenceLayout.astro +17 -6
  281. package/src/components/layout/RelatedPages.astro +42 -0
  282. package/src/components/layout/RootLayout.astro +308 -45
  283. package/src/components/layout/Search.astro +31 -17
  284. package/src/components/layout/SiteFooter.astro +124 -0
  285. package/src/components/layout/SiteMetatags.astro +16 -0
  286. package/src/components/layout/consent/native.ts +79 -0
  287. package/src/components/layout/consent/types.ts +37 -0
  288. package/src/components/layout/footer-links.ts +53 -0
  289. package/src/components/layout/footer-socials.ts +87 -0
  290. package/src/components/layout/locale-redirect.ts +107 -0
  291. package/src/components/layout/nav-utils.ts +83 -0
  292. package/src/components/layout/page-feedback.ts +96 -0
  293. package/src/components/layout/search/analytics.ts +98 -0
  294. package/src/components/layout/search/flexsearch.ts +17 -5
  295. package/src/components/layout/search/orama-cloud.ts +26 -14
  296. package/src/components/layout/search/types.ts +20 -0
  297. package/src/components/layout/search/typesense.ts +5 -1
  298. package/src/components/openapi/Operation.astro +161 -13
  299. package/src/components/openapi/RequestBody.astro +6 -4
  300. package/src/components/openapi/RequestPanel.astro +59 -7
  301. package/src/components/openapi/Responses.astro +6 -4
  302. package/src/components/openapi/async-snippets.ts +4 -4
  303. package/src/components/openapi/code-samples.ts +97 -0
  304. package/src/components/openapi/security.ts +6 -3
  305. package/src/components/openapi/snippets.ts +869 -8
  306. package/src/components/props.ts +6 -0
  307. package/src/consent/client.ts +95 -0
  308. package/src/consent/clients.ts +19 -0
  309. package/src/consent/ethyca.ts +71 -0
  310. package/src/consent/head.ts +24 -0
  311. package/src/consent/index.ts +37 -0
  312. package/src/consent/init.ts +29 -0
  313. package/src/consent/native.ts +36 -0
  314. package/src/consent/osano.ts +59 -0
  315. package/src/consent/schema.ts +30 -0
  316. package/src/core/builtin-tags.ts +5 -0
  317. package/src/core/client-address.ts +19 -0
  318. package/src/core/config-input.ts +194 -20
  319. package/src/core/data.ts +44 -4
  320. package/src/core/diagnostics.ts +3 -2
  321. package/src/core/footer.ts +26 -0
  322. package/src/core/graph.ts +39 -36
  323. package/src/core/i18n-ui.ts +82 -2
  324. package/src/core/includes.ts +20 -3
  325. package/src/core/links.ts +16 -2
  326. package/src/core/localizable.ts +26 -0
  327. package/src/core/navigation.ts +43 -12
  328. package/src/core/page-modes.ts +56 -0
  329. package/src/core/project-graph.ts +111 -5
  330. package/src/core/redirect-patterns.ts +491 -0
  331. package/src/core/related.ts +131 -0
  332. package/src/core/schema.ts +318 -64
  333. package/src/core/sources/normalize.ts +2 -1
  334. package/src/core/sources/read.ts +8 -3
  335. package/src/core/types.ts +1 -1
  336. package/src/core/ui-packs/ar.ts +43 -0
  337. package/src/core/ui-packs/bg.ts +45 -0
  338. package/src/core/ui-packs/bn.ts +43 -0
  339. package/src/core/ui-packs/ca.ts +46 -0
  340. package/src/core/ui-packs/cs.ts +44 -0
  341. package/src/core/ui-packs/da.ts +45 -0
  342. package/src/core/ui-packs/de.ts +46 -0
  343. package/src/core/ui-packs/el.ts +45 -0
  344. package/src/core/ui-packs/es.ts +46 -0
  345. package/src/core/ui-packs/fa.ts +45 -0
  346. package/src/core/ui-packs/fi.ts +45 -0
  347. package/src/core/ui-packs/fr.ts +46 -0
  348. package/src/core/ui-packs/he.ts +43 -0
  349. package/src/core/ui-packs/hi.ts +44 -0
  350. package/src/core/ui-packs/hr.ts +45 -0
  351. package/src/core/ui-packs/hu.ts +44 -0
  352. package/src/core/ui-packs/id.ts +45 -0
  353. package/src/core/ui-packs/it.ts +45 -0
  354. package/src/core/ui-packs/ja.ts +45 -0
  355. package/src/core/ui-packs/ko.ts +44 -0
  356. package/src/core/ui-packs/nl.ts +46 -0
  357. package/src/core/ui-packs/no.ts +44 -0
  358. package/src/core/ui-packs/pl.ts +45 -0
  359. package/src/core/ui-packs/pt-br.ts +46 -0
  360. package/src/core/ui-packs/pt.ts +45 -0
  361. package/src/core/ui-packs/ro.ts +45 -0
  362. package/src/core/ui-packs/ru.ts +45 -0
  363. package/src/core/ui-packs/sk.ts +44 -0
  364. package/src/core/ui-packs/sr.ts +45 -0
  365. package/src/core/ui-packs/sv.ts +44 -0
  366. package/src/core/ui-packs/th.ts +43 -0
  367. package/src/core/ui-packs/tr.ts +45 -0
  368. package/src/core/ui-packs/uk.ts +45 -0
  369. package/src/core/ui-packs/vi.ts +45 -0
  370. package/src/core/ui-packs/zh-tw.ts +43 -0
  371. package/src/core/ui-packs/zh.ts +43 -0
  372. package/src/core/variables.ts +103 -0
  373. package/src/deploy/artifacts.ts +77 -8
  374. package/src/deploy/cloudflare-negotiation.ts +35 -5
  375. package/src/deploy/headers.ts +2 -1
  376. package/src/deploy/node-headers.ts +63 -9
  377. package/src/deploy/platforms/cloudflare.ts +32 -0
  378. package/src/deploy/platforms/netlify.ts +33 -1
  379. package/src/deploy/platforms/vercel.ts +37 -2
  380. package/src/deploy/redirects.ts +91 -24
  381. package/src/deploy/vercel-negotiation.ts +36 -0
  382. package/src/markdown/api-rail.ts +259 -0
  383. package/src/markdown/code-title.ts +35 -9
  384. package/src/markdown/features.ts +3 -2
  385. package/src/markdown/fence-meta.ts +2 -0
  386. package/src/markdown/include.ts +11 -1
  387. package/src/markdown/index.ts +28 -5
  388. package/src/markdown/math.ts +4 -4
  389. package/src/markdown/variables.ts +56 -0
  390. package/src/markdown/views.ts +82 -0
  391. package/src/narration/build.ts +412 -0
  392. package/src/narration/player.ts +249 -0
  393. package/src/narration/provider.ts +29 -0
  394. package/src/narration/script.ts +592 -0
  395. package/src/og/card.ts +7 -6
  396. package/src/openapi/model.ts +57 -24
  397. package/src/openapi/overlay.ts +256 -0
  398. package/src/openapi/parse.ts +55 -2
  399. package/src/openapi/references.ts +21 -5
  400. package/src/openapi/render-mdx.ts +5 -1
  401. package/src/openapi/scalar.ts +40 -6
  402. package/src/openapi/source.ts +1 -1
  403. package/src/ratelimit/cloudflare.ts +54 -0
  404. package/src/ratelimit/index.ts +30 -0
  405. package/src/ratelimit/memory.ts +45 -0
  406. package/src/ratelimit/runtime.ts +229 -0
  407. package/src/ratelimit/schema.ts +28 -0
  408. package/src/ratelimit/upstash.ts +30 -0
  409. package/src/ratelimit/wrangler.ts +70 -0
  410. package/src/reference/asyncapi.ts +2 -2
  411. package/src/reference/graphql.ts +5 -2
  412. package/src/reference/openapi.ts +21 -7
  413. package/src/reference/options.ts +51 -7
  414. package/src/reference/scalar.ts +33 -8
  415. package/src/registry/eject.ts +20 -4
  416. package/src/registry/registry.ts +7 -0
  417. package/src/search/documents.ts +26 -2
  418. package/src/search/orama-index.ts +28 -3
  419. package/src/search/pagefind-ranking.ts +39 -0
  420. package/src/search/sync/algolia.ts +15 -4
  421. package/src/search/sync/orama-cloud.ts +3 -0
  422. package/src/search/sync/typesense.ts +5 -0
  423. package/src/seo/metatags.ts +67 -0
  424. package/src/theme/chrome-icons.ts +1 -0
  425. package/src/theme/color.ts +500 -0
  426. package/src/theme/entry.ts +56 -3
  427. package/src/theme/palette.ts +121 -45
  428. package/src/translate/prompts.ts +1 -0
  429. package/src/translate/validate.ts +8 -2
  430. package/src/upgrade/upgrade.ts +4 -171
  431. package/dist/cli/chunk-364znk6q.js +0 -75
  432. package/dist/cli/chunk-364znk6q.js.map +0 -11
  433. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  434. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  435. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  436. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  437. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  438. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  439. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  440. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  441. package/docs/content/sources.mdx +0 -334
  442. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-1n228xqb.js.map} +0 -0
  443. /package/dist/cli/{chunk-35d4wj9f.js.map → chunk-2n4ps1b3.js.map} +0 -0
  444. /package/dist/cli/{chunk-e04dxsz1.js.map → chunk-4ejxbzee.js.map} +0 -0
  445. /package/dist/cli/{chunk-tzmab476.js.map → chunk-5ffywsmb.js.map} +0 -0
  446. /package/dist/cli/{chunk-2z928egk.js.map → chunk-5wj2qwn2.js.map} +0 -0
  447. /package/dist/cli/{chunk-b07cmahc.js.map → chunk-amrb6hgh.js.map} +0 -0
  448. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-cmtcnh9f.js.map} +0 -0
  449. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-e7vvk5ya.js.map} +0 -0
  450. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-gp84za0h.js.map} +0 -0
  451. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-p804f3pe.js.map} +0 -0
  452. /package/dist/cli/{chunk-8ktnccpt.js.map → chunk-ta27feev.js.map} +0 -0
  453. /package/dist/cli/{chunk-a58773jm.js.map → chunk-y4ykbark.js.map} +0 -0
  454. /package/dist/cli/{chunk-p3v96n38.js.map → chunk-y5h4r2y0.js.map} +0 -0
  455. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-yj17vs39.js.map} +0 -0
@@ -8,9 +8,19 @@ import {
8
8
  } from "../ai/ask.ts";
9
9
  import type { ComponentMarkdown } from "../ai/component-markdown.ts";
10
10
  import { analyticsConfigSchema } from "../analytics/schema.ts";
11
+ import { captchaAdapterSchema } from "../captcha/schema.ts";
12
+ import {
13
+ API_ENDPOINT,
14
+ AUTH_METHODS,
15
+ PLAYGROUND_MODES,
16
+ } from "../components/content/api-page.ts";
17
+ import { consentConfigSchema } from "../consent/schema.ts";
11
18
  import { resolvedDeploymentSchema } from "../deploy/adapters/registry.ts";
12
19
  import type { CodeTheme } from "../markdown/themes.ts";
20
+ import { narrationProviderSchema } from "../narration/provider.ts";
13
21
  import { normalizeRoute } from "../openapi/references.ts";
22
+ import { rateLimitConfigSchema } from "../ratelimit/schema.ts";
23
+ import { playgroundSchema } from "../reference/options.ts";
14
24
  import {
15
25
  referenceConfigSchema,
16
26
  removedReferenceKeysHint,
@@ -21,6 +31,7 @@ import {
21
31
  resolvedSearchAdapterSchema,
22
32
  } from "../search/adapters/registry.ts";
23
33
  import type { SearchAdapterInput } from "../search/adapters/registry.ts";
34
+ import { ownedMetatag } from "../seo/metatags.ts";
24
35
  import { normalizeXHandle } from "../seo/x-handle.ts";
25
36
  import { filesystem } from "../sources/filesystem.ts";
26
37
  import {
@@ -28,14 +39,19 @@ import {
28
39
  resolvedSourceAdapterSchema,
29
40
  } from "../sources/registry.ts";
30
41
  import { FONT_SLUGS, isFontSlug } from "../theme/fonts.ts";
31
- import { normalizeBasePath } from "./base-path.ts";
42
+ import { isExternalUrl, normalizeBasePath } from "./base-path.ts";
43
+ import { FOOTER_SOCIALS } from "./footer.ts";
32
44
  import { PUBLIC_HOST_URL } from "./github.ts";
33
45
  import { uiLocaleOverridesSchema } from "./i18n-ui.ts";
34
46
  import { openInChatProviders } from "./open-in-chat.ts";
47
+ import { PAGE_MODES } from "./page-modes.ts";
48
+ import { redirectPatternError } from "./redirect-patterns.ts";
49
+ import { MAX_RELATED } from "./related.ts";
35
50
  import { isStandardSchema } from "./standard-schema.ts";
36
51
  import type { StandardSchema } from "./standard-schema.ts";
37
52
  import { trimEnd } from "./trim.ts";
38
53
  import { unrecognizedKeysMessage } from "./unrecognized-keys.ts";
54
+ import { VARIABLE_NAME } from "./variables.ts";
39
55
 
40
56
  /**
41
57
  * An absolute HTTP(S) URL, for any field that lands verbatim in an `href` —
@@ -119,6 +135,15 @@ const dateSchema = z
119
135
  const sidebarDisplaySchema = z.enum(["flat", "group", "page"]);
120
136
  export type SidebarDisplay = z.infer<typeof sidebarDisplaySchema>;
121
137
 
138
+ /**
139
+ * How a group's own page lists the group's other pages below its content:
140
+ * `card` a grid of cards, `accordion` a list with each subgroup collapsible,
141
+ * `none` no listing (the default). A group without its own page has nowhere
142
+ * to show one. Nested groups inherit the nearest setting above them.
143
+ */
144
+ const directoryModeSchema = z.enum(["accordion", "card", "none"]);
145
+ export type DirectoryMode = z.infer<typeof directoryModeSchema>;
146
+
122
147
  // ---------------------------------------------------------------------------
123
148
  // Page frontmatter
124
149
  // ---------------------------------------------------------------------------
@@ -155,16 +180,18 @@ const seoMetaSchema = z.strictObject({
155
180
  x: z.strictObject({ creator: xHandleSchema }).optional(),
156
181
  });
157
182
 
158
- const searchMetaSchema = z.strictObject(
159
- {
160
- exclude: z.boolean().default(false),
161
- tags: z.array(z.string()).optional(),
162
- },
163
- removedKeysHint({
164
- boost:
165
- "search.boost was removed: search never read it, so the page ranked the same without it. Delete the field.",
166
- })
167
- );
183
+ const searchMetaSchema = z.strictObject({
184
+ /**
185
+ * Multiply the page's search relevance: above 1 ranks it higher, below 1
186
+ * lower. Keep it to a few key pages, and under 10: a large boost floats a
187
+ * page over better matches.
188
+ */
189
+ boost: z.number().positive().optional(),
190
+ exclude: z.boolean().default(false),
191
+ /** Extra terms the page is found by, beyond its own text. */
192
+ keywords: z.array(z.string()).optional(),
193
+ tags: z.array(z.string()).optional(),
194
+ });
168
195
 
169
196
  const aiMetaSchema = z.strictObject({
170
197
  /** Exclude this page from llms.txt and llms-full.txt. */
@@ -201,9 +228,31 @@ const authorSchema = z.union([
201
228
  // `.default({})` on an object with inner defaults (or a transform) would
202
229
  // resolve to a bare `{}` instead of the fully-defaulted shape.
203
230
 
231
+ /** A `related` link: a root-relative page path or an absolute URL. */
232
+ const relatedLinkSchema = z
233
+ .string()
234
+ .refine((link) => link.startsWith("/") || isExternalUrl(link), {
235
+ message:
236
+ 'A related link is a root-relative path ("/guides/setup") or an absolute URL.',
237
+ });
238
+
204
239
  /** Frontmatter accepted on any content page. */
205
240
  const pageMetaBaseSchema = z.strictObject({
206
241
  ai: aiMetaSchema.prefault({}),
242
+ /**
243
+ * A hand-written endpoint: an HTTP method and a path or full URL
244
+ * (`POST /v1/users`). The page's `<ParamField>`s build its playground and
245
+ * request samples.
246
+ */
247
+ api: z
248
+ .string()
249
+ .refine((value) => API_ENDPOINT.test(value.trim()), {
250
+ message:
251
+ 'api takes an HTTP method and a path or URL, like "POST /v1/users" or "GET https://api.acme.com/v1/users/{id}".',
252
+ })
253
+ .optional(),
254
+ /** How this page's endpoint authenticates, over the site's `api.auth`. */
255
+ authMethod: z.enum(AUTH_METHODS).optional(),
207
256
  /** Post author(s) for blog/changelog content; preserved, not yet rendered. */
208
257
  authors: z.union([authorSchema, z.array(authorSchema)]).optional(),
209
258
  changelog: changelogMetaSchema.optional(),
@@ -216,7 +265,37 @@ const pageMetaBaseSchema = z.strictObject({
216
265
  icon: iconName.optional(),
217
266
  /** Overrides the git-derived last-modified date when `lastModified` is on. */
218
267
  lastModified: dateSchema.optional(),
268
+ /** What the page shows around its content (see `core/page-modes.ts`). */
269
+ mode: z.enum(PAGE_MODES).optional(),
270
+ /** `false` keeps the "Listen to this page" player off this page. */
271
+ narration: z.boolean().default(true),
219
272
  noindex: z.boolean().default(false),
273
+ /** `false` drops the previous/next links at the foot of this page. */
274
+ pagination: z.boolean().default(true),
275
+ /** What an `api` page's playground shows: `interactive`, `simple` (samples only), or `none`. */
276
+ playground: z.enum(PLAYGROUND_MODES).optional(),
277
+ /**
278
+ * Pages to suggest at the foot of this one: root-relative paths, absolute
279
+ * URLs, or `{ Title: link }` to name a link. `false` lists none.
280
+ */
281
+ related: z
282
+ .union([
283
+ z.literal(false),
284
+ z
285
+ .array(
286
+ z.union([
287
+ relatedLinkSchema,
288
+ z
289
+ .record(z.string().min(1), relatedLinkSchema)
290
+ .refine((entry) => Object.keys(entry).length === 1, {
291
+ message:
292
+ 'A titled related entry has one title and one link: { "Title": "/path" }.',
293
+ }),
294
+ ])
295
+ )
296
+ .max(MAX_RELATED),
297
+ ])
298
+ .optional(),
220
299
  search: searchMetaSchema.prefault({}),
221
300
  seo: seoMetaSchema.prefault({}),
222
301
  sidebar: sidebarMetaSchema.prefault({}),
@@ -292,6 +371,8 @@ const customKeySchemaRecord = (where: string) =>
292
371
 
293
372
  export const folderMetaSchema = z.strictObject({
294
373
  collapsed: z.boolean().optional(),
374
+ /** List the group's pages on its index page; inherited by nested folders. */
375
+ directory: directoryModeSchema.optional(),
295
376
  /** Render mode for this group; overrides `navigation.sidebar.display`. */
296
377
  display: sidebarDisplaySchema.optional(),
297
378
  icon: iconName.optional(),
@@ -333,19 +414,77 @@ const logoConfigSchema = z.union([
333
414
  }),
334
415
  ]);
335
416
 
336
- /** Site-wide announcement banner: a string, or text with an optional link. */
417
+ /**
418
+ * A label that may localize (header tabs and links, the banner, the footer):
419
+ * a plain string, or a map of locale code to label (`{ en: "Docs", ja: "ドキュメント" }`).
420
+ * Resolved per locale by `core/localizable.ts` — the active locale's entry
421
+ * wins, then the default locale's, then the map's first entry — so a
422
+ * single-locale site can keep plain strings and an i18n site can translate
423
+ * its labels without forking the config.
424
+ */
425
+ const localizableLabelSchema = z.union([
426
+ z.string(),
427
+ z
428
+ .record(z.string(), z.string())
429
+ .refine((value) => Object.keys(value).length > 0, {
430
+ message: "Provide at least one locale's label.",
431
+ }),
432
+ ]);
433
+
434
+ export type LocalizableLabel = z.infer<typeof localizableLabelSchema>;
435
+
436
+ /**
437
+ * Site-wide announcement banner: a string, or text with an optional link. The
438
+ * object form's `content` and link `text` may be per-locale maps.
439
+ */
337
440
  const bannerConfigSchema = z.union([
338
441
  z.string(),
339
442
  z.strictObject({
340
- content: z.string(),
443
+ content: localizableLabelSchema,
341
444
  /** Show a dismiss button; the choice is remembered per visitor. */
342
445
  dismissible: z.boolean().default(false),
343
- /** Stable key for remembering dismissal; defaults to the content. */
446
+ /**
447
+ * Stable key for remembering dismissal; defaults to the content (the
448
+ * default locale's, for a per-locale map).
449
+ */
344
450
  id: z.string().optional(),
345
- link: z.strictObject({ href: z.string(), text: z.string() }).optional(),
451
+ link: z
452
+ .strictObject({ href: z.string(), text: localizableLabelSchema })
453
+ .optional(),
346
454
  }),
347
455
  ]);
348
456
 
457
+ /**
458
+ * Defaults for hand-written endpoint pages (`api` frontmatter): the server a
459
+ * path joins, how requests authenticate, and the playground, which takes an
460
+ * OpenAPI reference's `playground` option.
461
+ */
462
+ const apiConfigSchema = z.strictObject({
463
+ auth: z
464
+ .strictObject({
465
+ method: z.enum(AUTH_METHODS),
466
+ /** The header an API key goes in. Defaults to `x-api-key`. */
467
+ name: z.string().optional(),
468
+ })
469
+ .optional(),
470
+ playground: playgroundSchema,
471
+ /** The base URL an `api` path joins (`https://api.acme.com/v1`). */
472
+ server: z.string().optional(),
473
+ });
474
+
475
+ /**
476
+ * The site footer: a row of links, and social profile icons. Unset, the site
477
+ * has no footer (a `components.ts` `Footer` still renders).
478
+ */
479
+ const footerConfigSchema = z.strictObject({
480
+ /** Links in one row, in the order written. */
481
+ links: z
482
+ .array(z.strictObject({ href: z.string(), label: localizableLabelSchema }))
483
+ .default([]),
484
+ /** Social profiles, platform to URL, shown as icons in the order written. */
485
+ socials: z.partialRecord(z.enum(FOOTER_SOCIALS), z.string()).default({}),
486
+ });
487
+
349
488
  /** A validated `content.sources` entry: an adapter descriptor from `blume/sources`. */
350
489
  export type { ContentSourceAdapter } from "../sources/registry.ts";
351
490
 
@@ -426,25 +565,6 @@ const contentConfigSchema = z
426
565
  ],
427
566
  }));
428
567
 
429
- /**
430
- * A header label that may localize: a plain string, or a map of locale code to
431
- * label (`{ en: "Docs", ja: "ドキュメント" }`). Resolved when each locale's
432
- * navigation is built — the active locale's entry wins, then the default
433
- * locale's, then the map's first entry — so a single-locale site can keep
434
- * plain strings and an i18n site can translate its header without forking the
435
- * config.
436
- */
437
- const localizableLabelSchema = z.union([
438
- z.string(),
439
- z
440
- .record(z.string(), z.string())
441
- .refine((value) => Object.keys(value).length > 0, {
442
- message: "Provide at least one locale's label.",
443
- }),
444
- ]);
445
-
446
- export type LocalizableLabel = z.infer<typeof localizableLabelSchema>;
447
-
448
568
  const navTabSchema = z.strictObject({
449
569
  // Rejected empty rather than accepted: an empty `href` would render a link to
450
570
  // nowhere, and it can't mean "resolve it for me" either — that's what
@@ -484,9 +604,6 @@ const navSelectorSchema = z.strictObject({
484
604
  label: z.string(),
485
605
  });
486
606
 
487
- const directoryModeSchema = z.enum(["accordion", "card", "none"]);
488
- export type DirectoryMode = z.infer<typeof directoryModeSchema>;
489
-
490
607
  /** A node in an explicit sidebar config: a page reference or a group/link. */
491
608
  export type SidebarItemConfig =
492
609
  | string
@@ -842,6 +959,9 @@ const aiConfigFields = {
842
959
  // origin. Each URL is reduced to its origin so a trailing slash or path
843
960
  // can't defeat the exact match the route performs. Read by the
844
961
  // generated route only; an external `endpoint` owns its own CORS.
962
+ // A bot check from `blume/captcha`: the panel sends a token with each
963
+ // question and the generated route verifies it before the model runs.
964
+ captcha: captchaAdapterSchema.optional(),
845
965
  cors: z
846
966
  .array(
847
967
  z.union([
@@ -890,6 +1010,27 @@ const aiConfigFields = {
890
1010
  })
891
1011
  )
892
1012
  .default([]),
1013
+ // Where the panel's Contact support link goes: an email address
1014
+ // (`mailto:`, which starts an email with the conversation), a URL,
1015
+ // or a root-relative page (which get the conversation's `thread` id).
1016
+ support: z
1017
+ .string()
1018
+ .refine(
1019
+ (link) =>
1020
+ link.startsWith("mailto:") ||
1021
+ link.startsWith("/") ||
1022
+ isExternalUrl(link),
1023
+ {
1024
+ message:
1025
+ 'ai.assistant.support is a mailto: address, a URL, or a root-relative path, like "mailto:help@example.com".',
1026
+ }
1027
+ )
1028
+ .optional(),
1029
+ // Search and read-page tools the model can call over several steps.
1030
+ // No default here: unset follows the adapter (`toolsByDefault` in
1031
+ // `ai/ask.ts`), on for the hosted catalogs and off for an
1032
+ // OpenAI-compatible backend that may not speak tool calling.
1033
+ tools: z.boolean().optional(),
893
1034
  },
894
1035
  assistantMovedFieldsHint
895
1036
  )
@@ -946,7 +1087,7 @@ const aiConfigSchema = z.strictObject(
946
1087
  const featuredLinkSchema = z.strictObject({
947
1088
  href: z.string(),
948
1089
  icon: iconName.optional(),
949
- label: z.string(),
1090
+ label: localizableLabelSchema,
950
1091
  });
951
1092
 
952
1093
  /**
@@ -956,7 +1097,7 @@ const featuredLinkSchema = z.strictObject({
956
1097
  */
957
1098
  const headerActionSchema = z.strictObject({
958
1099
  href: z.string(),
959
- label: z.string(),
1100
+ label: localizableLabelSchema,
960
1101
  });
961
1102
 
962
1103
  const navigationConfigSchema = z.strictObject({
@@ -973,7 +1114,7 @@ const navigationConfigSchema = z.strictObject({
973
1114
  /** Pinned links shown above the generated sidebar sections. */
974
1115
  featured: z.array(featuredLinkSchema).default([]),
975
1116
  /**
976
- * The GitHub link in the header. `true` derives it from `github`, `false`
1117
+ * The GitHub link in the footer. `true` derives it from `github`, `false`
977
1118
  * hides it, and an absolute URL points it anywhere on GitHub — an
978
1119
  * organization, say, when the docs repo itself is private and `github` has
979
1120
  * to stay unset. The mark stays the GitHub one, so a URL elsewhere belongs in
@@ -1026,6 +1167,45 @@ const exportConfigSchema = z
1026
1167
  isBoolean(value) ? { epub: value, pdf: value } : value
1027
1168
  );
1028
1169
 
1170
+ // The "Was this page helpful?" rating, on by default. `comments: true` adds a
1171
+ // box after the rating where the reader can say more, sent through the
1172
+ // analytics adapters as a `feedback_comment` event. Both forms normalize to
1173
+ // `{ enabled, comments }`.
1174
+ const feedbackConfigSchema = z
1175
+ .union([
1176
+ z.boolean(),
1177
+ z.strictObject({
1178
+ comments: z.boolean().default(false),
1179
+ enabled: z.boolean().default(true),
1180
+ }),
1181
+ ])
1182
+ // With the rating off there is nothing to comment on.
1183
+ .transform((value) =>
1184
+ isBoolean(value)
1185
+ ? { comments: false, enabled: value }
1186
+ : { comments: value.enabled && value.comments, enabled: value.enabled }
1187
+ );
1188
+
1189
+ // "Listen to this page". Off by default. `true` reads pages aloud with the
1190
+ // reader's browser voices, which needs no key and works on any host; an object
1191
+ // with a `provider` (`gateway()` from `blume/ai`) generates neural audio at
1192
+ // build instead, one cached clip per sentence, and falls back to the browser's
1193
+ // voices where no clips exist (`blume dev`, a build without the key). Both
1194
+ // normalize to `{ enabled, provider }`.
1195
+ const narrationConfigSchema = z
1196
+ .union([
1197
+ z.boolean(),
1198
+ z.strictObject({
1199
+ enabled: z.boolean().default(true),
1200
+ provider: narrationProviderSchema.optional(),
1201
+ }),
1202
+ ])
1203
+ .transform((value) =>
1204
+ isBoolean(value)
1205
+ ? { enabled: value, provider: null }
1206
+ : { enabled: value.enabled, provider: value.provider ?? null }
1207
+ );
1208
+
1029
1209
  /** A configured locale: ISO-ish code plus display metadata for the switcher. */
1030
1210
  const localeSchema = z.strictObject({
1031
1211
  code: z.string().min(1),
@@ -1055,6 +1235,12 @@ const i18nConfigSchema = z
1055
1235
  locales: z.array(localeSchema).min(1),
1056
1236
  /** `"dir"`: locale directories (`fr/page.mdx`). `"dot"`: filename suffix (`page.fr.mdx`). */
1057
1237
  parser: z.enum(["dir", "dot"]).default("dir"),
1238
+ /**
1239
+ * Send a visitor who lands on the default language's home page to the
1240
+ * home page in their browser's preferred language, until they pick a
1241
+ * language with the switcher. Off by default.
1242
+ */
1243
+ routeByBrowserLanguage: z.boolean().default(false),
1058
1244
  /** Per-locale UI string overrides: `{ fr: { search: { button: "…" } } }`. */
1059
1245
  ui: uiLocaleOverridesSchema.optional(),
1060
1246
  })
@@ -1156,13 +1342,6 @@ const versionsConfigSchema = z
1156
1342
  }
1157
1343
  });
1158
1344
 
1159
- /**
1160
- * A pattern segment in a redirect path: a named `:param` segment or a `*`
1161
- * splat. `from` is matched as an exact path, and hosts disagree on patterns —
1162
- * a static build would even write a literal `:slug` folder — so both ends are
1163
- * checked. An absolute `to` URL's own scheme and host are skipped.
1164
- */
1165
- const REDIRECT_PATTERN = /(?:^|\/):[A-Za-z_]|\*/u;
1166
1345
  const URL_ORIGIN = /^[a-z][\d+.a-z-]*:\/\/[^/]*/iu;
1167
1346
 
1168
1347
  /**
@@ -1183,23 +1362,33 @@ const REDIRECT_START = {
1183
1362
  },
1184
1363
  };
1185
1364
 
1186
- const exactRedirectPath = (end: "from" | "to") =>
1187
- z
1188
- .string()
1189
- .refine(REDIRECT_START[end].allows, {
1190
- message: REDIRECT_START[end].message,
1191
- })
1192
- .refine((path) => !REDIRECT_PATTERN.test(path.replace(URL_ORIGIN, "")), {
1193
- message: `redirects take exact paths: \`${end}\` can't hold a \`:param\` segment or a \`*\` wildcard. Add one redirect per path, or put pattern rules in your host's redirect config (vercel.json, _redirects).`,
1194
- });
1365
+ const redirectPath = (end: "from" | "to") =>
1366
+ z.string().refine(REDIRECT_START[end].allows, {
1367
+ message: REDIRECT_START[end].message,
1368
+ });
1195
1369
 
1196
- const redirectSchema = z.strictObject({
1197
- from: exactRedirectPath("from"),
1198
- status: z
1199
- .union([z.literal(301), z.literal(302), z.literal(307), z.literal(308)])
1200
- .default(301),
1201
- to: exactRedirectPath("to"),
1202
- });
1370
+ /**
1371
+ * A redirect: an exact path, or a pattern (`/beta/:slug*`, `/old/*`) whose
1372
+ * captures `to` may read (see `core/redirect-patterns.ts`).
1373
+ */
1374
+ const redirectSchema = z
1375
+ .strictObject({
1376
+ from: redirectPath("from"),
1377
+ status: z
1378
+ .union([z.literal(301), z.literal(302), z.literal(307), z.literal(308)])
1379
+ .default(301),
1380
+ to: redirectPath("to"),
1381
+ })
1382
+ .superRefine((redirect, ctx) => {
1383
+ const error = redirectPatternError(redirect.from, redirect.to);
1384
+ if (error) {
1385
+ ctx.addIssue({
1386
+ code: z.ZodIssueCode.custom,
1387
+ message: error.message,
1388
+ path: [error.end],
1389
+ });
1390
+ }
1391
+ });
1203
1392
 
1204
1393
  /**
1205
1394
  * One authorized remote image source, passed through to Astro's
@@ -1417,6 +1606,26 @@ type SoftwareResolved = z.output<typeof softwareConfigSchema>;
1417
1606
 
1418
1607
  /** Discoverability features: OG images, feeds, sitemap, structured data. */
1419
1608
  const seoConfigFields = {
1609
+ /**
1610
+ * Meta tags written into every page's head, name to content: site
1611
+ * verification, `theme-color`, and anything else Blume has no setting for.
1612
+ * A tag Blume writes itself is refused with the setting that controls it.
1613
+ */
1614
+ metatags: z
1615
+ .record(z.string().min(1), z.string())
1616
+ .optional()
1617
+ .superRefine((tags, ctx) => {
1618
+ for (const name of Object.keys(tags ?? {})) {
1619
+ const owner = ownedMetatag(name);
1620
+ if (owner) {
1621
+ ctx.addIssue({
1622
+ code: "custom",
1623
+ message: `seo.metatags can't set "${name}": Blume writes that tag. Set it with ${owner}.`,
1624
+ path: [name],
1625
+ });
1626
+ }
1627
+ }
1628
+ }),
1420
1629
  og: ogConfigSchema.default({}),
1421
1630
  /** The organization behind the site, as an `Organization` JSON-LD node. */
1422
1631
  organization: organizationConfigSchema.optional(),
@@ -1519,6 +1728,14 @@ const agentsConfigSchema = z.strictObject({
1519
1728
  .default({}),
1520
1729
  /** Expose the docs as an MCP server for connecting agents. */
1521
1730
  mcp: mcpConfigSchema.prefault({}),
1731
+ /**
1732
+ * Generate the site's own agent skill: a `SKILL.md` named after the site,
1733
+ * built from its navigation, page descriptions, and agent surfaces, served
1734
+ * at `/skill.md` and in the skills discovery index. Needs a
1735
+ * `deployment.site`. A skill in `agents.skills` with the same name replaces
1736
+ * it. On by default.
1737
+ */
1738
+ skillMd: z.boolean().default(true),
1522
1739
  /**
1523
1740
  * Publish Agent Skills for discovery: a directory (resolved against the
1524
1741
  * project root) whose subdirectories each hold a `SKILL.md`. The build
@@ -1867,6 +2084,7 @@ export const blumeConfigSchema = z
1867
2084
  ai: aiConfigSchema.prefault({}),
1868
2085
  // Adapters from `blume/analytics`, each a serializable descriptor.
1869
2086
  analytics: analyticsConfigSchema,
2087
+ api: apiConfigSchema.prefault({}),
1870
2088
  banner: bannerConfigSchema.optional(),
1871
2089
  /**
1872
2090
  * Site-wide mount point prepended to every generated route (e.g. `/docs`),
@@ -1884,6 +2102,8 @@ export const blumeConfigSchema = z
1884
2102
  })
1885
2103
  .optional()
1886
2104
  .transform((value) => normalizeBasePath(value)),
2105
+ // An adapter from `blume/consent`; analytics waits for the reader.
2106
+ consent: consentConfigSchema,
1887
2107
  content: contentConfigSchema.prefault({}),
1888
2108
  /**
1889
2109
  * Date presentation for the "last updated" stamp and the changelog timeline.
@@ -1907,7 +2127,8 @@ export const blumeConfigSchema = z
1907
2127
  */
1908
2128
  examples: examplesConfigSchema.prefault("examples"),
1909
2129
  export: exportConfigSchema.prefault(false),
1910
- feedback: z.boolean().default(true),
2130
+ feedback: feedbackConfigSchema.prefault(true),
2131
+ footer: footerConfigSchema.optional(),
1911
2132
  /** Opt-in custom frontmatter keys, validated by user-supplied schemas. */
1912
2133
  frontmatter: frontmatterConfigSchema.prefault({}),
1913
2134
  github: githubConfigSchema.optional(),
@@ -1917,7 +2138,10 @@ export const blumeConfigSchema = z
1917
2138
  lastModified: lastModifiedConfigSchema.default(false),
1918
2139
  logo: logoConfigSchema.optional(),
1919
2140
  markdown: markdownConfigSchema.prefault({}),
2141
+ narration: narrationConfigSchema.prefault(false),
1920
2142
  navigation: navigationConfigSchema.prefault({}),
2143
+ // An adapter from `blume/ratelimit`, `memory()` by default; `false` is off.
2144
+ rateLimit: rateLimitConfigSchema,
1921
2145
  react: reactConfigSchema.prefault({}),
1922
2146
  redirects: z.array(redirectSchema).default([]),
1923
2147
  /** API references: adapters from `blume/reference`, each a serializable descriptor. */
@@ -1927,6 +2151,23 @@ export const blumeConfigSchema = z
1927
2151
  theme: themeConfigSchema.prefault({}),
1928
2152
  title: z.string().default("Documentation"),
1929
2153
  toc: tocConfigSchema,
2154
+ /**
2155
+ * Content variables: `{{name}}` in a page body reads the value (see
2156
+ * `core/variables.ts`). Names take letters, digits, `_`, and `-`;
2157
+ * values are one line, so a substitution never moves the lines a
2158
+ * diagnostic points at.
2159
+ */
2160
+ variables: z
2161
+ .record(
2162
+ z.string().regex(VARIABLE_NAME, {
2163
+ message:
2164
+ "Variable names take letters, digits, `_`, and `-`, like `version` or `api-url`.",
2165
+ }),
2166
+ z.string().refine((value) => !/[\r\n]/u.test(value), {
2167
+ message: "A variable's value must fit on one line.",
2168
+ })
2169
+ )
2170
+ .default({}),
1930
2171
  versions: versionsConfigSchema.optional(),
1931
2172
  },
1932
2173
  {
@@ -1987,6 +2228,19 @@ export const blumeConfigSchema = z
1987
2228
  }
1988
2229
  }
1989
2230
  }
2231
+ // Cloudflare's rate limiting is a Worker binding, which only a Cloudflare
2232
+ // deployment has.
2233
+ if (
2234
+ config.rateLimit?.kind === "cloudflare" &&
2235
+ config.deployment.kind !== "cloudflare"
2236
+ ) {
2237
+ ctx.addIssue({
2238
+ code: z.ZodIssueCode.custom,
2239
+ message:
2240
+ 'rateLimit: cloudflare() counts with a Workers binding, so it needs `deployment: cloudflare()` from "blume/deploy". On another host, use upstash() or memory() from "blume/ratelimit".',
2241
+ path: ["rateLimit"],
2242
+ });
2243
+ }
1990
2244
  });
1991
2245
 
1992
2246
  /** Resolved config: every field present after defaults are applied. */
@@ -14,6 +14,7 @@ import { occupySlug, parseHeadingMarkers } from "../heading-markers.ts";
14
14
  import { localePlacement, localizeRoute } from "../i18n.ts";
15
15
  import { titleWord } from "../navigation.ts";
16
16
  import { stripOrderingPrefix } from "../ordering-prefix.ts";
17
+ import { relatedPageLinks } from "../related.ts";
17
18
  import { pageMetaSchema } from "../schema.ts";
18
19
  import type {
19
20
  FrontmatterExtend,
@@ -1554,7 +1555,7 @@ export const normalizeEntry = (
1554
1555
  id: `${ctx.source.name}:${entry.ref}`,
1555
1556
  includes: entryIncludes(entry),
1556
1557
  lastModified: meta.lastModified ?? entry.lastModified,
1557
- links: entryLinks(entry),
1558
+ links: [...entryLinks(entry), ...relatedPageLinks(meta.related)],
1558
1559
  meta,
1559
1560
  monolingual: ctx.source.monolingual,
1560
1561
  navPath,
@@ -2,10 +2,14 @@ import { readFile } from "node:fs/promises";
2
2
 
3
3
  import { expandIncludes, hasIncludeStatements } from "../includes.ts";
4
4
  import type { PageRecord } from "../types.ts";
5
+ import { substituteVariablesInBody } from "../variables.ts";
6
+ import type { ContentVariables } from "../variables.ts";
5
7
  import type { ContentSource } from "./types.ts";
6
8
 
7
9
  /** The subset of a scanned project the entry reader needs. */
8
10
  export interface EntryReadContext {
11
+ /** The site's content variables, substituted into what's read. */
12
+ config?: { variables?: ContentVariables };
9
13
  /** The instantiated sources, keyed for `read()` lookups. */
10
14
  sources?: ContentSource[];
11
15
  }
@@ -48,9 +52,10 @@ export const readExpandedEntryText = async (
48
52
  ctx: EntryReadContext,
49
53
  page: PageRecord
50
54
  ): Promise<string> => {
55
+ const variables = ctx.config?.variables;
51
56
  const raw = await readEntryText(ctx, page);
52
57
  if (!(page.sourcePath && hasIncludeStatements(raw))) {
53
- return raw;
58
+ return substituteVariablesInBody(raw, variables);
54
59
  }
55
60
  const source = ctx.sources?.find(
56
61
  (candidate) => candidate.name === page.source.name
@@ -61,11 +66,11 @@ export const readExpandedEntryText = async (
61
66
  // here would splice text the rest of the pipeline never validated, with no
62
67
  // content-root bound on resolution.
63
68
  if (!source || source.staged || !source.contentRoot) {
64
- return raw;
69
+ return substituteVariablesInBody(raw, variables);
65
70
  }
66
71
  const expansion = await expandIncludes(raw, {
67
72
  contentRoot: source.contentRoot,
68
73
  sourcePath: page.sourcePath,
69
74
  });
70
- return expansion.text;
75
+ return substituteVariablesInBody(expansion.text, variables);
71
76
  };
package/src/core/types.ts CHANGED
@@ -316,7 +316,7 @@ export interface Navigation {
316
316
  cta?: HeaderAction | null;
317
317
  /** Pinned links shown above the sidebar sections, unscoped by tab. */
318
318
  featured: FeaturedLink[];
319
- /** Repo URL for the header link, or null when hidden (`navigation.repo`). */
319
+ /** Repo URL for the footer's GitHub link, or null when hidden (`navigation.repo`). */
320
320
  repoUrl?: string | null;
321
321
  /**
322
322
  * Where the header's brand link points in this locale: `logo.href` moved