blume 2.0.2 → 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 (601) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +113 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-6k4ftwze.js → chunk-0gz36akr.js} +18 -2
  5. package/dist/cli/{chunk-6k4ftwze.js.map → chunk-0gz36akr.js.map} +3 -3
  6. package/dist/cli/{chunk-zp79m0ts.js → chunk-1n228xqb.js} +2 -2
  7. package/dist/cli/{chunk-ddndchfr.js → chunk-2f3ndkpw.js} +57 -29
  8. package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
  9. package/dist/cli/{chunk-fsmrqk8a.js → chunk-2f7n4qzt.js} +8 -2
  10. package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-2f7n4qzt.js.map} +3 -3
  11. package/dist/cli/{chunk-ppzjqwx2.js → chunk-2n4ps1b3.js} +17 -18
  12. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-2n4ps1b3.js.map} +3 -3
  13. package/dist/cli/{chunk-5r8g91qn.js → chunk-43zcf4gz.js} +398 -159
  14. package/dist/cli/chunk-43zcf4gz.js.map +36 -0
  15. package/dist/cli/chunk-4ejxbzee.js +39 -0
  16. package/dist/cli/chunk-4ejxbzee.js.map +10 -0
  17. package/dist/cli/{chunk-hr8ne106.js → chunk-4h8z9vv5.js} +296 -77
  18. package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
  19. package/dist/cli/{chunk-k7pj68a8.js → chunk-5cg2rzgf.js} +18 -16
  20. package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5cg2rzgf.js.map} +2 -2
  21. package/dist/cli/{chunk-h2ez8dzb.js → chunk-5ffywsmb.js} +4 -4
  22. package/dist/cli/{chunk-g698a744.js → chunk-5wj2qwn2.js} +5 -5
  23. package/dist/cli/chunk-6bm0xpgd.js +18 -0
  24. package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
  25. package/dist/cli/{chunk-3yce002v.js → chunk-713mngdq.js} +15 -8
  26. package/dist/cli/chunk-713mngdq.js.map +14 -0
  27. package/dist/cli/{chunk-91ws1n6j.js → chunk-9geymqns.js} +17 -15
  28. package/dist/cli/{chunk-91ws1n6j.js.map → chunk-9geymqns.js.map} +2 -2
  29. package/dist/cli/{chunk-6dtt0zfn.js → chunk-9xr4dgee.js} +27 -18
  30. package/dist/cli/chunk-9xr4dgee.js.map +10 -0
  31. package/dist/cli/{chunk-sqw4ekg1.js → chunk-amrb6hgh.js} +2 -2
  32. package/dist/cli/chunk-cm89scdv.js +443 -0
  33. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  34. package/dist/cli/{chunk-wdrt2k2v.js → chunk-cmtcnh9f.js} +90 -26
  35. package/dist/cli/chunk-cmtcnh9f.js.map +10 -0
  36. package/dist/cli/chunk-cy5bm0j0.js +104 -0
  37. package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
  38. package/dist/cli/{chunk-7mbqtmgb.js → chunk-e7vvk5ya.js} +21 -12
  39. package/dist/cli/chunk-e7vvk5ya.js.map +10 -0
  40. package/dist/cli/{chunk-qkb5a8sa.js → chunk-g2z9x01v.js} +6 -98
  41. package/dist/cli/chunk-g2z9x01v.js.map +10 -0
  42. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  43. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  44. package/dist/cli/{chunk-h7k3nq3v.js → chunk-gp84za0h.js} +2 -2
  45. package/dist/cli/{chunk-hqp2ajnh.js → chunk-h2a48yhd.js} +195 -73
  46. package/dist/cli/chunk-h2a48yhd.js.map +36 -0
  47. package/dist/cli/{chunk-pbg5a4s3.js → chunk-jnwj5kjj.js} +71 -47
  48. package/dist/cli/chunk-jnwj5kjj.js.map +19 -0
  49. package/dist/cli/chunk-kwrg63tm.js +31 -0
  50. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  51. package/dist/cli/{chunk-8g8ytmgx.js → chunk-nd1821sk.js} +21 -19
  52. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-nd1821sk.js.map} +2 -2
  53. package/dist/cli/{chunk-w4bxdvsa.js → chunk-ntv4qrt7.js} +17 -15
  54. package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-ntv4qrt7.js.map} +2 -2
  55. package/dist/cli/{chunk-bfwp9vp6.js → chunk-p804f3pe.js} +7 -7
  56. package/dist/cli/{chunk-4e9b9ra6.js → chunk-py62fewh.js} +6 -6
  57. package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-py62fewh.js.map} +1 -1
  58. package/dist/cli/chunk-q3wxxq39.js +126 -0
  59. package/dist/cli/chunk-q3wxxq39.js.map +11 -0
  60. package/dist/cli/{chunk-j85scx15.js → chunk-rekqz4qg.js} +142 -46
  61. package/dist/cli/chunk-rekqz4qg.js.map +10 -0
  62. package/dist/cli/{chunk-00gs3wqs.js → chunk-ta27feev.js} +1 -1
  63. package/dist/cli/{chunk-v6ya5kcb.js → chunk-xkz5xbtv.js} +4031 -1132
  64. package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
  65. package/dist/cli/{chunk-bbnwccaz.js → chunk-y4ykbark.js} +2 -2
  66. package/dist/cli/{chunk-n1yg3tj3.js → chunk-y5h4r2y0.js} +6 -6
  67. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-y5h4r2y0.js.map} +3 -3
  68. package/dist/cli/{chunk-nfcyttvj.js → chunk-yj17vs39.js} +9 -9
  69. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-yj17vs39.js.map} +1 -1
  70. package/dist/cli/{chunk-7vtckvaw.js → chunk-yxmk4pec.js} +16 -14
  71. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-yxmk4pec.js.map} +2 -2
  72. package/dist/cli/{chunk-gs7r695n.js → chunk-zccbjcj0.js} +22 -8
  73. package/dist/cli/{chunk-gs7r695n.js.map → chunk-zccbjcj0.js.map} +3 -3
  74. package/dist/cli/{chunk-xh43dwgw.js → chunk-zf748avv.js} +415 -225
  75. package/dist/cli/chunk-zf748avv.js.map +37 -0
  76. package/dist/cli/{chunk-d1v5rhy0.js → chunk-zhvd6adm.js} +16 -14
  77. package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-zhvd6adm.js.map} +2 -2
  78. package/dist/cli/index.js +28 -20
  79. package/dist/cli/index.js.map +5 -5
  80. package/dist/types/ai/agent-surface.d.ts +8 -7
  81. package/dist/types/ai/ai-catalog.d.ts +7 -0
  82. package/dist/types/ai/ask-context.d.ts +22 -0
  83. package/dist/types/ai/ask.d.ts +271 -58
  84. package/dist/types/ai/index.d.ts +2 -2
  85. package/dist/types/ai/link-headers.d.ts +8 -1
  86. package/dist/types/ai/openapi-components.d.ts +5 -2
  87. package/dist/types/ai/relative-links.d.ts +9 -7
  88. package/dist/types/ai/site-skill.d.ts +26 -0
  89. package/dist/types/ai/skills.d.ts +20 -1
  90. package/dist/types/ai/tar.d.ts +1 -3
  91. package/dist/types/analytics/index.d.ts +2 -0
  92. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  93. package/dist/types/analytics/schema.d.ts +14 -0
  94. package/dist/types/astro/integration.d.ts +11 -2
  95. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  96. package/dist/types/captcha/index.d.ts +28 -0
  97. package/dist/types/captcha/schema.d.ts +29 -0
  98. package/dist/types/captcha/turnstile.d.ts +28 -0
  99. package/dist/types/components/content/api-field.d.ts +59 -0
  100. package/dist/types/components/content/api-page.d.ts +95 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/helpers.d.ts +137 -0
  103. package/dist/types/components/openapi/security.d.ts +85 -0
  104. package/dist/types/consent/ethyca.d.ts +48 -0
  105. package/dist/types/consent/index.d.ts +37 -0
  106. package/dist/types/consent/native.d.ts +28 -0
  107. package/dist/types/consent/osano.d.ts +42 -0
  108. package/dist/types/consent/schema.d.ts +110 -0
  109. package/dist/types/core/base-path.d.ts +21 -7
  110. package/dist/types/core/config-input.d.ts +196 -27
  111. package/dist/types/core/data.d.ts +49 -4
  112. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  113. package/dist/types/core/footer.d.ts +8 -0
  114. package/dist/types/core/heading-markers.d.ts +5 -7
  115. package/dist/types/core/i18n-ui.d.ts +86 -0
  116. package/dist/types/core/i18n.d.ts +2 -0
  117. package/dist/types/core/includes.d.ts +8 -2
  118. package/dist/types/core/last-modified.d.ts +10 -0
  119. package/dist/types/core/localizable.d.ts +7 -0
  120. package/dist/types/core/meta.d.ts +8 -0
  121. package/dist/types/core/page-modes.d.ts +29 -0
  122. package/dist/types/core/redirect-patterns.d.ts +144 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +558 -56
  125. package/dist/types/core/sources/lower.d.ts +29 -16
  126. package/dist/types/core/sources/normalize.d.ts +13 -1
  127. package/dist/types/core/sources/read.d.ts +5 -0
  128. package/dist/types/core/sources/watch.d.ts +12 -5
  129. package/dist/types/core/standard-schema.d.ts +5 -0
  130. package/dist/types/core/types.d.ts +1 -1
  131. package/dist/types/core/variables.d.ts +42 -0
  132. package/dist/types/deploy/artifacts.d.ts +6 -4
  133. package/dist/types/deploy/headers.d.ts +5 -0
  134. package/dist/types/deploy/node-headers.d.ts +13 -2
  135. package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
  136. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  137. package/dist/types/deploy/platforms/types.d.ts +8 -0
  138. package/dist/types/deploy/platforms/vercel.d.ts +6 -0
  139. package/dist/types/deploy/redirects.d.ts +61 -22
  140. package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
  141. package/dist/types/markdown/code-title.d.ts +17 -0
  142. package/dist/types/markdown/directives.d.ts +62 -0
  143. package/dist/types/markdown/features.d.ts +24 -2
  144. package/dist/types/markdown/mdast.d.ts +63 -0
  145. package/dist/types/narration/build.d.ts +70 -0
  146. package/dist/types/narration/provider.d.ts +26 -0
  147. package/dist/types/narration/script.d.ts +154 -0
  148. package/dist/types/openapi/asyncapi.d.ts +4 -2
  149. package/dist/types/openapi/model.d.ts +12 -6
  150. package/dist/types/openapi/overlay.d.ts +54 -0
  151. package/dist/types/openapi/parse.d.ts +13 -0
  152. package/dist/types/openapi/references.d.ts +14 -4
  153. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  154. package/dist/types/ratelimit/index.d.ts +27 -0
  155. package/dist/types/ratelimit/memory.d.ts +36 -0
  156. package/dist/types/ratelimit/schema.d.ts +111 -0
  157. package/dist/types/ratelimit/upstash.d.ts +21 -0
  158. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  159. package/dist/types/reference/asyncapi.d.ts +10 -10
  160. package/dist/types/reference/graphql.d.ts +13 -10
  161. package/dist/types/reference/openapi.d.ts +33 -12
  162. package/dist/types/reference/options.d.ts +45 -5
  163. package/dist/types/reference/scalar.d.ts +13 -0
  164. package/dist/types/reference/schema.d.ts +60 -36
  165. package/dist/types/search/documents.d.ts +12 -1
  166. package/dist/types/search/orama-index.d.ts +4 -0
  167. package/dist/types/seo/metatags.d.ts +20 -0
  168. package/docs/02-deployment.mdx +44 -6
  169. package/docs/03-upgrading.mdx +4 -4
  170. package/docs/04-migrating.mdx +1 -1
  171. package/docs/08-faq.mdx +1 -1
  172. package/docs/advanced/changelog.mdx +2 -2
  173. package/docs/advanced/custom-pages.mdx +3 -3
  174. package/docs/advanced/skills.mdx +14 -0
  175. package/docs/cli/audit.mdx +38 -2
  176. package/docs/cli/evals.mdx +3 -3
  177. package/docs/cli/index.mdx +1 -0
  178. package/docs/cli/translate.mdx +3 -3
  179. package/docs/cli/version.mdx +1 -1
  180. package/docs/configuration/analytics.mdx +22 -1
  181. package/docs/configuration/assistant.mdx +188 -36
  182. package/docs/configuration/consent.mdx +100 -0
  183. package/docs/configuration/customization.mdx +6 -4
  184. package/docs/configuration/index.mdx +63 -5
  185. package/docs/configuration/meta.ts +3 -0
  186. package/docs/configuration/narration.mdx +119 -0
  187. package/docs/configuration/rate-limiting.mdx +69 -0
  188. package/docs/configuration/search.mdx +30 -2
  189. package/docs/configuration/theming.mdx +3 -1
  190. package/docs/content/components.mdx +107 -1
  191. package/docs/content/frontmatter.mdx +77 -1
  192. package/docs/content/i18n.mdx +40 -3
  193. package/docs/content/includes.mdx +16 -0
  194. package/docs/content/index.mdx +4 -2
  195. package/docs/content/islands.mdx +1 -1
  196. package/docs/content/meta.mdx +4 -1
  197. package/docs/content/meta.ts +1 -0
  198. package/docs/content/navigation.mdx +28 -6
  199. package/docs/content/sources/contentful.mdx +32 -0
  200. package/docs/content/sources/custom.mdx +39 -0
  201. package/docs/content/sources/github-releases.mdx +31 -0
  202. package/docs/content/sources/index.mdx +91 -0
  203. package/docs/content/sources/meta.ts +16 -0
  204. package/docs/content/sources/notion.mdx +32 -0
  205. package/docs/content/sources/obsidian.mdx +37 -0
  206. package/docs/content/sources/payload.mdx +30 -0
  207. package/docs/content/sources/remote-mdx.mdx +40 -0
  208. package/docs/content/sources/sanity.mdx +29 -0
  209. package/docs/content/sources/strapi.mdx +31 -0
  210. package/docs/content/syntax.mdx +81 -2
  211. package/docs/content/variables.mdx +41 -0
  212. package/docs/content/versioning.mdx +1 -1
  213. package/docs/discoverability/agent-discovery.mdx +30 -3
  214. package/docs/discoverability/index.mdx +3 -2
  215. package/docs/discoverability/llms-txt.mdx +1 -1
  216. package/docs/discoverability/markdown.mdx +2 -2
  217. package/docs/discoverability/metadata.mdx +17 -0
  218. package/docs/discoverability/open-graph.mdx +5 -3
  219. package/docs/discoverability/rss.mdx +1 -1
  220. package/docs/references/api-pages.mdx +74 -0
  221. package/docs/references/asyncapi.mdx +2 -2
  222. package/docs/references/graphql.mdx +3 -3
  223. package/docs/references/meta.ts +1 -1
  224. package/docs/references/openapi.mdx +106 -5
  225. package/docs/references/scalar.mdx +3 -1
  226. package/package.json +36 -1
  227. package/skills/blume/SKILL.md +3 -0
  228. package/skills/blume-migrate/SKILL.md +5 -5
  229. package/skills/blume-migrate/references/docusaurus.md +3 -3
  230. package/skills/blume-migrate/references/fumadocs.md +1 -1
  231. package/skills/blume-migrate/references/mintlify.md +37 -19
  232. package/skills/blume-migrate/references/nextra.md +3 -3
  233. package/skills/blume-migrate/references/starlight.md +2 -1
  234. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +67 -16
  235. package/skills/blume-write-skill/SKILL.md +82 -0
  236. package/src/ai/agent-readability.ts +4 -3
  237. package/src/ai/agent-surface.ts +9 -7
  238. package/src/ai/ai-catalog.ts +12 -5
  239. package/src/ai/api/handlers.ts +2 -2
  240. package/src/ai/api/spec.ts +2 -2
  241. package/src/ai/api-catalog.ts +6 -2
  242. package/src/ai/ask-context.ts +73 -20
  243. package/src/ai/ask-data.ts +6 -0
  244. package/src/ai/ask-tools.ts +142 -0
  245. package/src/ai/ask.ts +354 -76
  246. package/src/ai/changelog-markdown.ts +2 -2
  247. package/src/ai/component-markdown.ts +75 -0
  248. package/src/ai/cors.ts +2 -0
  249. package/src/ai/index.ts +12 -0
  250. package/src/ai/link-headers.ts +14 -5
  251. package/src/ai/llms.ts +24 -9
  252. package/src/ai/markdown.ts +2 -1
  253. package/src/ai/mcp/data.ts +6 -0
  254. package/src/ai/mcp/discovery.ts +2 -2
  255. package/src/ai/mcp/query.ts +2 -2
  256. package/src/ai/mcp/server.ts +2 -2
  257. package/src/ai/openapi-components.ts +31 -9
  258. package/src/ai/relative-links.ts +42 -26
  259. package/src/ai/serializers.ts +2 -1
  260. package/src/ai/site-skill.ts +274 -0
  261. package/src/ai/skills.ts +49 -3
  262. package/src/ai/tar.ts +139 -12
  263. package/src/ai/write-skill.ts +97 -0
  264. package/src/analytics/head.ts +4 -0
  265. package/src/analytics/index.ts +5 -0
  266. package/src/analytics/one-dollar-stats.ts +86 -0
  267. package/src/analytics/schema.ts +2 -0
  268. package/src/astro/generate.ts +133 -19
  269. package/src/astro/include-hmr.ts +31 -12
  270. package/src/astro/include-refresh.ts +19 -8
  271. package/src/astro/index.ts +1 -0
  272. package/src/astro/integration.ts +143 -14
  273. package/src/astro/module-types.ts +6 -0
  274. package/src/astro/runtime-modules.ts +28 -13
  275. package/src/astro/templates.ts +364 -67
  276. package/src/astro/variables.ts +31 -0
  277. package/src/audit/catalog.ts +19 -0
  278. package/src/audit/checks/contrast.ts +183 -0
  279. package/src/audit/checks/i18n.ts +2 -3
  280. package/src/audit/checks/indexability.ts +2 -3
  281. package/src/audit/checks/links.ts +13 -13
  282. package/src/audit/checks/sitemap.ts +2 -3
  283. package/src/audit/graph.ts +3 -1
  284. package/src/audit/redirects.ts +46 -15
  285. package/src/audit/run.ts +14 -9
  286. package/src/audit/snapshot.ts +5 -0
  287. package/src/audit/types.ts +2 -1
  288. package/src/blume-modules.d.ts +7 -0
  289. package/src/captcha/client.ts +152 -0
  290. package/src/captcha/hcaptcha.ts +36 -0
  291. package/src/captcha/index.ts +28 -0
  292. package/src/captcha/schema.ts +32 -0
  293. package/src/captcha/turnstile.ts +38 -0
  294. package/src/captcha/verify.ts +71 -0
  295. package/src/cli/args.ts +32 -0
  296. package/src/cli/command-meta.ts +5 -0
  297. package/src/cli/commands/audit.ts +4 -1
  298. package/src/cli/commands/doctor.ts +25 -17
  299. package/src/cli/commands/eval.ts +19 -11
  300. package/src/cli/commands/skill.ts +96 -0
  301. package/src/cli/commands/translate.ts +9 -12
  302. package/src/cli/commands/validate.ts +3 -1
  303. package/src/cli/dev-lock.ts +157 -37
  304. package/src/cli/index.ts +5 -0
  305. package/src/cli/report-format.ts +11 -6
  306. package/src/cli/required-secrets.ts +71 -29
  307. package/src/components/BlumePage.astro +2 -0
  308. package/src/components/colors.ts +19 -0
  309. package/src/components/content/ApiEndpoint.astro +18 -0
  310. package/src/components/content/ApiField.astro +79 -0
  311. package/src/components/content/ApiPlayground.astro +62 -0
  312. package/src/components/content/ApiRail.astro +19 -0
  313. package/src/components/content/Badge.astro +5 -3
  314. package/src/components/content/Color.astro +1 -0
  315. package/src/components/content/Component.astro +2 -2
  316. package/src/components/content/FileTree.astro +1 -0
  317. package/src/components/content/GithubInfo.astro +1 -0
  318. package/src/components/content/Panel.astro +1 -0
  319. package/src/components/content/ParamField.astro +25 -0
  320. package/src/components/content/RequestExample.astro +17 -0
  321. package/src/components/content/ResponseExample.astro +17 -0
  322. package/src/components/content/ResponseField.astro +32 -0
  323. package/src/components/content/Step.astro +1 -0
  324. package/src/components/content/Tab.astro +0 -1
  325. package/src/components/content/Tabs.astro +4 -0
  326. package/src/components/content/Tile.astro +1 -0
  327. package/src/components/content/Tooltip.astro +1 -0
  328. package/src/components/content/Tree.astro +1 -0
  329. package/src/components/content/TypeTable.astro +4 -1
  330. package/src/components/content/View.astro +21 -0
  331. package/src/components/content/ViewSwitcher.astro +168 -0
  332. package/src/components/content/api-field.ts +100 -0
  333. package/src/components/content/api-page.ts +224 -0
  334. package/src/components/content/badge-color.ts +5 -2
  335. package/src/components/content/base-href.ts +13 -36
  336. package/src/components/content/view-switcher.ts +71 -0
  337. package/src/components/index.ts +2 -0
  338. package/src/components/islands/Assistant.astro +10 -1
  339. package/src/components/islands/assistant.tsx +125 -18
  340. package/src/components/islands/base-path.ts +47 -10
  341. package/src/components/islands/code-question.ts +62 -0
  342. package/src/components/islands/hooks.ts +145 -40
  343. package/src/components/islands/support-link.ts +68 -0
  344. package/src/components/islands/webmcp.ts +4 -2
  345. package/src/components/layout/Analytics.astro +11 -2
  346. package/src/components/layout/Banner.astro +14 -10
  347. package/src/components/layout/Breadcrumbs.astro +11 -4
  348. package/src/components/layout/ConsentBanner.astro +62 -0
  349. package/src/components/layout/ConsentHead.astro +38 -0
  350. package/src/components/layout/Directory.astro +94 -0
  351. package/src/components/layout/DiscoveryLinks.astro +9 -5
  352. package/src/components/layout/DrawerSwitchers.astro +51 -0
  353. package/src/components/layout/Empty.astro +2 -2
  354. package/src/components/layout/Header.astro +124 -82
  355. package/src/components/layout/LanguageSwitcher.astro +50 -17
  356. package/src/components/layout/NarrationPlayer.astro +1085 -0
  357. package/src/components/layout/NavSelector.astro +35 -9
  358. package/src/components/layout/NavTabMenu.astro +3 -3
  359. package/src/components/layout/NavTree.astro +16 -6
  360. package/src/components/layout/PageActions.astro +4 -3
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +78 -9
  363. package/src/components/layout/Pagination.astro +3 -3
  364. package/src/components/layout/ReferenceLayout.astro +17 -6
  365. package/src/components/layout/RelatedPages.astro +42 -0
  366. package/src/components/layout/RootLayout.astro +313 -50
  367. package/src/components/layout/Search.astro +64 -30
  368. package/src/components/layout/SiteFooter.astro +124 -0
  369. package/src/components/layout/SiteMetatags.astro +16 -0
  370. package/src/components/layout/VersionBanner.astro +2 -2
  371. package/src/components/layout/analytics-client.ts +12 -0
  372. package/src/components/layout/consent/native.ts +79 -0
  373. package/src/components/layout/consent/types.ts +37 -0
  374. package/src/components/layout/footer-links.ts +53 -0
  375. package/src/components/layout/footer-socials.ts +87 -0
  376. package/src/components/layout/locale-redirect.ts +107 -0
  377. package/src/components/layout/nav-utils.ts +83 -0
  378. package/src/components/layout/page-feedback.ts +96 -0
  379. package/src/components/layout/search/analytics.ts +98 -0
  380. package/src/components/layout/search/flexsearch.ts +17 -5
  381. package/src/components/layout/search/orama-cloud.ts +26 -14
  382. package/src/components/layout/search/types.ts +20 -0
  383. package/src/components/layout/search/typesense.ts +5 -1
  384. package/src/components/layout/toc-active.ts +41 -0
  385. package/src/components/layout/toc-element.ts +8 -14
  386. package/src/components/openapi/ApiTagOperations.astro +2 -2
  387. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  388. package/src/components/openapi/GraphqlChip.astro +2 -2
  389. package/src/components/openapi/Operation.astro +163 -13
  390. package/src/components/openapi/Playground.astro +4 -4
  391. package/src/components/openapi/RequestBody.astro +6 -4
  392. package/src/components/openapi/RequestPanel.astro +63 -8
  393. package/src/components/openapi/Responses.astro +6 -4
  394. package/src/components/openapi/SchemaTable.astro +7 -0
  395. package/src/components/openapi/async-snippets.ts +4 -4
  396. package/src/components/openapi/async.ts +38 -6
  397. package/src/components/openapi/code-samples.ts +97 -0
  398. package/src/components/openapi/helpers.ts +19 -9
  399. package/src/components/openapi/message-composer.ts +6 -1
  400. package/src/components/openapi/message.ts +17 -2
  401. package/src/components/openapi/operation-model.ts +81 -11
  402. package/src/components/openapi/panel.ts +4 -2
  403. package/src/components/openapi/param-style.ts +181 -0
  404. package/src/components/openapi/playground-client.ts +73 -16
  405. package/src/components/openapi/request.ts +190 -26
  406. package/src/components/openapi/schema-tree.ts +30 -24
  407. package/src/components/openapi/security.ts +6 -3
  408. package/src/components/openapi/snippets.ts +953 -13
  409. package/src/components/openapi/ws-client.ts +18 -2
  410. package/src/components/props.ts +6 -0
  411. package/src/consent/client.ts +95 -0
  412. package/src/consent/clients.ts +19 -0
  413. package/src/consent/ethyca.ts +71 -0
  414. package/src/consent/head.ts +24 -0
  415. package/src/consent/index.ts +37 -0
  416. package/src/consent/init.ts +29 -0
  417. package/src/consent/native.ts +36 -0
  418. package/src/consent/osano.ts +59 -0
  419. package/src/consent/schema.ts +30 -0
  420. package/src/core/base-path.ts +62 -16
  421. package/src/core/builtin-tags.ts +5 -0
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/config-input.ts +203 -27
  424. package/src/core/data.ts +44 -4
  425. package/src/core/diagnostics.ts +5 -2
  426. package/src/core/directive-diagnostics.ts +99 -0
  427. package/src/core/footer.ts +26 -0
  428. package/src/core/frontmatter.ts +21 -18
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/heading-markers.ts +5 -18
  431. package/src/core/i18n-ui.ts +82 -2
  432. package/src/core/i18n.ts +31 -3
  433. package/src/core/includes.ts +20 -3
  434. package/src/core/last-modified.ts +25 -3
  435. package/src/core/links.ts +16 -2
  436. package/src/core/locale-links.ts +5 -1
  437. package/src/core/localizable.ts +26 -0
  438. package/src/core/meta.ts +33 -19
  439. package/src/core/navigation.ts +71 -20
  440. package/src/core/page-modes.ts +56 -0
  441. package/src/core/project-graph.ts +138 -10
  442. package/src/core/redirect-patterns.ts +491 -0
  443. package/src/core/related.ts +131 -0
  444. package/src/core/schema.ts +342 -68
  445. package/src/core/sources/contentful-rich-text.ts +27 -20
  446. package/src/core/sources/filesystem.ts +20 -2
  447. package/src/core/sources/github-releases.ts +25 -6
  448. package/src/core/sources/lexical.ts +23 -18
  449. package/src/core/sources/lower.ts +201 -34
  450. package/src/core/sources/mdx-remote.ts +51 -16
  451. package/src/core/sources/normalize.ts +327 -91
  452. package/src/core/sources/notion.ts +52 -25
  453. package/src/core/sources/obsidian.ts +23 -6
  454. package/src/core/sources/portable-text.ts +38 -21
  455. package/src/core/sources/read.ts +8 -3
  456. package/src/core/sources/strapi-blocks.ts +20 -16
  457. package/src/core/sources/watch.ts +20 -7
  458. package/src/core/standard-schema.ts +10 -6
  459. package/src/core/types.ts +1 -1
  460. package/src/core/ui-packs/ar.ts +43 -0
  461. package/src/core/ui-packs/bg.ts +45 -0
  462. package/src/core/ui-packs/bn.ts +43 -0
  463. package/src/core/ui-packs/ca.ts +46 -0
  464. package/src/core/ui-packs/cs.ts +44 -0
  465. package/src/core/ui-packs/da.ts +45 -0
  466. package/src/core/ui-packs/de.ts +46 -0
  467. package/src/core/ui-packs/el.ts +45 -0
  468. package/src/core/ui-packs/es.ts +46 -0
  469. package/src/core/ui-packs/fa.ts +45 -0
  470. package/src/core/ui-packs/fi.ts +45 -0
  471. package/src/core/ui-packs/fr.ts +46 -0
  472. package/src/core/ui-packs/he.ts +43 -0
  473. package/src/core/ui-packs/hi.ts +44 -0
  474. package/src/core/ui-packs/hr.ts +45 -0
  475. package/src/core/ui-packs/hu.ts +44 -0
  476. package/src/core/ui-packs/id.ts +45 -0
  477. package/src/core/ui-packs/it.ts +45 -0
  478. package/src/core/ui-packs/ja.ts +45 -0
  479. package/src/core/ui-packs/ko.ts +44 -0
  480. package/src/core/ui-packs/nl.ts +46 -0
  481. package/src/core/ui-packs/no.ts +44 -0
  482. package/src/core/ui-packs/pl.ts +45 -0
  483. package/src/core/ui-packs/pt-br.ts +46 -0
  484. package/src/core/ui-packs/pt.ts +45 -0
  485. package/src/core/ui-packs/ro.ts +45 -0
  486. package/src/core/ui-packs/ru.ts +45 -0
  487. package/src/core/ui-packs/sk.ts +44 -0
  488. package/src/core/ui-packs/sr.ts +45 -0
  489. package/src/core/ui-packs/sv.ts +44 -0
  490. package/src/core/ui-packs/th.ts +43 -0
  491. package/src/core/ui-packs/tr.ts +45 -0
  492. package/src/core/ui-packs/uk.ts +45 -0
  493. package/src/core/ui-packs/vi.ts +45 -0
  494. package/src/core/ui-packs/zh-tw.ts +43 -0
  495. package/src/core/ui-packs/zh.ts +43 -0
  496. package/src/core/variables.ts +103 -0
  497. package/src/core/version-cut.ts +52 -15
  498. package/src/deploy/artifacts.ts +104 -14
  499. package/src/deploy/cloudflare-negotiation.ts +39 -17
  500. package/src/deploy/headers.ts +10 -5
  501. package/src/deploy/node-headers.ts +64 -10
  502. package/src/deploy/platforms/cloudflare.ts +44 -3
  503. package/src/deploy/platforms/netlify.ts +34 -1
  504. package/src/deploy/platforms/node.ts +1 -0
  505. package/src/deploy/platforms/static.ts +1 -0
  506. package/src/deploy/platforms/types.ts +8 -0
  507. package/src/deploy/platforms/vercel.ts +38 -2
  508. package/src/deploy/redirects.ts +139 -36
  509. package/src/deploy/robots.ts +2 -2
  510. package/src/deploy/rss.ts +4 -3
  511. package/src/deploy/sitemap.ts +7 -5
  512. package/src/deploy/vercel-negotiation.ts +36 -0
  513. package/src/markdown/api-rail.ts +259 -0
  514. package/src/markdown/base-links.ts +34 -36
  515. package/src/markdown/code-title.ts +35 -9
  516. package/src/markdown/directives.ts +242 -36
  517. package/src/markdown/features.ts +20 -2
  518. package/src/markdown/fence-meta.ts +2 -0
  519. package/src/markdown/include.ts +11 -1
  520. package/src/markdown/index.ts +35 -11
  521. package/src/markdown/math.ts +4 -4
  522. package/src/markdown/mdast.ts +5 -2
  523. package/src/markdown/relative-links.ts +12 -4
  524. package/src/markdown/variables.ts +56 -0
  525. package/src/markdown/views.ts +82 -0
  526. package/src/narration/build.ts +412 -0
  527. package/src/narration/player.ts +249 -0
  528. package/src/narration/provider.ts +29 -0
  529. package/src/narration/script.ts +592 -0
  530. package/src/og/card.ts +136 -11
  531. package/src/og/derive.ts +41 -31
  532. package/src/og/index.ts +1 -0
  533. package/src/openapi/asyncapi.ts +4 -2
  534. package/src/openapi/model.ts +77 -36
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +55 -2
  537. package/src/openapi/proxy.ts +63 -10
  538. package/src/openapi/references.ts +21 -5
  539. package/src/openapi/render-mdx.ts +46 -3
  540. package/src/openapi/scalar.ts +40 -6
  541. package/src/openapi/source.ts +1 -1
  542. package/src/ratelimit/cloudflare.ts +54 -0
  543. package/src/ratelimit/index.ts +30 -0
  544. package/src/ratelimit/memory.ts +45 -0
  545. package/src/ratelimit/runtime.ts +229 -0
  546. package/src/ratelimit/schema.ts +28 -0
  547. package/src/ratelimit/upstash.ts +30 -0
  548. package/src/ratelimit/wrangler.ts +70 -0
  549. package/src/reference/asyncapi.ts +2 -2
  550. package/src/reference/graphql.ts +5 -2
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +51 -7
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +201 -28
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/version-scope.ts +30 -0
  557. package/src/search/documents.ts +93 -28
  558. package/src/search/orama-index.ts +28 -3
  559. package/src/search/pagefind-ranking.ts +39 -0
  560. package/src/search/popular.ts +2 -1
  561. package/src/search/sync/algolia.ts +15 -4
  562. package/src/search/sync/orama-cloud.ts +3 -0
  563. package/src/search/sync/typesense.ts +5 -0
  564. package/src/seo/jsonld.ts +7 -3
  565. package/src/seo/metatags.ts +67 -0
  566. package/src/theme/chrome-icons.ts +1 -0
  567. package/src/theme/color.ts +500 -0
  568. package/src/theme/entry.ts +56 -3
  569. package/src/theme/palette.ts +121 -45
  570. package/src/translate/meta.ts +68 -24
  571. package/src/translate/prompts.ts +1 -0
  572. package/src/translate/run.ts +3 -3
  573. package/src/translate/validate.ts +12 -3
  574. package/src/translate/work-list.ts +56 -17
  575. package/src/upgrade/upgrade.ts +4 -171
  576. package/dist/cli/chunk-3yce002v.js.map +0 -14
  577. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  578. package/dist/cli/chunk-6dtt0zfn.js.map +0 -10
  579. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  580. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  581. package/dist/cli/chunk-esh98wmb.js +0 -23
  582. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  583. package/dist/cli/chunk-hqp2ajnh.js.map +0 -35
  584. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  585. package/dist/cli/chunk-j85scx15.js.map +0 -10
  586. package/dist/cli/chunk-mqc662a6.js +0 -75
  587. package/dist/cli/chunk-mqc662a6.js.map +0 -11
  588. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  589. package/dist/cli/chunk-qkb5a8sa.js.map +0 -10
  590. package/dist/cli/chunk-v6ya5kcb.js.map +0 -189
  591. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  592. package/dist/cli/chunk-xh43dwgw.js.map +0 -36
  593. package/docs/content/sources.mdx +0 -338
  594. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-1n228xqb.js.map} +0 -0
  595. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-5ffywsmb.js.map} +0 -0
  596. /package/dist/cli/{chunk-g698a744.js.map → chunk-5wj2qwn2.js.map} +0 -0
  597. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-amrb6hgh.js.map} +0 -0
  598. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-gp84za0h.js.map} +0 -0
  599. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-p804f3pe.js.map} +0 -0
  600. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-ta27feev.js.map} +0 -0
  601. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-y4ykbark.js.map} +0 -0
