blume 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (607) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +142 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-5f86nr5m.js → chunk-073rdvgb.js} +21 -48
  5. package/dist/cli/{chunk-5f86nr5m.js.map → chunk-073rdvgb.js.map} +4 -5
  6. package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
  7. package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
  8. package/dist/cli/chunk-150jk7km.js +52 -0
  9. package/dist/cli/chunk-150jk7km.js.map +10 -0
  10. package/dist/cli/{chunk-9t7a85s3.js → chunk-313vnn40.js} +30 -19
  11. package/dist/cli/chunk-313vnn40.js.map +10 -0
  12. package/dist/cli/{chunk-tkacnehg.js → chunk-3v3ffbrj.js} +2 -2
  13. package/dist/cli/{chunk-q58y5e6a.js → chunk-3x6x3pph.js} +46 -24
  14. package/dist/cli/chunk-3x6x3pph.js.map +10 -0
  15. package/dist/cli/{chunk-1d7ve1dm.js → chunk-42t4jxdx.js} +37 -5
  16. package/dist/cli/chunk-42t4jxdx.js.map +12 -0
  17. package/dist/cli/{chunk-8ktnccpt.js → chunk-54ztw1nj.js} +21 -2
  18. package/dist/cli/chunk-54ztw1nj.js.map +10 -0
  19. package/dist/cli/{chunk-hm3vjy5s.js → chunk-5vjtgs6e.js} +28 -10
  20. package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-5vjtgs6e.js.map} +4 -4
  21. package/dist/cli/chunk-66w9j7p6.js +322 -0
  22. package/dist/cli/chunk-66w9j7p6.js.map +14 -0
  23. package/dist/cli/{chunk-2hsdwb9n.js → chunk-7ppf1rva.js} +34 -18
  24. package/dist/cli/chunk-7ppf1rva.js.map +10 -0
  25. package/dist/cli/{chunk-r9rcc4w7.js → chunk-8y2kmjv8.js} +7 -7
  26. package/dist/cli/{chunk-r20tn01b.js → chunk-c1y538kd.js} +8 -2
  27. package/dist/cli/{chunk-r20tn01b.js.map → chunk-c1y538kd.js.map} +3 -3
  28. package/dist/cli/{chunk-2eytanqx.js → chunk-c887fj0s.js} +2 -2
  29. package/dist/cli/{chunk-p3v96n38.js → chunk-cfyg47zg.js} +16 -6
  30. package/dist/cli/chunk-cfyg47zg.js.map +10 -0
  31. package/dist/cli/chunk-cm89scdv.js +443 -0
  32. package/dist/cli/chunk-cm89scdv.js.map +11 -0
  33. package/dist/cli/{chunk-akbpwfxc.js → chunk-dzwehpy7.js} +2 -2
  34. package/dist/cli/{chunk-wrr3j9w9.js → chunk-e6atmn4h.js} +299 -72
  35. package/dist/cli/chunk-e6atmn4h.js.map +36 -0
  36. package/dist/cli/{chunk-tzmab476.js → chunk-e8xw9qkh.js} +8 -6
  37. package/dist/cli/{chunk-tzmab476.js.map → chunk-e8xw9qkh.js.map} +3 -3
  38. package/dist/cli/{chunk-6dsbexzp.js → chunk-e9q7a2ak.js} +17 -15
  39. package/dist/cli/{chunk-6dsbexzp.js.map → chunk-e9q7a2ak.js.map} +2 -2
  40. package/dist/cli/{chunk-pv29h0wf.js → chunk-ehweg4fr.js} +5155 -1249
  41. package/dist/cli/chunk-ehweg4fr.js.map +218 -0
  42. package/dist/cli/{chunk-c8chx29p.js → chunk-ep6nd0rn.js} +390 -318
  43. package/dist/cli/chunk-ep6nd0rn.js.map +36 -0
  44. package/dist/cli/{chunk-p73c0m7w.js → chunk-fdrkhpjy.js} +18 -15
  45. package/dist/cli/{chunk-p73c0m7w.js.map → chunk-fdrkhpjy.js.map} +3 -3
  46. package/dist/cli/{chunk-2z928egk.js → chunk-ft9vfys6.js} +5 -5
  47. package/dist/cli/{chunk-5m5nmvyq.js → chunk-gb420vvz.js} +316 -79
  48. package/dist/cli/chunk-gb420vvz.js.map +17 -0
  49. package/dist/cli/{chunk-vg9r4eb9.js → chunk-hebn0tpx.js} +86 -13
  50. package/dist/cli/chunk-hebn0tpx.js.map +14 -0
  51. package/dist/cli/{chunk-35d4wj9f.js → chunk-htrwgabr.js} +41 -23
  52. package/dist/cli/chunk-htrwgabr.js.map +15 -0
  53. package/dist/cli/{chunk-g4hq16wv.js → chunk-hx3xqnk9.js} +16 -14
  54. package/dist/cli/{chunk-g4hq16wv.js.map → chunk-hx3xqnk9.js.map} +2 -2
  55. package/dist/cli/chunk-j134vrp2.js +65 -0
  56. package/dist/cli/chunk-j134vrp2.js.map +10 -0
  57. package/dist/cli/{chunk-a58773jm.js → chunk-kma9f6sf.js} +2 -2
  58. package/dist/cli/chunk-kwrg63tm.js +31 -0
  59. package/dist/cli/chunk-kwrg63tm.js.map +10 -0
  60. package/dist/cli/{chunk-ey84smr6.js → chunk-mrcccgnh.js} +6 -6
  61. package/dist/cli/{chunk-ey84smr6.js.map → chunk-mrcccgnh.js.map} +1 -1
  62. package/dist/cli/{chunk-yfyb25rh.js → chunk-n0v7m8ry.js} +24 -8
  63. package/dist/cli/chunk-n0v7m8ry.js.map +14 -0
  64. package/dist/cli/{chunk-j85vccga.js → chunk-ncj7w596.js} +576 -367
  65. package/dist/cli/chunk-ncj7w596.js.map +35 -0
  66. package/dist/cli/{chunk-5xvm6tfj.js → chunk-nq1d20w2.js} +17 -15
  67. package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-nq1d20w2.js.map} +2 -2
  68. package/dist/cli/{chunk-pehfxfta.js → chunk-nrns10mj.js} +6 -98
  69. package/dist/cli/chunk-nrns10mj.js.map +10 -0
  70. package/dist/cli/{chunk-crgn1q09.js → chunk-sds923fa.js} +4 -4
  71. package/dist/cli/chunk-t2m5j4pr.js +104 -0
  72. package/dist/cli/chunk-t2m5j4pr.js.map +11 -0
  73. package/dist/cli/{chunk-acanzt5p.js → chunk-tdqhyxfz.js} +6 -6
  74. package/dist/cli/chunk-tphac74f.js +126 -0
  75. package/dist/cli/chunk-tphac74f.js.map +11 -0
  76. package/dist/cli/chunk-w1r4vase.js +18 -0
  77. package/dist/cli/chunk-w1r4vase.js.map +10 -0
  78. package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
  79. package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
  80. package/dist/cli/index.js +31 -20
  81. package/dist/cli/index.js.map +5 -5
  82. package/dist/types/ai/agent-surface.d.ts +8 -7
  83. package/dist/types/ai/ai-catalog.d.ts +7 -0
  84. package/dist/types/ai/ask-context.d.ts +26 -0
  85. package/dist/types/ai/ask.d.ts +271 -58
  86. package/dist/types/ai/component-markdown.d.ts +2 -0
  87. package/dist/types/ai/index.d.ts +2 -2
  88. package/dist/types/ai/openapi-components.d.ts +4 -1
  89. package/dist/types/ai/site-skill.d.ts +26 -0
  90. package/dist/types/ai/skills.d.ts +16 -0
  91. package/dist/types/astro/integration.d.ts +9 -0
  92. package/dist/types/astro/markdown-negotiation.d.ts +3 -0
  93. package/dist/types/astro/pages.d.ts +10 -0
  94. package/dist/types/captcha/hcaptcha.d.ts +27 -0
  95. package/dist/types/captcha/index.d.ts +28 -0
  96. package/dist/types/captcha/schema.d.ts +29 -0
  97. package/dist/types/captcha/turnstile.d.ts +28 -0
  98. package/dist/types/cli/init/scaffold.d.ts +1 -1
  99. package/dist/types/components/content/api-field.d.ts +62 -0
  100. package/dist/types/components/content/api-page.d.ts +100 -0
  101. package/dist/types/components/layout/nav-utils.d.ts +26 -0
  102. package/dist/types/components/openapi/code-samples.d.ts +29 -0
  103. package/dist/types/components/openapi/helpers.d.ts +158 -0
  104. package/dist/types/components/openapi/security.d.ts +95 -0
  105. package/dist/types/components/openapi/snippets.d.ts +60 -0
  106. package/dist/types/consent/ethyca.d.ts +48 -0
  107. package/dist/types/consent/index.d.ts +37 -0
  108. package/dist/types/consent/native.d.ts +28 -0
  109. package/dist/types/consent/osano.d.ts +42 -0
  110. package/dist/types/consent/schema.d.ts +110 -0
  111. package/dist/types/core/changelog-index.d.ts +8 -0
  112. package/dist/types/core/config-input.d.ts +217 -23
  113. package/dist/types/core/data.d.ts +53 -4
  114. package/dist/types/core/footer.d.ts +8 -0
  115. package/dist/types/core/i18n-ui.d.ts +88 -0
  116. package/dist/types/core/includes.d.ts +8 -2
  117. package/dist/types/core/links.d.ts +8 -0
  118. package/dist/types/core/localizable.d.ts +7 -0
  119. package/dist/types/core/page-modes.d.ts +29 -0
  120. package/dist/types/core/probe.d.ts +2 -0
  121. package/dist/types/core/project-graph.d.ts +6 -0
  122. package/dist/types/core/redirect-patterns.d.ts +152 -0
  123. package/dist/types/core/related.d.ts +40 -0
  124. package/dist/types/core/schema.d.ts +670 -56
  125. package/dist/types/core/sources/cache.d.ts +3 -0
  126. package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
  127. package/dist/types/core/sources/contentful.d.ts +17 -1
  128. package/dist/types/core/sources/lexical.d.ts +5 -0
  129. package/dist/types/core/sources/normalize.d.ts +20 -1
  130. package/dist/types/core/sources/read.d.ts +5 -0
  131. package/dist/types/core/sources/remote.d.ts +37 -10
  132. package/dist/types/core/sources/sanity.d.ts +1 -1
  133. package/dist/types/core/types.d.ts +18 -1
  134. package/dist/types/core/variables.d.ts +42 -0
  135. package/dist/types/deploy/adapter-output.d.ts +2 -1
  136. package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
  137. package/dist/types/deploy/adapters/node.d.ts +58 -23
  138. package/dist/types/deploy/adapters/registry.d.ts +10 -0
  139. package/dist/types/deploy/adapters/types.d.ts +3 -2
  140. package/dist/types/deploy/node-headers.d.ts +13 -2
  141. package/dist/types/deploy/platforms/cloudflare.d.ts +15 -7
  142. package/dist/types/deploy/platforms/netlify.d.ts +7 -0
  143. package/dist/types/deploy/platforms/node.d.ts +3 -1
  144. package/dist/types/deploy/platforms/types.d.ts +18 -7
  145. package/dist/types/deploy/platforms/vercel.d.ts +26 -1
  146. package/dist/types/deploy/redirects.d.ts +55 -11
  147. package/dist/types/deploy/sitemap.d.ts +2 -1
  148. package/dist/types/deploy/vercel-negotiation.d.ts +51 -21
  149. package/dist/types/markdown/code-title.d.ts +17 -0
  150. package/dist/types/markdown/features.d.ts +3 -2
  151. package/dist/types/narration/build.d.ts +70 -0
  152. package/dist/types/narration/provider.d.ts +26 -0
  153. package/dist/types/narration/script.d.ts +154 -0
  154. package/dist/types/openapi/checks.d.ts +27 -0
  155. package/dist/types/openapi/graphql.d.ts +6 -0
  156. package/dist/types/openapi/model.d.ts +27 -10
  157. package/dist/types/openapi/overlay.d.ts +54 -0
  158. package/dist/types/openapi/parse.d.ts +16 -0
  159. package/dist/types/openapi/references.d.ts +30 -4
  160. package/dist/types/ratelimit/cloudflare.d.ts +39 -0
  161. package/dist/types/ratelimit/index.d.ts +27 -0
  162. package/dist/types/ratelimit/memory.d.ts +36 -0
  163. package/dist/types/ratelimit/schema.d.ts +119 -0
  164. package/dist/types/ratelimit/upstash.d.ts +46 -0
  165. package/dist/types/ratelimit/wrangler.d.ts +23 -0
  166. package/dist/types/reference/asyncapi.d.ts +10 -10
  167. package/dist/types/reference/graphql.d.ts +89 -11
  168. package/dist/types/reference/index.d.ts +1 -1
  169. package/dist/types/reference/openapi.d.ts +33 -12
  170. package/dist/types/reference/options.d.ts +82 -6
  171. package/dist/types/reference/scalar.d.ts +13 -0
  172. package/dist/types/reference/schema.d.ts +144 -36
  173. package/dist/types/search/adapters/registry.d.ts +3 -0
  174. package/dist/types/search/adapters/typesense.d.ts +14 -4
  175. package/dist/types/search/build.d.ts +3 -1
  176. package/dist/types/search/documents.d.ts +12 -1
  177. package/dist/types/search/orama-index.d.ts +13 -9
  178. package/dist/types/search/sync/algolia.d.ts +5 -2
  179. package/dist/types/search/sync/index.d.ts +3 -1
  180. package/dist/types/search/sync/typesense.d.ts +11 -7
  181. package/dist/types/seo/metatags.d.ts +20 -0
  182. package/dist/types/sources/contentful.d.ts +16 -0
  183. package/dist/types/sources/github-releases.d.ts +8 -1
  184. package/dist/types/sources/payload.d.ts +2 -0
  185. package/dist/types/sources/registry.d.ts +54 -0
  186. package/dist/types/sources/shared.d.ts +3 -0
  187. package/dist/types/sources/strapi.d.ts +2 -0
  188. package/docs/01-quickstart.mdx +4 -2
  189. package/docs/02-deployment.mdx +71 -17
  190. package/docs/03-upgrading.mdx +4 -4
  191. package/docs/04-migrating.mdx +1 -1
  192. package/docs/advanced/changelog.mdx +13 -2
  193. package/docs/advanced/skills.mdx +15 -1
  194. package/docs/cli/audit.mdx +51 -8
  195. package/docs/cli/doctor.mdx +5 -1
  196. package/docs/cli/evals.mdx +4 -4
  197. package/docs/cli/index.mdx +1 -0
  198. package/docs/cli/translate.mdx +1 -0
  199. package/docs/cli/validate.mdx +4 -3
  200. package/docs/configuration/analytics.mdx +3 -1
  201. package/docs/configuration/assistant.mdx +194 -38
  202. package/docs/configuration/consent.mdx +100 -0
  203. package/docs/configuration/customization.mdx +8 -6
  204. package/docs/configuration/export.mdx +5 -3
  205. package/docs/configuration/index.mdx +64 -5
  206. package/docs/configuration/meta.ts +3 -0
  207. package/docs/configuration/narration.mdx +119 -0
  208. package/docs/configuration/rate-limiting.mdx +90 -0
  209. package/docs/configuration/search.mdx +63 -12
  210. package/docs/configuration/theming.mdx +3 -1
  211. package/docs/content/components.mdx +108 -2
  212. package/docs/content/frontmatter.mdx +80 -2
  213. package/docs/content/i18n.mdx +39 -4
  214. package/docs/content/includes.mdx +16 -0
  215. package/docs/content/meta.mdx +1 -0
  216. package/docs/content/meta.ts +1 -0
  217. package/docs/content/navigation.mdx +32 -10
  218. package/docs/content/sources/contentful.mdx +35 -0
  219. package/docs/content/sources/custom.mdx +43 -0
  220. package/docs/content/sources/github-releases.mdx +39 -0
  221. package/docs/content/sources/index.mdx +91 -0
  222. package/docs/content/sources/meta.ts +16 -0
  223. package/docs/content/sources/notion.mdx +32 -0
  224. package/docs/content/sources/obsidian.mdx +37 -0
  225. package/docs/content/sources/payload.mdx +31 -0
  226. package/docs/content/sources/remote-mdx.mdx +44 -0
  227. package/docs/content/sources/sanity.mdx +29 -0
  228. package/docs/content/sources/strapi.mdx +32 -0
  229. package/docs/content/syntax.mdx +69 -4
  230. package/docs/content/variables.mdx +41 -0
  231. package/docs/discoverability/agent-discovery.mdx +29 -2
  232. package/docs/discoverability/index.mdx +2 -2
  233. package/docs/discoverability/json-api.mdx +2 -0
  234. package/docs/discoverability/llms-txt.mdx +2 -2
  235. package/docs/discoverability/markdown.mdx +3 -1
  236. package/docs/discoverability/mcp.mdx +4 -0
  237. package/docs/discoverability/metadata.mdx +17 -0
  238. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  239. package/docs/references/api-pages.mdx +74 -0
  240. package/docs/references/asyncapi.mdx +4 -2
  241. package/docs/references/graphql.mdx +14 -2
  242. package/docs/references/meta.ts +1 -1
  243. package/docs/references/openapi.mdx +148 -7
  244. package/docs/references/scalar.mdx +3 -1
  245. package/package.json +43 -8
  246. package/skills/blume/SKILL.md +3 -0
  247. package/skills/blume-migrate/SKILL.md +6 -6
  248. package/skills/blume-migrate/references/docusaurus.md +7 -7
  249. package/skills/blume-migrate/references/fumadocs.md +17 -13
  250. package/skills/blume-migrate/references/mintlify.md +37 -19
  251. package/skills/blume-migrate/references/nextra.md +10 -7
  252. package/skills/blume-migrate/references/starlight.md +11 -8
  253. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
  254. package/skills/blume-write-skill/SKILL.md +82 -0
  255. package/src/ai/agent-readability.ts +2 -1
  256. package/src/ai/agent-surface.ts +9 -7
  257. package/src/ai/ai-catalog.ts +6 -3
  258. package/src/ai/ask-context.ts +103 -20
  259. package/src/ai/ask-data.ts +6 -0
  260. package/src/ai/ask-limits.ts +13 -0
  261. package/src/ai/ask-tools.ts +142 -0
  262. package/src/ai/ask.ts +354 -76
  263. package/src/ai/changelog-markdown.ts +11 -6
  264. package/src/ai/component-markdown.ts +76 -1
  265. package/src/ai/cors.ts +14 -3
  266. package/src/ai/index.ts +12 -0
  267. package/src/ai/llms.ts +6 -2
  268. package/src/ai/markdown.ts +2 -1
  269. package/src/ai/mcp/data.ts +6 -0
  270. package/src/ai/mcp/query.ts +14 -6
  271. package/src/ai/openapi-components.ts +36 -6
  272. package/src/ai/relative-links.ts +4 -38
  273. package/src/ai/site-skill.ts +274 -0
  274. package/src/ai/skills.ts +35 -2
  275. package/src/ai/write-skill.ts +97 -0
  276. package/src/astro/generate.ts +210 -66
  277. package/src/astro/index.ts +1 -0
  278. package/src/astro/integration.ts +42 -4
  279. package/src/astro/markdown-negotiation.ts +3 -0
  280. package/src/astro/module-types.ts +9 -1
  281. package/src/astro/pages.ts +23 -0
  282. package/src/astro/templates.ts +406 -108
  283. package/src/astro/variables.ts +31 -0
  284. package/src/audit/catalog.ts +38 -3
  285. package/src/audit/checks/contrast.ts +183 -0
  286. package/src/audit/checks/i18n.ts +2 -3
  287. package/src/audit/checks/indexability.ts +2 -3
  288. package/src/audit/checks/links.ts +19 -13
  289. package/src/audit/checks/network.ts +110 -6
  290. package/src/audit/checks/redirects.ts +6 -4
  291. package/src/audit/checks/robots.ts +58 -2
  292. package/src/audit/checks/sitemap.ts +2 -3
  293. package/src/audit/redirects.ts +46 -15
  294. package/src/audit/run.ts +5 -0
  295. package/src/audit/types.ts +4 -1
  296. package/src/audit/url.ts +14 -5
  297. package/src/blume-modules.d.ts +12 -1
  298. package/src/captcha/client.ts +152 -0
  299. package/src/captcha/hcaptcha.ts +36 -0
  300. package/src/captcha/index.ts +28 -0
  301. package/src/captcha/schema.ts +32 -0
  302. package/src/captcha/turnstile.ts +38 -0
  303. package/src/captcha/verify.ts +71 -0
  304. package/src/cli/args.ts +51 -0
  305. package/src/cli/build-failure.ts +23 -0
  306. package/src/cli/command-meta.ts +5 -0
  307. package/src/cli/commands/audit.ts +10 -1
  308. package/src/cli/commands/dev.ts +18 -2
  309. package/src/cli/commands/doctor.ts +67 -28
  310. package/src/cli/commands/eval.ts +36 -4
  311. package/src/cli/commands/init.ts +6 -4
  312. package/src/cli/commands/skill.ts +96 -0
  313. package/src/cli/commands/validate.ts +10 -1
  314. package/src/cli/index.ts +5 -0
  315. package/src/cli/init/scaffold.ts +3 -3
  316. package/src/cli/prepare.ts +6 -3
  317. package/src/cli/required-secrets.ts +72 -30
  318. package/src/components/BlumePage.astro +2 -0
  319. package/src/components/content/ApiEndpoint.astro +18 -0
  320. package/src/components/content/ApiField.astro +79 -0
  321. package/src/components/content/ApiPlayground.astro +66 -0
  322. package/src/components/content/ApiRail.astro +19 -0
  323. package/src/components/content/Color.astro +1 -0
  324. package/src/components/content/FileTree.astro +1 -0
  325. package/src/components/content/GithubInfo.astro +1 -0
  326. package/src/components/content/Panel.astro +1 -0
  327. package/src/components/content/ParamField.astro +25 -0
  328. package/src/components/content/RequestExample.astro +17 -0
  329. package/src/components/content/ResponseExample.astro +17 -0
  330. package/src/components/content/ResponseField.astro +32 -0
  331. package/src/components/content/Step.astro +1 -0
  332. package/src/components/content/Tile.astro +1 -0
  333. package/src/components/content/Tooltip.astro +1 -0
  334. package/src/components/content/Tree.astro +1 -0
  335. package/src/components/content/TypeTable.astro +4 -1
  336. package/src/components/content/View.astro +21 -0
  337. package/src/components/content/ViewSwitcher.astro +168 -0
  338. package/src/components/content/api-field.ts +103 -0
  339. package/src/components/content/api-page.ts +248 -0
  340. package/src/components/content/mermaid-element.ts +15 -1
  341. package/src/components/content/view-switcher.ts +71 -0
  342. package/src/components/index.ts +2 -0
  343. package/src/components/islands/Assistant.astro +10 -1
  344. package/src/components/islands/assistant.tsx +104 -14
  345. package/src/components/islands/code-question.ts +62 -0
  346. package/src/components/islands/hooks.ts +136 -21
  347. package/src/components/islands/support-link.ts +68 -0
  348. package/src/components/layout/Analytics.astro +11 -2
  349. package/src/components/layout/Banner.astro +14 -10
  350. package/src/components/layout/Breadcrumbs.astro +6 -2
  351. package/src/components/layout/ConsentBanner.astro +62 -0
  352. package/src/components/layout/ConsentHead.astro +38 -0
  353. package/src/components/layout/Directory.astro +94 -0
  354. package/src/components/layout/DrawerSwitchers.astro +51 -0
  355. package/src/components/layout/Empty.astro +2 -2
  356. package/src/components/layout/Header.astro +122 -80
  357. package/src/components/layout/LanguageSwitcher.astro +48 -15
  358. package/src/components/layout/NarrationPlayer.astro +1085 -0
  359. package/src/components/layout/NavSelector.astro +33 -7
  360. package/src/components/layout/PageActions.astro +139 -24
  361. package/src/components/layout/PageFeedback.astro +54 -49
  362. package/src/components/layout/PageLayout.astro +71 -3
  363. package/src/components/layout/ReferenceLayout.astro +17 -6
  364. package/src/components/layout/RelatedPages.astro +42 -0
  365. package/src/components/layout/RootLayout.astro +318 -47
  366. package/src/components/layout/Search.astro +47 -19
  367. package/src/components/layout/SiteFooter.astro +124 -0
  368. package/src/components/layout/SiteMetatags.astro +16 -0
  369. package/src/components/layout/TableOfContents.astro +24 -17
  370. package/src/components/layout/consent/native.ts +79 -0
  371. package/src/components/layout/consent/types.ts +37 -0
  372. package/src/components/layout/footer-links.ts +53 -0
  373. package/src/components/layout/footer-socials.ts +87 -0
  374. package/src/components/layout/head-scripts.ts +6 -1
  375. package/src/components/layout/locale-redirect.ts +107 -0
  376. package/src/components/layout/nav-utils.ts +83 -0
  377. package/src/components/layout/page-feedback.ts +96 -0
  378. package/src/components/layout/search/algolia.ts +10 -1
  379. package/src/components/layout/search/analytics.ts +126 -0
  380. package/src/components/layout/search/endpoint.ts +48 -5
  381. package/src/components/layout/search/flexsearch.ts +17 -5
  382. package/src/components/layout/search/orama-cloud.ts +26 -14
  383. package/src/components/layout/search/pagefind.ts +114 -5
  384. package/src/components/layout/search/types.ts +32 -0
  385. package/src/components/layout/search/typesense.ts +19 -3
  386. package/src/components/openapi/AsyncApiOperation.astro +14 -7
  387. package/src/components/openapi/Authorization.astro +10 -0
  388. package/src/components/openapi/GraphqlOperation.astro +5 -0
  389. package/src/components/openapi/Operation.astro +184 -13
  390. package/src/components/openapi/ParametersTable.astro +8 -4
  391. package/src/components/openapi/Playground.astro +7 -1
  392. package/src/components/openapi/RequestBody.astro +24 -5
  393. package/src/components/openapi/RequestPanel.astro +59 -7
  394. package/src/components/openapi/Responses.astro +7 -4
  395. package/src/components/openapi/SchemaTable.astro +15 -3
  396. package/src/components/openapi/async-snippets.ts +65 -9
  397. package/src/components/openapi/async.ts +123 -29
  398. package/src/components/openapi/code-samples.ts +97 -0
  399. package/src/components/openapi/graphql-helpers.ts +35 -12
  400. package/src/components/openapi/helpers.ts +36 -2
  401. package/src/components/openapi/message-model.ts +54 -11
  402. package/src/components/openapi/message.ts +14 -4
  403. package/src/components/openapi/operation-model.ts +16 -7
  404. package/src/components/openapi/playground-client.ts +24 -5
  405. package/src/components/openapi/playground-schema.ts +1 -1
  406. package/src/components/openapi/schema-tree.ts +44 -14
  407. package/src/components/openapi/security.ts +16 -3
  408. package/src/components/openapi/snippets.ts +873 -10
  409. package/src/components/openapi/validate-json.ts +4 -3
  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/builtin-tags.ts +5 -0
  421. package/src/core/changelog-index.ts +31 -1
  422. package/src/core/client-address.ts +19 -0
  423. package/src/core/component-diagnostics.ts +53 -0
  424. package/src/core/component-overrides.ts +32 -0
  425. package/src/core/config-input.ts +225 -23
  426. package/src/core/data.ts +46 -4
  427. package/src/core/diagnostics.ts +8 -2
  428. package/src/core/footer.ts +26 -0
  429. package/src/core/graph.ts +39 -36
  430. package/src/core/i18n-ui.ts +84 -2
  431. package/src/core/includes.ts +21 -4
  432. package/src/core/links.ts +63 -19
  433. package/src/core/localizable.ts +26 -0
  434. package/src/core/navigation.ts +43 -12
  435. package/src/core/page-modes.ts +56 -0
  436. package/src/core/probe.ts +26 -6
  437. package/src/core/project-graph.ts +163 -16
  438. package/src/core/redirect-patterns.ts +520 -0
  439. package/src/core/related.ts +131 -0
  440. package/src/core/schema.ts +339 -66
  441. package/src/core/sources/cache.ts +6 -0
  442. package/src/core/sources/contentful-rich-text.ts +29 -6
  443. package/src/core/sources/contentful.ts +135 -25
  444. package/src/core/sources/filesystem.ts +26 -4
  445. package/src/core/sources/github-releases.ts +12 -1
  446. package/src/core/sources/lexical.ts +15 -10
  447. package/src/core/sources/mdx-remote.ts +62 -10
  448. package/src/core/sources/normalize.ts +228 -41
  449. package/src/core/sources/notion.ts +7 -1
  450. package/src/core/sources/obsidian.ts +16 -3
  451. package/src/core/sources/payload.ts +38 -15
  452. package/src/core/sources/read.ts +8 -3
  453. package/src/core/sources/remote.ts +72 -15
  454. package/src/core/sources/resolve.ts +11 -1
  455. package/src/core/sources/sanity.ts +43 -25
  456. package/src/core/sources/strapi.ts +2 -2
  457. package/src/core/types.ts +19 -1
  458. package/src/core/ui-packs/ar.ts +44 -0
  459. package/src/core/ui-packs/bg.ts +46 -0
  460. package/src/core/ui-packs/bn.ts +44 -0
  461. package/src/core/ui-packs/ca.ts +47 -0
  462. package/src/core/ui-packs/cs.ts +45 -0
  463. package/src/core/ui-packs/da.ts +46 -0
  464. package/src/core/ui-packs/de.ts +47 -0
  465. package/src/core/ui-packs/el.ts +46 -0
  466. package/src/core/ui-packs/es.ts +47 -0
  467. package/src/core/ui-packs/fa.ts +46 -0
  468. package/src/core/ui-packs/fi.ts +46 -0
  469. package/src/core/ui-packs/fr.ts +47 -0
  470. package/src/core/ui-packs/he.ts +44 -0
  471. package/src/core/ui-packs/hi.ts +45 -0
  472. package/src/core/ui-packs/hr.ts +46 -0
  473. package/src/core/ui-packs/hu.ts +45 -0
  474. package/src/core/ui-packs/id.ts +46 -0
  475. package/src/core/ui-packs/it.ts +46 -0
  476. package/src/core/ui-packs/ja.ts +46 -0
  477. package/src/core/ui-packs/ko.ts +45 -0
  478. package/src/core/ui-packs/nl.ts +47 -0
  479. package/src/core/ui-packs/no.ts +45 -0
  480. package/src/core/ui-packs/pl.ts +46 -0
  481. package/src/core/ui-packs/pt-br.ts +47 -0
  482. package/src/core/ui-packs/pt.ts +46 -0
  483. package/src/core/ui-packs/ro.ts +46 -0
  484. package/src/core/ui-packs/ru.ts +46 -0
  485. package/src/core/ui-packs/sk.ts +45 -0
  486. package/src/core/ui-packs/sr.ts +46 -0
  487. package/src/core/ui-packs/sv.ts +45 -0
  488. package/src/core/ui-packs/th.ts +44 -0
  489. package/src/core/ui-packs/tr.ts +46 -0
  490. package/src/core/ui-packs/uk.ts +46 -0
  491. package/src/core/ui-packs/vi.ts +46 -0
  492. package/src/core/ui-packs/zh-tw.ts +44 -0
  493. package/src/core/ui-packs/zh.ts +44 -0
  494. package/src/core/variables.ts +103 -0
  495. package/src/deploy/adapter-output.ts +7 -2
  496. package/src/deploy/adapters/cloudflare.ts +7 -5
  497. package/src/deploy/adapters/node.ts +47 -9
  498. package/src/deploy/adapters/types.ts +7 -3
  499. package/src/deploy/artifacts.ts +79 -9
  500. package/src/deploy/cloudflare-negotiation.ts +35 -5
  501. package/src/deploy/headers.ts +2 -1
  502. package/src/deploy/node-headers.ts +63 -9
  503. package/src/deploy/platforms/cloudflare.ts +45 -9
  504. package/src/deploy/platforms/netlify.ts +35 -2
  505. package/src/deploy/platforms/node.ts +12 -2
  506. package/src/deploy/platforms/types.ts +21 -7
  507. package/src/deploy/platforms/vercel.ts +149 -17
  508. package/src/deploy/redirects.ts +151 -24
  509. package/src/deploy/rss.ts +3 -1
  510. package/src/deploy/sitemap.ts +39 -5
  511. package/src/deploy/vercel-negotiation.ts +326 -102
  512. package/src/eval/agents.ts +18 -1
  513. package/src/eval/prompts.ts +6 -3
  514. package/src/eval/report.ts +34 -13
  515. package/src/eval/run.ts +5 -0
  516. package/src/markdown/api-rail.ts +259 -0
  517. package/src/markdown/code-title.ts +35 -9
  518. package/src/markdown/features.ts +3 -2
  519. package/src/markdown/fence-meta.ts +2 -0
  520. package/src/markdown/include.ts +11 -1
  521. package/src/markdown/index.ts +28 -5
  522. package/src/markdown/math.ts +4 -4
  523. package/src/markdown/relative-links.ts +3 -2
  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 +7 -6
  531. package/src/openapi/checks.ts +273 -0
  532. package/src/openapi/graphql-build.ts +21 -0
  533. package/src/openapi/graphql.ts +11 -0
  534. package/src/openapi/model.ts +72 -27
  535. package/src/openapi/overlay.ts +256 -0
  536. package/src/openapi/parse.ts +182 -4
  537. package/src/openapi/references.ts +64 -8
  538. package/src/openapi/render-mdx.ts +5 -1
  539. package/src/openapi/scalar.ts +40 -6
  540. package/src/openapi/source.ts +40 -15
  541. package/src/ratelimit/cloudflare.ts +54 -0
  542. package/src/ratelimit/index.ts +30 -0
  543. package/src/ratelimit/memory.ts +45 -0
  544. package/src/ratelimit/runtime.ts +247 -0
  545. package/src/ratelimit/schema.ts +28 -0
  546. package/src/ratelimit/upstash.ts +60 -0
  547. package/src/ratelimit/wrangler.ts +70 -0
  548. package/src/reference/asyncapi.ts +2 -2
  549. package/src/reference/graphql.ts +20 -3
  550. package/src/reference/index.ts +1 -0
  551. package/src/reference/openapi.ts +21 -7
  552. package/src/reference/options.ts +80 -8
  553. package/src/reference/scalar.ts +33 -8
  554. package/src/registry/eject.ts +23 -5
  555. package/src/registry/registry.ts +7 -0
  556. package/src/search/adapters/locale-scope.ts +30 -0
  557. package/src/search/adapters/typesense.ts +14 -4
  558. package/src/search/build.ts +21 -3
  559. package/src/search/documents.ts +26 -2
  560. package/src/search/orama-index.ts +191 -34
  561. package/src/search/pagefind-ranking.ts +39 -0
  562. package/src/search/source-pages.ts +29 -0
  563. package/src/search/sync/algolia.ts +111 -10
  564. package/src/search/sync/index.ts +52 -13
  565. package/src/search/sync/orama-cloud.ts +3 -0
  566. package/src/search/sync/typesense.ts +76 -26
  567. package/src/seo/metatags.ts +67 -0
  568. package/src/sources/contentful.ts +12 -0
  569. package/src/sources/github-releases.ts +7 -1
  570. package/src/sources/shared.ts +3 -0
  571. package/src/theme/chrome-icons.ts +1 -0
  572. package/src/theme/color.ts +500 -0
  573. package/src/theme/entry.ts +85 -6
  574. package/src/theme/palette.ts +121 -45
  575. package/src/translate/prompts.ts +1 -0
  576. package/src/translate/validate.ts +31 -3
  577. package/src/upgrade/upgrade.ts +4 -171
  578. package/dist/cli/chunk-1d7ve1dm.js.map +0 -12
  579. package/dist/cli/chunk-2hsdwb9n.js.map +0 -10
  580. package/dist/cli/chunk-35d4wj9f.js.map +0 -15
  581. package/dist/cli/chunk-364znk6q.js +0 -75
  582. package/dist/cli/chunk-364znk6q.js.map +0 -11
  583. package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
  584. package/dist/cli/chunk-8ktnccpt.js.map +0 -10
  585. package/dist/cli/chunk-9t7a85s3.js.map +0 -10
  586. package/dist/cli/chunk-b07cmahc.js +0 -145
  587. package/dist/cli/chunk-b07cmahc.js.map +0 -11
  588. package/dist/cli/chunk-c8chx29p.js.map +0 -36
  589. package/dist/cli/chunk-e04dxsz1.js +0 -39
  590. package/dist/cli/chunk-e04dxsz1.js.map +0 -10
  591. package/dist/cli/chunk-j85vccga.js.map +0 -36
  592. package/dist/cli/chunk-p3v96n38.js.map +0 -10
  593. package/dist/cli/chunk-pehfxfta.js.map +0 -10
  594. package/dist/cli/chunk-pv29h0wf.js.map +0 -190
  595. package/dist/cli/chunk-q58y5e6a.js.map +0 -10
  596. package/dist/cli/chunk-vg9r4eb9.js.map +0 -14
  597. package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
  598. package/dist/cli/chunk-yfyb25rh.js.map +0 -14
  599. package/docs/content/sources.mdx +0 -334
  600. /package/dist/cli/{chunk-tkacnehg.js.map → chunk-3v3ffbrj.js.map} +0 -0
  601. /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-8y2kmjv8.js.map} +0 -0
  602. /package/dist/cli/{chunk-2eytanqx.js.map → chunk-c887fj0s.js.map} +0 -0
  603. /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-dzwehpy7.js.map} +0 -0
  604. /package/dist/cli/{chunk-2z928egk.js.map → chunk-ft9vfys6.js.map} +0 -0
  605. /package/dist/cli/{chunk-a58773jm.js.map → chunk-kma9f6sf.js.map} +0 -0
  606. /package/dist/cli/{chunk-crgn1q09.js.map → chunk-sds923fa.js.map} +0 -0
  607. /package/dist/cli/{chunk-acanzt5p.js.map → chunk-tdqhyxfz.js.map} +0 -0