@@ -23,7 +23,12 @@ import { HTML_COMMENT } from "../core/sources/normalize.ts";
23
23
  import { readExpandedEntryText } from "../core/sources/read.ts";
24
24
  import type { NavNode, PageRecord } from "../core/types.ts";
25
25
  import { parseCodeTitle } from "../markdown/code-title.ts";
26
- import { MARKDOWN_FEATURES, MDX_FEATURES } from "../markdown/features.ts";
26
+ import { directiveSource } from "../markdown/directives.ts";
27
+ import type { DirectiveNode } from "../markdown/directives.ts";
28
+ import {
29
+ MARKDOWN_BODY_FEATURES,
30
+ MDX_BODY_FEATURES,
31
+ } from "../markdown/features.ts";
27
32
  import { pageFacets } from "./facets.ts";
28
33
 
29
34
  /** A document indexed by the client-side search providers (Orama, FlexSearch). */
@@ -49,6 +54,10 @@ export interface SearchDocument {
49
54
  * Filterable through the MCP tools' `filters` input.
50
55
  */
51
56
  facets?: Record<string, string>;
57
+ /** Frontmatter `search.boost`: the page's relevance multiplier, when set. */
58
+ boost?: number;
59
+ /** Frontmatter `search.keywords`: extra terms the page is found by. */
60
+ keywords?: string[];
52
61
  }
53
62
 
54
63
  /**
@@ -72,6 +81,13 @@ export interface SearchRecord {
72
81
  version: string;
73
82
  /** Single faceting tag (the first frontmatter tag, when present). */
74
83
  tag?: string;
84
+ /**
85
+ * The page's relevance multiplier, 1 unless `search.boost` sets it: a
86
+ * ranking attribute every record carries, so the backend can sort by it.
87
+ */
88
+ boost: number;
89
+ /** Extra terms the page is found by (`search.keywords`). */
90
+ keywords?: string[];
75
91
  }
76
92
 
77
93
  // Tag-shaped only: a name (or closing slash/fragment) right after `<`, and no
@@ -94,7 +110,6 @@ const INLINE_PARENTS = new Set([
94
110
  "strong",
95
111
  "subscript",
96
112
  "superscript",
97
- "textDirective",
98
113
  ]);
99
114
 
100
115
  // Nodes that never render as prose: image alt text was never indexed, MDX
@@ -120,9 +135,9 @@ interface Walk {
120
135
 
121
136
  // Front matter is already off (core/frontmatter.ts) by the time a body gets
122
137
  // here, so a body that opens with a `---` divider must read as a thematic
123
- // break rather than a second front matter block.
124
- const MD_PARSE = { features: { ...MARKDOWN_FEATURES, frontmatter: false } };
125
- const MDX_PARSE = { features: { ...MDX_FEATURES, frontmatter: false } };
138
+ // break rather than a second front matter block — as the renderer reads it.
139
+ const MD_PARSE = { features: MARKDOWN_BODY_FEATURES };
140
+ const MDX_PARSE = { features: MDX_BODY_FEATURES };
126
141
 
127
142
  const parseMdx = (markdown: string): Nodes | undefined => {
128
143
  try {
@@ -170,6 +185,25 @@ const collectCode = (
170
185
  out.push(node.value, " ");
171
186
  };
172
187
 
188
+ /**
189
+ * A text or leaf directive as the page shows it. Blume renders neither (see
190
+ * `markdown/directives.ts`), and prose is full of text that parses as one —
191
+ * `16:9`, `og:image` — so the page shows the source the author wrote, and the
192
+ * index reads the same text rather than dropping the `:name` part.
193
+ */
194
+ const directiveText = (
195
+ node: Extract<Nodes, { type: "leafDirective" | "textDirective" }>,
196
+ walk: Walk
197
+ ): string =>
198
+ directiveSource(
199
+ // SAFETY: DirectiveNode is a structural subset of Satteri's directive
200
+ // nodes — a `name`, optional attributes and children, and a position.
201
+ node as DirectiveNode,
202
+ node.type === "textDirective" ? ":" : "::",
203
+ // The walked tree was parsed from this text, so its offsets index into it.
204
+ walk.downlevel.source
205
+ );
206
+
173
207
  // Block boundaries separate words; so does an empty inline element (`<br />`,
174
208
  // a self-closing icon), which otherwise fuses its neighbors.
175
209
  const separatesWords = (node: Extract<Nodes, { children: unknown }>): boolean =>
@@ -223,28 +257,17 @@ const collectText = (node: Nodes, out: string[], walk: Walk): void => {
223
257
  out.push(" ");
224
258
  return;
225
259
  }
226
- // Trailing heading markers (`[#custom-id]`, `{#custom-id}`, `[!toc]`,
227
- // `[toc]`) are anchor
228
-
229
- // metadata, not prose — strip them so they never pollute the index. Only
230
- // a marker that ends the heading's final plain-text child counts,
231
- // mirroring the renderer: a heading ending in inline code or an image
232
- // keeps its bracketed text on the page (so it stays searchable), and a
233
- // heading that is nothing but markers renders them literally.
260
+ case "textDirective": {
261
+ out.push(directiveText(node, walk));
262
+ return;
263
+ }
264
+ case "leafDirective": {
265
+ out.push(directiveText(node, walk), " ");
266
+ return;
267
+ }
234
268
  case "heading": {
235
- const last = node.children.at(-1);
236
- const trailing = last?.type === "text" ? last : undefined;
237
- const inner: string[] = [];
238
- const kept = trailing ? node.children.slice(0, -1) : node.children;
239
- for (const child of kept) {
240
- collectText(child, inner, walk);
241
- }
242
- if (trailing) {
243
- const stripped = parseHeadingMarkers(trailing.value).text;
244
- const literal = stripped === "" && node.children.length === 1;
245
- inner.push(literal ? trailing.value : stripped);
246
- }
247
- out.push(inner.join("").trimEnd(), " ");
269
+ // oxlint-disable-next-line no-use-before-define -- mutual recursion: a heading's children are walked like any others
270
+ collectHeading(node, out, walk);
248
271
  return;
249
272
  }
250
273
  default: {
@@ -265,6 +288,35 @@ const collectText = (node: Nodes, out: string[], walk: Walk): void => {
265
288
  }
266
289
  };
267
290
 
291
+ /**
292
+ * Fold a heading into the accumulator. Trailing heading markers
293
+ * (`[#custom-id]`, `{#custom-id}`, `[!toc]`, `[toc]`) are anchor metadata,
294
+ * not prose — strip them so they never pollute the index. Only a marker that
295
+ * ends the heading's final plain-text child counts, mirroring the renderer: a
296
+ * heading ending in inline code or an image keeps its bracketed text on the
297
+ * page (so it stays searchable), and a heading that is nothing but markers
298
+ * renders them literally.
299
+ */
300
+ const collectHeading = (
301
+ node: Extract<Nodes, { type: "heading" }>,
302
+ out: string[],
303
+ walk: Walk
304
+ ): void => {
305
+ const last = node.children.at(-1);
306
+ const trailing = last?.type === "text" ? last : undefined;
307
+ const inner: string[] = [];
308
+ const kept = trailing ? node.children.slice(0, -1) : node.children;
309
+ for (const child of kept) {
310
+ collectText(child, inner, walk);
311
+ }
312
+ if (trailing) {
313
+ const stripped = parseHeadingMarkers(trailing.value).text;
314
+ const literal = stripped === "" && node.children.length === 1;
315
+ inner.push(literal ? trailing.value : stripped);
316
+ }
317
+ out.push(inner.join("").trimEnd(), " ");
318
+ };
319
+
268
320
  /**
269
321
  * Reduce Markdown/MDX to plain, searchable text: parse (GFM included) and walk
270
322
  * the tree instead of regex-stripping the source, so reference-style links,
@@ -373,6 +425,17 @@ const pageBody = async (
373
425
  });
374
426
  };
375
427
 
428
+ /** The ranking a page asks for: its `search.boost` and `search.keywords`. */
429
+ const rankingFields = (
430
+ search: PageRecord["meta"]["search"] | undefined
431
+ ): Pick<SearchDocument, "boost" | "keywords"> => {
432
+ const { boost, keywords } = search ?? {};
433
+ return {
434
+ ...(boost !== undefined && boost !== 1 && { boost }),
435
+ ...(keywords && keywords.length > 0 && { keywords }),
436
+ };
437
+ };
438
+
376
439
  /**
377
440
  * Build search documents from the content graph. Only indexable pages are
378
441
  * included (per the route manifest), and content comes from the source files,
@@ -490,7 +553,7 @@ export const buildSearchDocuments = async (
490
553
  if (facets) {
491
554
  document.facets = facets;
492
555
  }
493
- return document;
556
+ return { ...document, ...rankingFields(page?.meta?.search) };
494
557
  })
495
558
  );
496
559
  };
@@ -498,13 +561,15 @@ export const buildSearchDocuments = async (
498
561
  /**
499
562
  * Map per-page search documents to the flat record shape hosted backends
500
563
  * ingest. One record per page, keyed by route; the first tag becomes the
501
- * faceting `tag`.
564
+ * faceting `tag`, and every record carries a `boost` (1 by default).
502
565
  */
503
566
  export const toSearchRecords = (documents: SearchDocument[]): SearchRecord[] =>
504
567
  documents.map((doc) => ({
505
568
  _id: doc.route,
569
+ boost: doc.boost ?? 1,
506
570
  content: doc.content,
507
571
  description: doc.description,
572
+ ...(doc.keywords && { keywords: doc.keywords }),
508
573
  locale: doc.locale,
509
574
  tag: doc.tags?.[0],
510
575
  title: doc.title,
@@ -31,6 +31,10 @@ export interface OramaDoc {
31
31
  * but not indexed, so they ride along on the returned document untouched. */
32
32
  breadcrumb?: string[];
33
33
  section?: string;
34
+ /** The page's relevance multiplier (`search.boost`); stored, not indexed. */
35
+ boost?: number;
36
+ /** Extra terms the page is found by (`search.keywords`). */
37
+ keywords?: string[];
34
38
  }
35
39
 
36
40
  const SCHEMA = {
@@ -42,6 +46,7 @@ const SCHEMA = {
42
46
  // serves every project's facet keys, with `containsAll` matching a filter
43
47
  // set. Derived from `facets` at insert time.
44
48
  facetTerms: "enum[]",
49
+ keywords: "string[]",
45
50
  locale: "enum",
46
51
  route: "string",
47
52
  title: "string",
@@ -52,8 +57,21 @@ const SCHEMA = {
52
57
  const toFacetTerms = (facets: Record<string, string>): string[] =>
53
58
  Object.entries(facets).map(([key, value]) => `${key}:${value}`);
54
59
 
55
- /** Title and description outrank body text, matching the search dialog. */
56
- const BOOST = { description: 2, title: 3 };
60
+ /**
61
+ * Title and description outrank body text, matching the search dialog; a
62
+ * page's keywords count as much as its title, since its author chose them.
63
+ */
64
+ const BOOST = { description: 2, keywords: 3, title: 3 };
65
+
66
+ /** Databases holding a boosted page, which rank by boosted score. */
67
+ const boostedDatabases = new WeakSet<AnyOrama>();
68
+
69
+ /** A search match as Orama's sort function sees it: id, score, document. */
70
+ type RankedMatch = [unknown, number, OramaDoc];
71
+
72
+ /** Rank matches by relevance times the page's `search.boost`. */
73
+ const byBoostedScore = (a: RankedMatch, b: RankedMatch): number =>
74
+ b[1] * (b[2].boost ?? 1) - a[1] * (a[2].boost ?? 1);
57
75
 
58
76
  /**
59
77
  * The script whose text Orama's default tokenizer keeps mostly intact. Its
@@ -322,6 +340,9 @@ export const buildOramaIndex = async (
322
340
  doc.facets ? { ...doc, facetTerms: toFacetTerms(doc.facets) } : doc
323
341
  )
324
342
  );
343
+ if (documents.some((doc) => (doc.boost ?? 1) !== 1)) {
344
+ boostedDatabases.add(db);
345
+ }
325
346
  return db;
326
347
  };
327
348
 
@@ -391,10 +412,14 @@ export const queryOramaIndex = async (
391
412
  if (facetTerms.length > 0) {
392
413
  where.facetTerms = { containsAll: facetTerms };
393
414
  }
415
+ // Boosts multiply every match's score before the limit applies (Orama
416
+ // sorts the whole match set with a sort function), so a boosted page can
417
+ // rise past matches outside the first page.
394
418
  const unfiltered = {
395
419
  boost: BOOST,
396
420
  limit,
397
- properties: ["title", "description", "content"],
421
+ properties: ["title", "keywords", "description", "content"],
422
+ ...(boostedDatabases.has(db) && { sortBy: byBoostedScore }),
398
423
  term,
399
424
  };
400
425
  const params =
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A page's `search.boost` and `search.keywords`, as Pagefind reads them from
3
+ * the built page. Pagefind has no page-level boost, only element weights, so
4
+ * the page's body carries its boost as its weight: a boosted page's matches
5
+ * count for more and it ranks higher. Pagefind damps repeated matches, so the
6
+ * lift is smaller than the full multiplier: a boost of 5 scored its page about
7
+ * 1.5 to 2 times higher in testing. A boost past Pagefind's largest weight,
8
+ * 10, counts as 10.
9
+ * Keywords go on an empty element as an indexed attribute, found like text
10
+ * and never shown.
11
+ */
12
+
13
+ /** The largest weight Pagefind takes. */
14
+ const MAX_WEIGHT = 10;
15
+
16
+ /** The attribute a page's keywords ride on. */
17
+ export const KEYWORDS_ATTRIBUTE = "data-blume-keywords";
18
+
19
+ /** What the page body carries for Pagefind. */
20
+ export interface PagefindRanking {
21
+ /** `data-pagefind-weight` for the body, when the page is boosted. */
22
+ weight?: string;
23
+ /** The keywords, space-separated, when the page sets any. */
24
+ keywords?: string;
25
+ }
26
+
27
+ export const pagefindRanking = (search: {
28
+ boost?: number;
29
+ keywords?: string[];
30
+ }): PagefindRanking => {
31
+ const { boost, keywords } = search;
32
+ return {
33
+ ...(boost !== undefined &&
34
+ boost !== 1 && {
35
+ weight: String(Math.min(boost, MAX_WEIGHT)),
36
+ }),
37
+ ...(keywords && keywords.length > 0 && { keywords: keywords.join(" ") }),
38
+ };
39
+ };
@@ -17,7 +17,8 @@ export interface SearchPopularPage {
17
17
  * `page.route`). `withBasePath` is idempotent and skips external URLs.
18
18
  *
19
19
  * `deployment.base` is deliberately *not* applied: routes stay deploy-base-less
20
- * so `createLinkRow` can prefix it at click time, same as sidebar-derived pages.
20
+ * so `Search.astro` can add it when it renders the entry, same as
21
+ * sidebar-derived pages.
21
22
  *
22
23
  * `icon` stays a name here (like `navigation.featured`): the icon set is a
23
24
  * server-only module, so `Search.astro` resolves it to inline SVG at render.
@@ -16,6 +16,9 @@ export type AlgoliaSyncConfig = Pick<AlgoliaOptions, "appId" | "indexName">;
16
16
  */
17
17
  const FILTER_ATTRIBUTES = ["locale", "version"] as const;
18
18
 
19
+ /** The custom ranking rule that orders close matches by `search.boost`. */
20
+ const BOOST_RANKING = "desc(boost)";
21
+
19
22
  /** A faceting declaration's attribute: `filterOnly(locale)` → `locale`. */
20
23
  const facetAttribute = (declaration: string): string =>
21
24
  declaration.replace(/^\w+\((?<name>.*)\)$/u, "$<name>");
@@ -48,12 +51,17 @@ export const syncAlgolia = async (
48
51
  indexName,
49
52
  objects: records.map((record) => ({ ...record, objectID: record._id })),
50
53
  });
51
- const { attributesForFaceting = [] } = await client.getSettings({
52
- indexName,
53
- });
54
+ const { attributesForFaceting = [], customRanking = [] } =
55
+ await client.getSettings({ indexName });
54
56
  const declared = new Set(attributesForFaceting.map(facetAttribute));
55
57
  const missing = FILTER_ATTRIBUTES.filter((name) => !declared.has(name));
56
- if (missing.length === 0) {
58
+ // Every record carries `boost` (search.boost, 1 by default): ranking by it
59
+ // after textual relevance puts boosted pages first among close matches.
60
+ // A ranking the site set in the dashboard keeps its place ahead of it.
61
+ const ranksByBoost = customRanking.some((rule) =>
62
+ /^(?:asc|desc)\(boost\)$/u.test(rule)
63
+ );
64
+ if (missing.length === 0 && ranksByBoost) {
57
65
  return;
58
66
  }
59
67
  const { taskID } = await client.setSettings({
@@ -63,6 +71,9 @@ export const syncAlgolia = async (
63
71
  ...attributesForFaceting,
64
72
  ...missing.map((name) => `filterOnly(${name})`),
65
73
  ],
74
+ customRanking: ranksByBoost
75
+ ? customRanking
76
+ : [...customRanking, BOOST_RANKING],
66
77
  },
67
78
  });
68
79
  await client.waitForTask({ indexName, taskID });
@@ -31,9 +31,12 @@ export const syncOramaCloud = async (
31
31
  const index = manager.index(config.indexId);
32
32
  await index.snapshot(
33
33
  records.map((record) => ({
34
+ // The dialog multiplies each hit's score by it (search.boost).
35
+ boost: record.boost,
34
36
  content: record.content,
35
37
  description: record.description,
36
38
  id: record._id,
39
+ keywords: record.keywords ?? [],
37
40
  // Carried so an i18n site can filter hosted results per language.
38
41
  locale: record.locale,
39
42
  tag: record.tag,
@@ -57,6 +57,9 @@ export const syncTypesense = async (
57
57
  { name: "description", optional: true, type: "string" },
58
58
  { name: "content", type: "string" },
59
59
  { name: "url", type: "string" },
60
+ { name: "keywords", optional: true, type: "string[]" },
61
+ // The dialog sorts close matches by it (search.boost, 1 by default).
62
+ { name: "boost", type: "float" },
60
63
  { facet: true, name: "tag", optional: true, type: "string" },
61
64
  // Carried as facets so hosted results can filter per language and per
62
65
  // docs version (the SearchRecord contract; current docs = "current").
@@ -67,9 +70,11 @@ export const syncTypesense = async (
67
70
  });
68
71
 
69
72
  const documents = records.map((record) => ({
73
+ boost: record.boost,
70
74
  content: record.content,
71
75
  description: record.description,
72
76
  id: record._id,
77
+ keywords: record.keywords ?? [],
73
78
  locale: record.locale,
74
79
  tag: record.tag,
75
80
  title: record.title,
package/src/seo/jsonld.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { Crumb } from "../components/layout/nav-utils.ts";
2
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
2
+ import {
3
+ mountBasePath,
4
+ normalizeBasePath,
5
+ withBasePath,
6
+ } from "../core/base-path.ts";
3
7
 
4
8
  /** A date-ish value carried through frontmatter (string, YAML Date, or unset). */
5
9
  type DateInput = string | Date | null;
@@ -301,7 +305,7 @@ const breadcrumbNode = (
301
305
  "@type": "BreadcrumbList",
302
306
  itemListElement: linked.map((crumb, index) => ({
303
307
  "@type": "ListItem",
304
- item: absolute(base, withBasePath(deployBase, crumb.route)),
308
+ item: absolute(base, mountBasePath(deployBase, crumb.route)),
305
309
  name: crumb.label,
306
310
  position: index + 1,
307
311
  })),
@@ -323,7 +327,7 @@ export const buildStructuredData = (
323
327
  // Routes carry `basePath`; a `deployment.base` subdirectory is layered on top
324
328
  // so JSON-LD URLs match the served location.
325
329
  const deployBase = normalizeBasePath(input.base);
326
- const pageUrl = absolute(base, withBasePath(deployBase, input.route));
330
+ const pageUrl = absolute(base, mountBasePath(deployBase, input.route));
327
331
  const rootUrl = absolute(base, deployBase);
328
332
  const graph: JsonLdNode[] = [];
329
333
 
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Site-wide meta tags (`seo.metatags`): tags written into every page's head,
3
+ * for what Blume has no setting of its own for, such as site-verification
4
+ * tokens, `theme-color`, or an app banner. The tags Blume writes itself are
5
+ * refused, each pointing at the setting that controls it, so a page never
6
+ * carries two values for one tag.
7
+ */
8
+
9
+ const IMAGE =
10
+ "`seo.og` for the generated cards, or `seo.image` in a page's frontmatter";
11
+ const TITLE = "`title`, or `seo.title` in a page's frontmatter";
12
+ const DESCRIPTION =
13
+ "`description` in a page's frontmatter or the config, or a page's `seo.description`";
14
+ const FROM_PAGE = "Blume sets it from each page's type and dates";
15
+
16
+ /** The tags Blume writes itself, by lowercase name, and what sets each. */
17
+ const OWNED = new Map([
18
+ ["article:modified_time", FROM_PAGE],
19
+ ["article:published_time", FROM_PAGE],
20
+ ["description", DESCRIPTION],
21
+ ["og:description", DESCRIPTION],
22
+ ["og:site_name", "the config's `title`"],
23
+ ["og:title", TITLE],
24
+ ["og:type", FROM_PAGE],
25
+ ["og:url", "`deployment.site`, which sets each page's canonical URL"],
26
+ ["robots", "`seo.noindex` in a page's frontmatter"],
27
+ ["twitter:card", "Blume sets it from each page's image"],
28
+ ["twitter:creator", "`seo.x.creator` in the config or a page's frontmatter"],
29
+ ["twitter:description", DESCRIPTION],
30
+ ["twitter:site", "`seo.x.handle`"],
31
+ ["twitter:title", TITLE],
32
+ ["viewport", "Blume sets it for every page"],
33
+ ]);
34
+
35
+ /** `og:image` and `twitter:image`, and their sub-properties. */
36
+ const IMAGE_TAG = /^(?:og|twitter):image(?::|$)/u;
37
+
38
+ /**
39
+ * What sets a tag Blume writes itself, or `undefined` for a tag the site can
40
+ * add.
41
+ */
42
+ export const ownedMetatag = (name: string): string | undefined => {
43
+ const key = name.trim().toLowerCase();
44
+ return IMAGE_TAG.test(key) ? IMAGE : OWNED.get(key);
45
+ };
46
+
47
+ /**
48
+ * The Open Graph families, which the protocol names with `property`; every
49
+ * other tag takes `name`.
50
+ */
51
+ const PROPERTY_TAG =
52
+ /^(?:og|fb|article|book|profile|music|video|al|product):/iu;
53
+
54
+ /** One site-wide tag, as its `<meta>` element's attributes. */
55
+ export interface MetatagAttributes {
56
+ content: string;
57
+ name?: string;
58
+ property?: string;
59
+ }
60
+
61
+ /** The site's `seo.metatags` as `<meta>` attributes, in the order written. */
62
+ export const metatagAttributes = (
63
+ tags?: Readonly<Record<string, string>> | null
64
+ ): MetatagAttributes[] =>
65
+ Object.entries(tags ?? {}).map(([name, content]) =>
66
+ PROPERTY_TAG.test(name) ? { content, property: name } : { content, name }
67
+ );
@@ -19,6 +19,7 @@ interface ChromeIconSet {
19
19
 
20
20
  export const chromeIcons: ChromeIconSet = {
21
21
  check: '<path d="M20 6 9 17l-5-5"/>',
22
+ "chevron-down": '<path d="m6 9 6 6 6-6"/>',
22
23
  copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
23
24
  file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
24
25
  search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',