@@ -4,6 +4,7 @@ import data from "blume:data";
4
4
  import { EN_UI } from "../../core/i18n-ui.ts";
5
5
  import type { UIStrings } from "../../core/i18n-ui.ts";
6
6
  import type { Navigation } from "../../core/types.ts";
7
+ import { scopesLocales } from "../../search/adapters/locale-scope.ts";
7
8
  import { scopesVersions } from "../../search/adapters/version-scope.ts";
8
9
  import { resolvePopularIconMarkup } from "../../search/popular-icon.ts";
9
10
  import Icon from "../Icon.astro";
@@ -36,9 +37,15 @@ const {
36
37
  navigation,
37
38
  popularPages,
38
39
  strings,
39
- locale,
40
+ locale: viewedLocale,
40
41
  version: viewedVersion = null,
41
42
  } = Astro.props;
43
+ // Mixedbread searches every language whatever the dialog asks, so on it an
44
+ // "All languages" toggle would change nothing: treat the site as
45
+ // single-language for search instead.
46
+ const locale = scopesLocales(data.config.search.provider)
47
+ ? viewedLocale
48
+ : undefined;
42
49
  // Pagefind, Orama Cloud, and Mixedbread search every version whatever the
43
50
  // dialog asks, so on those an "All versions" toggle would change nothing:
44
51
  // treat the site as unversioned for search instead.
@@ -90,26 +97,23 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
90
97
  data-i18n-popular={s.popular}
91
98
  data-i18n-results={s.results}
92
99
  data-locale={locale || undefined}
100
+ data-redact-queries={data.config.search.analytics.queries ? undefined : ""}
93
101
  data-version={version ?? undefined}
94
102
  data-versioned={version === null ? undefined : ""}
95
103
  >
96
- {/* The label and shortcut hint wait until `lg`: below it the hamburger and
97
- inline tab bar share the header row, and a full-width search field would
98
- press into the language switcher. */}
104
+ {/* An icon button like the theme toggle beside it, named with its shortcut
105
+ by the header's tooltip. The chord reads left to right in every locale
106
+ ("⌘K", never "K⌘"), and the script swaps in Ctrl off Apple devices. */}
99
107
  <button
108
+ aria-keyshortcuts="Meta+K Control+K"
100
109
  aria-label={s.button}
101
- class="inline-flex h-9 cursor-pointer items-center gap-2 rounded-full border border-border bg-background px-3 text-muted-foreground text-sm transition-colors hover:border-foreground hover:text-foreground lg:min-w-40"
110
+ class="inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
102
111
  data-blume-search-open
112
+ data-blume-tooltip={`${s.button} (\u2066⌘K\u2069)`}
113
+ data-label={s.button}
103
114
  type="button"
104
115
  >
105
- <Icon name="search" size={16} />
106
- <span class="flex-1 text-start max-lg:hidden">{s.button}</span>
107
- {/* A key chord reads left to right in every locale ("⌘K", never "K⌘"). */}
108
- <kbd
109
- class="font-mono text-[0.7rem] max-lg:hidden"
110
- data-blume-search-kbd
111
- dir="ltr">⌘K</kbd
112
- >
116
+ <Icon name="search" size={18} />
113
117
  </button>
114
118
 
115
119
  <dialog
@@ -242,6 +246,9 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
242
246
  import { mountBase } from "../islands/base-path.ts";
243
247
  import { escape as escapeHtml } from "html-escaper";
244
248
 
249
+ import { track } from "./analytics-client.ts";
250
+ import { createSearchTracker } from "./search/analytics.ts";
251
+ import type { SearchTracker } from "./search/analytics.ts";
245
252
  import { highlight, matchSnippet } from "./search/types.ts";
246
253
  import type { SearchFn, SearchHit } from "./search/types.ts";
247
254
 
@@ -308,7 +315,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
308
315
  searchClient ??= (async () => {
309
316
  try {
310
317
  const { createSearch } = await import("blume:search-client");
311
- return await createSearch();
318
+ // The reader types each query, so each supersedes the last.
319
+ return await createSearch({ typing: true });
312
320
  } catch (error) {
313
321
  searchClient = undefined;
314
322
  throw error;
@@ -326,6 +334,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
326
334
  message!: HTMLElement;
327
335
  preview!: HTMLElement;
328
336
  searchFn: SearchFn | null = null;
337
+ /** Records settled queries and picked results (search/analytics.ts). */
338
+ tracker!: SearchTracker;
329
339
  loaded = false;
330
340
  loadFailed = false;
331
341
  /** First-open client/index load still in flight. */
@@ -419,6 +429,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
419
429
  this.message = this.querySelector("[data-blume-search-message]")!;
420
430
  this.preview = this.querySelector("[data-blume-search-preview]")!;
421
431
  this.assistantEnabled = this.hasAttribute("data-ask");
432
+ // `search.analytics.queries: false` keeps query text off providers.
433
+ this.tracker = createSearchTracker(track, {
434
+ queries: !this.hasAttribute("data-redact-queries"),
435
+ });
422
436
 
423
437
  const popularScript = this.querySelector(
424
438
  "[data-blume-search-popular]"
@@ -470,11 +484,13 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
470
484
  }
471
485
 
472
486
  // The handlers accept both ⌘ and Ctrl chords; show the right modifier
473
- // per platform on the button hint and the footer's preview hint.
487
+ // per platform in the button's tooltip and the footer's preview hint.
474
488
  const isApple = /mac|iphone|ipad|ipod/iu.test(navigator.platform);
475
- const hint = this.querySelector("[data-blume-search-kbd]");
476
- if (hint) {
477
- hint.textContent = isApple ? "⌘K" : "Ctrl K";
489
+ const trigger = this.querySelector<HTMLElement>(
490
+ "[data-blume-search-open]"
491
+ );
492
+ if (trigger && !isApple) {
493
+ trigger.dataset.blumeTooltip = `${trigger.dataset.label ?? ""} (\u2066Ctrl K\u2069)`;
478
494
  }
479
495
  const previewHint = this.querySelector(
480
496
  "[data-blume-search-preview-kbd]"
@@ -494,7 +510,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
494
510
  this.dialog.addEventListener("keydown", (event) =>
495
511
  this.onKeydown(event)
496
512
  );
497
- this.dialog.addEventListener("close", () => this.unlockPageScroll());
513
+ this.dialog.addEventListener("close", () => {
514
+ this.unlockPageScroll();
515
+ this.tracker.closed();
516
+ });
498
517
  this.dialog.addEventListener("click", (event) => {
499
518
  if (event.target === this.dialog) {
500
519
  this.dialog.close();
@@ -678,6 +697,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
678
697
  this.setMessage(this.noResultsMsg);
679
698
  }
680
699
 
700
+ this.tracker.settled(query, result.hits.length);
681
701
  this.finishRender();
682
702
  }
683
703
 
@@ -901,6 +921,14 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
901
921
  new CustomEvent("blume:open-assistant", { detail: { query } })
902
922
  );
903
923
  } else if (item.url) {
924
+ if (item.hit) {
925
+ const hits = this.selectables.filter((row) => row.hit);
926
+ this.tracker.selected(
927
+ this.input.value.trim(),
928
+ hits.indexOf(item) + 1,
929
+ item.url
930
+ );
931
+ }
904
932
  // Close first so the dialog isn't left open over the transition;
905
933
  // navigate() rides the client router (and falls back to a normal
906
934
  // full load on pages without it).
@@ -0,0 +1,124 @@
1
+ ---
2
+ // The site footer, below the content on every page: one row, as tall as the
3
+ // header, with `footer.links` on one side and the social profile icons on the
4
+ // other. The site's repository (`github`, unless `navigation.repo` hides it)
5
+ // is the first icon; the footer is the only place it's linked. With `consent` configured the links end with the
6
+ // Cookie settings link, which reopens the reader's consent choices. Renders
7
+ // nothing when there's none of these; a `components.ts` `Footer` replaces it.
8
+ import data from "blume:data";
9
+
10
+ import type { BlumeDataConfig } from "../../core/data.ts";
11
+ import type { Navigation } from "../../core/types.ts";
12
+ import { EN_UI } from "../../core/i18n-ui.ts";
13
+ import type { UIStrings } from "../../core/i18n-ui.ts";
14
+ import { routeSetFor } from "../../core/locale-links.ts";
15
+ import { newTabAttrs } from "../../core/new-tab.ts";
16
+ import Icon from "../Icon.astro";
17
+ import { withBase } from "../islands/base-path.ts";
18
+ import { footerLinks } from "./footer-links.ts";
19
+ import { footerSocialLinks } from "./footer-socials.ts";
20
+
21
+ interface Props {
22
+ footer?: BlumeDataConfig["footer"];
23
+ /**
24
+ * Active locale of the page: labels written as per-locale maps show its
25
+ * entry, and internal links move into it (see `footer-links.ts`).
26
+ */
27
+ locale?: string;
28
+ /** The page's navigation: its `repoUrl` is the repository link. */
29
+ navigation?: Navigation;
30
+ /** Localized UI strings; the Cookie settings label comes from `consent`. */
31
+ ui?: UIStrings;
32
+ }
33
+
34
+ const { footer, locale, navigation, ui } = Astro.props;
35
+ // `github`, as `navigation.repo` leaves it: hidden, or pointed elsewhere.
36
+ const repoUrl = navigation?.repoUrl;
37
+ const socials = footerSocialLinks(
38
+ footer?.socials ?? {},
39
+ repoUrl
40
+ ? { href: repoUrl, label: { ...EN_UI.nav, ...ui?.nav }.githubRepository }
41
+ : null
42
+ );
43
+ const links = footerLinks(footer?.links ?? [], {
44
+ basePath: data.config.basePath,
45
+ i18n: data.config.i18n,
46
+ locale,
47
+ routes: routeSetFor(data.routes),
48
+ });
49
+ const consentSettings = data.config.consent
50
+ ? { ...EN_UI.consent, ...ui?.consent }.settings
51
+ : null;
52
+ const linkClass =
53
+ "text-muted-foreground transition-colors hover:text-foreground focus-visible:text-foreground";
54
+ // Round icon buttons, like the header's.
55
+ const iconButton =
56
+ "inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:text-foreground";
57
+ const hasLinks = links.length > 0 || consentSettings !== null;
58
+ ---
59
+
60
+ {
61
+ (hasLinks || socials.length > 0) && (
62
+ <footer class="border-border border-t px-4 md:px-6" data-blume-footer>
63
+ <div class="flex min-h-16 flex-wrap items-center justify-between gap-x-6 gap-y-3 py-3">
64
+ {hasLinks && (
65
+ <ul class="flex flex-wrap items-center gap-x-5 gap-y-2 text-sm">
66
+ {links.map((link) => (
67
+ <li>
68
+ <a
69
+ class={linkClass}
70
+ href={withBase(link.href)}
71
+ {...newTabAttrs(link.href)}
72
+ >
73
+ {link.label}
74
+ </a>
75
+ </li>
76
+ ))}
77
+ {consentSettings && (
78
+ <li>
79
+ <button
80
+ class:list={[linkClass, "cursor-pointer"]}
81
+ data-blume-consent-open
82
+ type="button"
83
+ >
84
+ {consentSettings}
85
+ </button>
86
+ </li>
87
+ )}
88
+ </ul>
89
+ )}
90
+ {/* Pulled out by the buttons' padding, so the glyphs line up with the
91
+ footer's edge, and pushed to the far side even with no links. */}
92
+ {socials.length > 0 && (
93
+ <ul class="-me-[9px] -my-[9px] ms-auto flex flex-wrap items-center gap-1">
94
+ {socials.map((social) => (
95
+ <li>
96
+ <a
97
+ aria-label={social.label}
98
+ class={iconButton}
99
+ href={withBase(social.href)}
100
+ title={social.label}
101
+ {...newTabAttrs(social.href)}
102
+ >
103
+ {social.path ? (
104
+ <svg
105
+ aria-hidden="true"
106
+ fill="currentColor"
107
+ height="18"
108
+ viewBox="0 0 24 24"
109
+ width="18"
110
+ >
111
+ <path d={social.path} />
112
+ </svg>
113
+ ) : (
114
+ <Icon name={social.icon} size={18} />
115
+ )}
116
+ </a>
117
+ </li>
118
+ ))}
119
+ </ul>
120
+ )}
121
+ </div>
122
+ </footer>
123
+ )
124
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ import { metatagAttributes } from "../../seo/metatags.ts";
3
+
4
+ /**
5
+ * The site's own `seo.metatags` in a page's head, after the tags Blume
6
+ * writes (see seo/metatags.ts). Every layout renders it, so a verification
7
+ * token or `theme-color` reaches every page.
8
+ */
9
+ interface Props {
10
+ tags: Record<string, string> | null | undefined;
11
+ }
12
+
13
+ const { tags } = Astro.props;
14
+ ---
15
+
16
+ {metatagAttributes(tags).map((attributes) => <meta {...attributes} />)}
@@ -15,10 +15,14 @@ interface Props {
15
15
  }
16
16
 
17
17
  const { headings, title, variant } = Astro.props;
18
+ // The mobile dropdown also holds what the layout passes as children (the
19
+ // mount the page actions move into below `xl`, where the rail that renders
20
+ // them is hidden), so a page with actions but no headings still gets it.
21
+ const hasChildren = Astro.slots.has("default");
18
22
  ---
19
23
 
20
24
  {
21
- headings.length > 0 && variant === "mobile" && (
25
+ (headings.length > 0 || hasChildren) && variant === "mobile" && (
22
26
  <details class="group mx-auto mb-6 max-w-content rounded-blume border border-border xl:hidden">
23
27
  <summary class="flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 font-medium text-foreground text-sm [&::-webkit-details-marker]:hidden">
24
28
  <span>{title}</span>
@@ -28,23 +32,26 @@ const { headings, title, variant } = Astro.props;
28
32
  size={16}
29
33
  />
30
34
  </summary>
31
- <blume-toc class="block">
32
- <ul class="m-0 list-none border-border border-t p-2">
33
- {headings.map((heading) => (
34
- <li
35
- data-depth={heading.depth}
36
- style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}
37
- >
38
- <a
39
- class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
40
- href={`#${heading.slug}`}
35
+ {headings.length > 0 && (
36
+ <blume-toc class="block">
37
+ <ul class="m-0 list-none border-border border-t p-2">
38
+ {headings.map((heading) => (
39
+ <li
40
+ data-depth={heading.depth}
41
+ style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}
41
42
  >
42
- {heading.text}
43
- </a>
44
- </li>
45
- ))}
46
- </ul>
47
- </blume-toc>
43
+ <a
44
+ class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
45
+ href={`#${heading.slug}`}
46
+ >
47
+ {heading.text}
48
+ </a>
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ </blume-toc>
53
+ )}
54
+ <slot />
48
55
  </details>
49
56
  )
50
57
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `native()`'s browser module: Blume's own banner. The generated
3
+ * `blume:consent-client` brings it in when `native()` is the configured
4
+ * consent adapter (see `consent/clients.ts`). It reports the reader's stored
5
+ * answer, shows the banner (`ConsentBanner.astro`) until they pick one, stores
6
+ * the pick, and makes the footer's Cookie settings link bring the banner back.
7
+ */
8
+ import type { NativeOptions } from "../../../consent/native.ts";
9
+ import type { BlumeConsent, ConsentClientStart } from "./types.ts";
10
+
11
+ /** Where `native()` keeps the reader's answer. */
12
+ export const CONSENT_STORAGE_KEY = "blume-consent";
13
+
14
+ const BANNER = "[data-blume-consent-banner]";
15
+ const CHOICE = "[data-blume-consent-choice]";
16
+
17
+ /** The stored answer, or `null` before one (or without storage). */
18
+ export const storedChoice = (): boolean | null => {
19
+ try {
20
+ const value = localStorage.getItem(CONSENT_STORAGE_KEY);
21
+ if (value === "granted" || value === "denied") {
22
+ return value === "granted";
23
+ }
24
+ } catch {
25
+ // Storage blocked: ask again on every page rather than fail.
26
+ }
27
+ return null;
28
+ };
29
+
30
+ const storeChoice = (granted: boolean): void => {
31
+ try {
32
+ localStorage.setItem(CONSENT_STORAGE_KEY, granted ? "granted" : "denied");
33
+ } catch {
34
+ // Storage blocked: the answer holds for this page load only.
35
+ }
36
+ };
37
+
38
+ const banner = (): HTMLElement | null =>
39
+ document.querySelector<HTMLElement>(BANNER);
40
+
41
+ /**
42
+ * Wire this page's banner: show it while the reader hasn't answered, and
43
+ * store and report an answer. Runs again after every client-router swap,
44
+ * which brings a fresh, hidden banner.
45
+ */
46
+ const syncBanner = (consent: BlumeConsent): void => {
47
+ const element = banner();
48
+ if (!element) {
49
+ return;
50
+ }
51
+ element.hidden = storedChoice() !== null;
52
+ for (const button of element.querySelectorAll<HTMLElement>(CHOICE)) {
53
+ button.addEventListener("click", () => {
54
+ const granted = button.dataset.blumeConsentChoice === "accept";
55
+ storeChoice(granted);
56
+ element.hidden = true;
57
+ consent.set({ analytics: granted });
58
+ });
59
+ }
60
+ };
61
+
62
+ /**
63
+ * Report the stored answer, and wire the banner on this page and every page
64
+ * the client router swaps in. The options (the banner's privacy link) are
65
+ * rendered into the banner itself, so there's nothing to read here.
66
+ */
67
+ export const start: ConsentClientStart<NativeOptions> = (consent) => {
68
+ consent.open = () => {
69
+ const element = banner();
70
+ if (element) {
71
+ element.hidden = false;
72
+ }
73
+ };
74
+ syncBanner(consent);
75
+ consent.set({ analytics: storedChoice() === true });
76
+ document.addEventListener("astro:after-swap", () => {
77
+ syncBanner(consent);
78
+ });
79
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The consent contract every adapter talks to, shared by the consent runtime
3
+ * (`consent/client.ts`) and each adapter's browser module (like `native.ts`
4
+ * here). `window.blumeConsent` is created by the inline init script
5
+ * (`consent/init.ts`) before any adapter loads.
6
+ */
7
+
8
+ /** What a consent adapter reports: whether the reader allows analytics. */
9
+ export interface ConsentState {
10
+ analytics: boolean;
11
+ }
12
+
13
+ /** `window.blumeConsent`, created by the inline init script. */
14
+ export interface BlumeConsent {
15
+ /** `null` until an adapter reports, then the reader's choice. */
16
+ analytics: boolean | null;
17
+ /** The configured adapter's kind. */
18
+ kind: string;
19
+ /** Reopen the reader's consent choices; set by the adapter. */
20
+ open?: () => void;
21
+ /** Report the reader's choice; fires `blume:consent` when it changes. */
22
+ set: (state: ConsentState) => void;
23
+ }
24
+
25
+ /**
26
+ * A consent adapter's browser module entry point, which the generated
27
+ * `blume:consent-client` calls once per real page load, after the runtime is
28
+ * listening. It gets the consent state and the adapter's options from
29
+ * `blume.config.ts`, and must report the reader's answer with
30
+ * `consent.set({ analytics })` (on load, and again whenever it changes) and
31
+ * set `consent.open` to reopen the adapter's preferences. Anything it does per
32
+ * page, it redoes on `astro:after-swap`.
33
+ */
34
+ export type ConsentClientStart<Options> = (
35
+ consent: BlumeConsent,
36
+ options: Options
37
+ ) => void;
@@ -0,0 +1,53 @@
1
+ import type { LocaleRouting } from "../../core/i18n.ts";
2
+ import { localizeHref } from "../../core/locale-links.ts";
3
+ import type { RouteSet } from "../../core/locale-links.ts";
4
+ import { resolveLocalizable } from "../../core/localizable.ts";
5
+ import type { ResolvedConfig } from "../../core/schema.ts";
6
+
7
+ /** The configured `footer.links`. */
8
+ type FooterLinks = NonNullable<ResolvedConfig["footer"]>["links"];
9
+
10
+ /** A footer link, ready to render. */
11
+ export interface FooterLinkItem {
12
+ href: string;
13
+ label: string;
14
+ }
15
+
16
+ /** What resolving needs from the site and the page. */
17
+ export interface FooterContext {
18
+ basePath: string;
19
+ /** The site's locales, when it has more than one. */
20
+ i18n: LocaleRouting | null;
21
+ /** The locale of the page being rendered. */
22
+ locale?: string;
23
+ routes: RouteSet;
24
+ }
25
+
26
+ /**
27
+ * The footer's links in the page's locale: a label written as a per-locale
28
+ * map shows that locale's entry (see `core/localizable.ts`), and an internal
29
+ * link moves into the locale when it serves that page, the way content and
30
+ * header links do, so a reader on `/fr/…` stays in French.
31
+ */
32
+ export const footerLinks = (
33
+ links: FooterLinks,
34
+ context: FooterContext
35
+ ): FooterLinkItem[] => {
36
+ const { i18n, locale } = context;
37
+ const defaultLocale = i18n?.defaultLocale;
38
+ const localize = (href: string): string =>
39
+ i18n && locale
40
+ ? localizeHref(href, {
41
+ basePath: context.basePath,
42
+ // The layout mounts the deployment base afterwards.
43
+ deployBase: "",
44
+ i18n,
45
+ locale,
46
+ routes: context.routes,
47
+ })
48
+ : href;
49
+ return links.map((link) => ({
50
+ href: localize(link.href),
51
+ label: resolveLocalizable(link.label, locale, defaultLocale),
52
+ }));
53
+ };
@@ -0,0 +1,87 @@
1
+ import {
2
+ siBluesky,
3
+ siDiscord,
4
+ siFacebook,
5
+ siGithub,
6
+ siInstagram,
7
+ siMedium,
8
+ siReddit,
9
+ siTelegram,
10
+ siThreads,
11
+ siX,
12
+ siYcombinator,
13
+ siYoutube,
14
+ } from "simple-icons";
15
+
16
+ import type { FooterSocial } from "../../core/footer.ts";
17
+
18
+ /**
19
+ * How a `footer.socials` platform is shown: its name, for the link's label,
20
+ * and either a filled brand mark or a Lucide icon.
21
+ */
22
+ export interface SocialIcon {
23
+ label: string;
24
+ /** A Lucide icon name, where simple-icons carries no mark for the brand. */
25
+ icon?: string;
26
+ /** A simple-icons path on a 24×24 viewBox, drawn filled. */
27
+ path?: string;
28
+ }
29
+
30
+ /**
31
+ * Each platform's icon. Brand marks come from simple-icons; LinkedIn and
32
+ * Slack had theirs removed there at the brands' request, so they take
33
+ * Lucide's glyphs, as do the generic website and podcast links.
34
+ */
35
+ export const SOCIAL_ICONS: Record<FooterSocial, SocialIcon> = {
36
+ bluesky: { label: "Bluesky", path: siBluesky.path },
37
+ discord: { label: "Discord", path: siDiscord.path },
38
+ facebook: { label: "Facebook", path: siFacebook.path },
39
+ github: { label: "GitHub", path: siGithub.path },
40
+ "hacker-news": { label: "Hacker News", path: siYcombinator.path },
41
+ instagram: { label: "Instagram", path: siInstagram.path },
42
+ linkedin: { icon: "linkedin", label: "LinkedIn" },
43
+ medium: { label: "Medium", path: siMedium.path },
44
+ podcast: { icon: "podcast", label: "Podcast" },
45
+ reddit: { label: "Reddit", path: siReddit.path },
46
+ slack: { icon: "slack", label: "Slack" },
47
+ telegram: { label: "Telegram", path: siTelegram.path },
48
+ threads: { label: "Threads", path: siThreads.path },
49
+ website: { icon: "globe", label: "Website" },
50
+ x: { label: "X", path: siX.path },
51
+ youtube: { label: "YouTube", path: siYoutube.path },
52
+ };
53
+
54
+ /** A configured social link, with the icon it's shown by. */
55
+ export interface FooterSocialLink extends SocialIcon {
56
+ href: string;
57
+ }
58
+
59
+ const isFooterSocial = (key: string): key is FooterSocial =>
60
+ Object.hasOwn(SOCIAL_ICONS, key);
61
+
62
+ /** The site's repository, as the footer links it. */
63
+ export interface FooterRepoLink {
64
+ href: string;
65
+ /** Its accessible name, in the page's language ("GitHub repository"). */
66
+ label: string;
67
+ }
68
+
69
+ /**
70
+ * The footer's social links, in the order written, each with its icon. The
71
+ * footer is the only place the site links its repository, so `repo` (from
72
+ * `github` in the config) leads as the GitHub link, unless `socials.github`
73
+ * names one itself.
74
+ */
75
+ export const footerSocialLinks = (
76
+ socials: Partial<Record<FooterSocial, string>>,
77
+ repo?: FooterRepoLink | null
78
+ ): FooterSocialLink[] => {
79
+ const links = Object.entries(socials).flatMap(([platform, href]) =>
80
+ isFooterSocial(platform) && href
81
+ ? [{ ...SOCIAL_ICONS[platform], href }]
82
+ : []
83
+ );
84
+ return repo && !socials.github
85
+ ? [{ ...SOCIAL_ICONS.github, ...repo }, ...links]
86
+ : links;
87
+ };
@@ -44,8 +44,13 @@
44
44
  * stored preference instead of aborting before the swap listeners exist, and
45
45
  * after a swap the theme the `before-swap` stamp carried over stands, so a
46
46
  * toggle the reader made still holds for the rest of their visit.
47
+ *
48
+ * Paper is white, so a dark page prints light: `beforeprint` switches the root
49
+ * to the light theme (every token, Shiki color, and `dark:` utility follows
50
+ * it) and `afterprint` puts the dark theme back. The listeners sit on
51
+ * `window`, so the one registration outlives client-router swaps.
47
52
  */
48
- export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";const apply=()=>{const r=document.documentElement;let s=null;let blocked=false;try{s=localStorage.getItem("blume-theme");}catch{blocked=true;}const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.dataset.theme=s??(blocked&&r.dataset.theme?r.dataset.theme:m==="system"?sys:m);};apply();document.addEventListener("astro:before-swap",(e)=>{const t=document.documentElement.dataset.theme;if(t){e.newDocument.documentElement.dataset.theme=t;}});document.addEventListener("astro:after-swap",apply);})();`;
53
+ export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";const apply=()=>{const r=document.documentElement;let s=null;let blocked=false;try{s=localStorage.getItem("blume-theme");}catch{blocked=true;}const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.dataset.theme=s??(blocked&&r.dataset.theme?r.dataset.theme:m==="system"?sys:m);};apply();document.addEventListener("astro:before-swap",(e)=>{const t=document.documentElement.dataset.theme;if(t){e.newDocument.documentElement.dataset.theme=t;}});document.addEventListener("astro:after-swap",apply);let printed=null;addEventListener("beforeprint",()=>{const r=document.documentElement;if(r.dataset.theme==="dark"){printed="dark";r.dataset.theme="light";}});addEventListener("afterprint",()=>{if(printed){document.documentElement.dataset.theme=printed;printed=null;}});})();`;
49
54
 
50
55
  /**
51
56
  * Keep an embedded Scalar reference on Blume's theme. Scalar picks its own