blume 2.1.3 → 2.2.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 (397) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +134 -0
  3. package/README.md +4 -4
  4. package/dist/cli/{chunk-y4msrwm9.js → chunk-080mfch6.js} +15 -15
  5. package/dist/cli/{chunk-gfc5fqvf.js → chunk-1ch3ct3r.js} +18 -18
  6. package/dist/cli/{chunk-gfc5fqvf.js.map → chunk-1ch3ct3r.js.map} +1 -1
  7. package/dist/cli/{chunk-32khwcb6.js → chunk-2ssh60qh.js} +42 -69
  8. package/dist/cli/{chunk-32khwcb6.js.map → chunk-2ssh60qh.js.map} +9 -10
  9. package/dist/cli/{chunk-9qzgg6mj.js → chunk-34wap3ed.js} +3 -3
  10. package/dist/cli/{chunk-k3gr1nm9.js → chunk-40vk8kgf.js} +13 -16
  11. package/dist/cli/{chunk-k3gr1nm9.js.map → chunk-40vk8kgf.js.map} +3 -3
  12. package/dist/cli/chunk-487b95qy.js +329 -0
  13. package/dist/cli/chunk-487b95qy.js.map +13 -0
  14. package/dist/cli/chunk-4zxh25hc.js +157 -0
  15. package/dist/cli/chunk-4zxh25hc.js.map +10 -0
  16. package/dist/cli/chunk-69rjf786.js +146 -0
  17. package/dist/cli/chunk-69rjf786.js.map +11 -0
  18. package/dist/cli/{chunk-dqhg54mr.js → chunk-9h8mh2tk.js} +3 -3
  19. package/dist/cli/{chunk-dqhg54mr.js.map → chunk-9h8mh2tk.js.map} +2 -2
  20. package/dist/cli/{chunk-bqxaqn5p.js → chunk-9jqmq5hv.js} +8 -8
  21. package/dist/cli/{chunk-bqxaqn5p.js.map → chunk-9jqmq5hv.js.map} +1 -1
  22. package/dist/cli/{chunk-dv2z0rzf.js → chunk-aafhwx74.js} +38 -13
  23. package/dist/cli/{chunk-dv2z0rzf.js.map → chunk-aafhwx74.js.map} +5 -4
  24. package/dist/cli/{chunk-desvm72z.js → chunk-b680sv30.js} +22 -26
  25. package/dist/cli/chunk-b680sv30.js.map +13 -0
  26. package/dist/cli/{chunk-0w6hs479.js → chunk-beb0z983.js} +30 -18
  27. package/dist/cli/chunk-beb0z983.js.map +10 -0
  28. package/dist/cli/{chunk-1f882900.js → chunk-c5bjxq8v.js} +2 -2
  29. package/dist/cli/chunk-e1n41w9m.js +155 -0
  30. package/dist/cli/chunk-e1n41w9m.js.map +11 -0
  31. package/dist/cli/{chunk-sc93apfs.js → chunk-etckvbrh.js} +17 -17
  32. package/dist/cli/{chunk-sc93apfs.js.map → chunk-etckvbrh.js.map} +2 -2
  33. package/dist/cli/{chunk-bxwhtvw8.js → chunk-f260jdrn.js} +5 -5
  34. package/dist/cli/{chunk-74wfhv79.js → chunk-fnxgtjeq.js} +1 -1
  35. package/dist/cli/{chunk-ms9z66nz.js → chunk-gwy55cx3.js} +2 -2
  36. package/dist/cli/{chunk-ms9z66nz.js.map → chunk-gwy55cx3.js.map} +2 -2
  37. package/dist/cli/{chunk-dhwtqnnd.js → chunk-gxhcf2hs.js} +2 -2
  38. package/dist/cli/{chunk-tfycddbt.js → chunk-h0qwdbem.js} +2 -2
  39. package/dist/cli/{chunk-nbyvgnnm.js → chunk-hktq4j5r.js} +109 -49
  40. package/dist/cli/chunk-hktq4j5r.js.map +36 -0
  41. package/dist/cli/{chunk-twy7psdz.js → chunk-hn3g2b89.js} +14 -10
  42. package/dist/cli/chunk-hn3g2b89.js.map +10 -0
  43. package/dist/cli/{chunk-j1x92mx9.js → chunk-j4nfgn2j.js} +1 -1
  44. package/dist/cli/{chunk-dpy8qdjf.js → chunk-k45sq16q.js} +74 -183
  45. package/dist/cli/chunk-k45sq16q.js.map +18 -0
  46. package/dist/cli/{chunk-5cmts6dv.js → chunk-kmswjxj9.js} +7 -7
  47. package/dist/cli/{chunk-5cmts6dv.js.map → chunk-kmswjxj9.js.map} +1 -1
  48. package/dist/cli/{chunk-36bxs9c7.js → chunk-nqna8dzp.js} +27 -23
  49. package/dist/cli/chunk-nqna8dzp.js.map +10 -0
  50. package/dist/cli/{chunk-a57yh0h9.js → chunk-p834rkcn.js} +7 -7
  51. package/dist/cli/{chunk-a57yh0h9.js.map → chunk-p834rkcn.js.map} +1 -1
  52. package/dist/cli/{chunk-60hz265r.js → chunk-ppa6mg4d.js} +24 -11
  53. package/dist/cli/chunk-ppa6mg4d.js.map +10 -0
  54. package/dist/cli/{chunk-s0bdnc7v.js → chunk-q1wwrxzw.js} +5 -4
  55. package/dist/cli/chunk-q1wwrxzw.js.map +10 -0
  56. package/dist/cli/{chunk-ave1wcbr.js → chunk-qcyxkj44.js} +1 -1
  57. package/dist/cli/{chunk-124fvmzq.js → chunk-r5nm9w0c.js} +11183 -8806
  58. package/dist/cli/{chunk-124fvmzq.js.map → chunk-r5nm9w0c.js.map} +82 -66
  59. package/dist/cli/{chunk-ckrcm08h.js → chunk-rtvgmg2j.js} +48 -62
  60. package/dist/cli/{chunk-ckrcm08h.js.map → chunk-rtvgmg2j.js.map} +4 -5
  61. package/dist/cli/{chunk-297ajdcv.js → chunk-t2h8pwy1.js} +6 -6
  62. package/dist/cli/{chunk-297ajdcv.js.map → chunk-t2h8pwy1.js.map} +3 -3
  63. package/dist/cli/{chunk-gxtg6ed2.js → chunk-x60e15st.js} +21 -21
  64. package/dist/cli/{chunk-gxtg6ed2.js.map → chunk-x60e15st.js.map} +3 -3
  65. package/dist/cli/{chunk-58bt3hag.js → chunk-x7bh6d9j.js} +4 -4
  66. package/dist/cli/{chunk-hvcxng5f.js → chunk-ydqzkhjn.js} +6 -6
  67. package/dist/cli/{chunk-kdpzw38a.js → chunk-yg8ckc3g.js} +95 -7
  68. package/dist/cli/chunk-yg8ckc3g.js.map +11 -0
  69. package/dist/cli/{chunk-0pmb4tr4.js → chunk-zxd5jrgx.js} +1205 -222
  70. package/dist/cli/chunk-zxd5jrgx.js.map +44 -0
  71. package/dist/cli/index.js +78 -42
  72. package/dist/cli/index.js.map +7 -6
  73. package/dist/types/ai/changelog-components.d.ts +18 -0
  74. package/dist/types/ai/changelog-markdown.d.ts +12 -0
  75. package/dist/types/ai/openapi-components.d.ts +7 -7
  76. package/dist/types/ai/operation-details.d.ts +39 -0
  77. package/dist/types/ai/relative-links.d.ts +3 -1
  78. package/dist/types/ai/serializers.d.ts +6 -4
  79. package/dist/types/analytics/head.d.ts +4 -2
  80. package/dist/types/analytics/script.d.ts +8 -2
  81. package/dist/types/components/content/youtube.d.ts +20 -0
  82. package/dist/types/components/layout/nav-utils.d.ts +8 -0
  83. package/dist/types/components/openapi/example-panels.d.ts +33 -0
  84. package/dist/types/components/openapi/helpers.d.ts +53 -11
  85. package/dist/types/components/openapi/snippets.d.ts +7 -1
  86. package/dist/types/core/alert-diagnostics.d.ts +10 -0
  87. package/dist/types/core/builtin-tags.d.ts +16 -0
  88. package/dist/types/core/changelog-list.d.ts +48 -0
  89. package/dist/types/core/closest-match.d.ts +6 -0
  90. package/dist/types/core/code-fence-diagnostics.d.ts +18 -0
  91. package/dist/types/core/code-fences.d.ts +8 -1
  92. package/dist/types/core/config-input.d.ts +38 -6
  93. package/dist/types/core/content-assets.d.ts +16 -18
  94. package/dist/types/core/data.d.ts +6 -2
  95. package/dist/types/core/diagnostics.d.ts +14 -0
  96. package/dist/types/core/directive-diagnostics.d.ts +22 -8
  97. package/dist/types/core/former-routes.d.ts +14 -0
  98. package/dist/types/core/github.d.ts +5 -4
  99. package/dist/types/core/heading-markers.d.ts +65 -2
  100. package/dist/types/core/hidden-pages.d.ts +3 -0
  101. package/dist/types/core/includes.d.ts +23 -0
  102. package/dist/types/core/last-modified.d.ts +18 -6
  103. package/dist/types/core/links.d.ts +42 -2
  104. package/dist/types/core/load-module.d.ts +19 -4
  105. package/dist/types/core/manifest.d.ts +14 -1
  106. package/dist/types/core/markdown-mirrors.d.ts +22 -0
  107. package/dist/types/core/mcp-clients.d.ts +47 -0
  108. package/dist/types/core/mdx-elements.d.ts +32 -0
  109. package/dist/types/core/mdx-syntax.d.ts +100 -0
  110. package/dist/types/core/meta-diagnostics.d.ts +7 -0
  111. package/dist/types/core/meta.d.ts +32 -5
  112. package/dist/types/core/nav-diagnostics.d.ts +16 -2
  113. package/dist/types/core/navigation.d.ts +13 -0
  114. package/dist/types/core/ordering-prefix.d.ts +6 -0
  115. package/dist/types/core/pinned-ids.d.ts +23 -0
  116. package/dist/types/core/project-graph.d.ts +9 -1
  117. package/dist/types/core/redirect-patterns.d.ts +18 -0
  118. package/dist/types/core/relative-files.d.ts +31 -0
  119. package/dist/types/core/schema.d.ts +322 -11
  120. package/dist/types/core/sources/normalize.d.ts +101 -9
  121. package/dist/types/core/sources/types.d.ts +16 -0
  122. package/dist/types/core/static-files.d.ts +10 -0
  123. package/dist/types/core/static-host.d.ts +42 -0
  124. package/dist/types/core/svg-images.d.ts +26 -0
  125. package/dist/types/core/syntax-diagnostics.d.ts +14 -0
  126. package/dist/types/core/types.d.ts +42 -1
  127. package/dist/types/deploy/generated-files.d.ts +41 -0
  128. package/dist/types/deploy/redirects.d.ts +21 -7
  129. package/dist/types/markdown/code-title.d.ts +9 -5
  130. package/dist/types/markdown/directives.d.ts +8 -0
  131. package/dist/types/markdown/fence-language.d.ts +51 -0
  132. package/dist/types/markdown/fence-meta.d.ts +5 -0
  133. package/dist/types/markdown/github-alerts.d.ts +35 -0
  134. package/dist/types/openapi/asyncapi.d.ts +6 -5
  135. package/dist/types/openapi/checks.d.ts +10 -5
  136. package/dist/types/openapi/model.d.ts +55 -8
  137. package/dist/types/openapi/parse.d.ts +4 -1
  138. package/dist/types/ratelimit/index.d.ts +2 -0
  139. package/dist/types/ratelimit/memory.d.ts +1 -1
  140. package/dist/types/ratelimit/schema.d.ts +40 -0
  141. package/dist/types/ratelimit/unkey.d.ts +48 -0
  142. package/dist/types/sources/filesystem.d.ts +14 -4
  143. package/dist/types/sources/mdx-remote.d.ts +6 -3
  144. package/dist/types/sources/registry.d.ts +3 -3
  145. package/dist/types/theme/color.d.ts +33 -0
  146. package/dist/types/theme/palette.d.ts +122 -0
  147. package/docs/01-quickstart.mdx +1 -1
  148. package/docs/02-deployment.mdx +24 -8
  149. package/docs/04-migrating.mdx +16 -4
  150. package/docs/advanced/changelog.mdx +27 -1
  151. package/docs/advanced/skills.mdx +1 -1
  152. package/docs/cli/audit.mdx +1 -1
  153. package/docs/cli/doctor.mdx +2 -2
  154. package/docs/cli/evals.mdx +1 -1
  155. package/docs/cli/index.mdx +4 -2
  156. package/docs/cli/validate.mdx +10 -7
  157. package/docs/configuration/analytics.mdx +2 -0
  158. package/docs/configuration/customization.mdx +3 -1
  159. package/docs/configuration/index.mdx +7 -6
  160. package/docs/configuration/rate-limiting.mdx +20 -1
  161. package/docs/configuration/theming.mdx +5 -1
  162. package/docs/content/components.mdx +26 -6
  163. package/docs/content/frontmatter.mdx +1 -1
  164. package/docs/content/i18n.mdx +1 -1
  165. package/docs/content/includes.mdx +6 -2
  166. package/docs/content/index.mdx +11 -1
  167. package/docs/content/islands.mdx +2 -0
  168. package/docs/content/meta.mdx +9 -3
  169. package/docs/content/navigation.mdx +14 -6
  170. package/docs/content/sources/obsidian.mdx +1 -1
  171. package/docs/content/sources/remote-mdx.mdx +3 -1
  172. package/docs/content/syntax.mdx +76 -8
  173. package/docs/content/variables.mdx +3 -1
  174. package/docs/discoverability/agent-discovery.mdx +1 -1
  175. package/docs/discoverability/json-api.mdx +1 -1
  176. package/docs/discoverability/llms-txt.mdx +1 -1
  177. package/docs/discoverability/markdown.mdx +2 -2
  178. package/docs/discoverability/mcp.mdx +40 -8
  179. package/docs/discoverability/open-graph.mdx +1 -1
  180. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  181. package/docs/references/graphql.mdx +2 -0
  182. package/docs/references/openapi.mdx +45 -7
  183. package/package.json +4 -4
  184. package/skills/blume/SKILL.md +2 -2
  185. package/skills/blume-migrate/SKILL.md +51 -29
  186. package/skills/blume-migrate/references/docsify.md +262 -0
  187. package/skills/blume-migrate/references/docus.md +298 -0
  188. package/skills/blume-migrate/references/docusaurus.md +5 -5
  189. package/skills/blume-migrate/references/fern.md +327 -0
  190. package/skills/blume-migrate/references/fumadocs.md +1 -1
  191. package/skills/blume-migrate/references/gitbook.md +267 -0
  192. package/skills/blume-migrate/references/github-wiki.md +259 -0
  193. package/skills/blume-migrate/references/jekyll.md +253 -0
  194. package/skills/blume-migrate/references/mdbook.md +256 -0
  195. package/skills/blume-migrate/references/mintlify.md +5 -5
  196. package/skills/blume-migrate/references/mkdocs.md +236 -0
  197. package/skills/blume-migrate/references/monorepo.md +20 -8
  198. package/skills/blume-migrate/references/nextra.md +5 -5
  199. package/skills/blume-migrate/references/readme.md +257 -0
  200. package/skills/blume-migrate/references/redocly.md +198 -0
  201. package/skills/blume-migrate/references/starlight.md +3 -3
  202. package/skills/blume-migrate/references/vitepress.md +204 -0
  203. package/skills/blume-migrate/references/vuepress.md +271 -0
  204. package/skills/blume-migrate/scripts/docsify-codemod.mjs +2387 -0
  205. package/skills/blume-migrate/scripts/docus-codemod.mjs +2542 -0
  206. package/skills/blume-migrate/scripts/fern-codemod.mjs +2633 -0
  207. package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +2808 -0
  208. package/skills/blume-migrate/scripts/include-excerpts.mjs +649 -0
  209. package/skills/blume-migrate/scripts/jekyll-codemod.mjs +3649 -0
  210. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +17 -13
  211. package/skills/blume-migrate/scripts/mkdocs-codemod.mjs +2526 -0
  212. package/skills/blume-migrate/scripts/operation-routes.mjs +508 -0
  213. package/skills/blume-migrate/scripts/pin-heading-ids.mjs +1049 -0
  214. package/skills/blume-migrate/scripts/readme-codemod.mjs +4139 -0
  215. package/skills/blume-migrate/scripts/vitepress-codemod.mjs +1511 -0
  216. package/src/ai/changelog-components.ts +24 -0
  217. package/src/ai/changelog-markdown.ts +45 -41
  218. package/src/ai/component-markdown.ts +22 -9
  219. package/src/ai/llms.ts +36 -5
  220. package/src/ai/markdown.ts +2 -2
  221. package/src/ai/openapi-components.ts +25 -9
  222. package/src/ai/operation-details.ts +208 -0
  223. package/src/ai/relative-links.ts +48 -8
  224. package/src/ai/serializers.ts +7 -6
  225. package/src/ai/site-skill.ts +6 -2
  226. package/src/analytics/head.ts +11 -5
  227. package/src/analytics/script.ts +13 -3
  228. package/src/astro/generate.ts +47 -11
  229. package/src/astro/index.ts +1 -0
  230. package/src/astro/integration.ts +27 -1
  231. package/src/astro/mdx-source-errors.ts +46 -0
  232. package/src/astro/templates.ts +93 -189
  233. package/src/audit/checks/redirects.ts +5 -1
  234. package/src/audit/redirects.ts +36 -19
  235. package/src/audit/report.ts +22 -9
  236. package/src/audit/run.ts +12 -28
  237. package/src/audit/terms.ts +23 -0
  238. package/src/audit/url.ts +2 -2
  239. package/src/cli/build-failure.ts +256 -7
  240. package/src/cli/command-meta.ts +1 -1
  241. package/src/cli/commands/audit.ts +1 -1
  242. package/src/cli/commands/build.ts +12 -3
  243. package/src/cli/commands/check.ts +13 -2
  244. package/src/cli/commands/dev.ts +1 -1
  245. package/src/cli/commands/doctor.ts +4 -2
  246. package/src/cli/commands/init.ts +3 -3
  247. package/src/cli/commands/preview.ts +17 -1
  248. package/src/cli/commands/validate.ts +22 -10
  249. package/src/cli/prepare.ts +1 -1
  250. package/src/cli/preview-pages.ts +161 -0
  251. package/src/cli/required-secrets.ts +0 -1
  252. package/src/cli/unknown-flags.ts +1 -42
  253. package/src/components/code-copy.ts +51 -0
  254. package/src/components/code-language.ts +75 -0
  255. package/src/components/content/AccordionItem.astro +16 -1
  256. package/src/components/content/Callout.astro +11 -1
  257. package/src/components/content/Changelog.astro +189 -0
  258. package/src/components/content/CodeBlock.astro +8 -11
  259. package/src/components/content/Tabs.astro +16 -2
  260. package/src/components/content/YouTube.astro +6 -4
  261. package/src/components/content/code-block.ts +33 -0
  262. package/src/components/content/content-strings.ts +5 -1
  263. package/src/components/content/inline-markdown.ts +35 -8
  264. package/src/components/content/youtube.ts +49 -7
  265. package/src/components/layout/Analytics.astro +5 -1
  266. package/src/components/layout/NavTree.astro +8 -3
  267. package/src/components/layout/PageActions.astro +81 -23
  268. package/src/components/layout/RootLayout.astro +35 -32
  269. package/src/components/layout/SiteFooter.astro +31 -8
  270. package/src/components/layout/nav-utils.ts +14 -0
  271. package/src/components/layout/search/pagefind.ts +2 -1
  272. package/src/components/openapi/Operation.astro +4 -0
  273. package/src/components/openapi/PanelTabs.astro +9 -3
  274. package/src/components/openapi/RequestBody.astro +42 -8
  275. package/src/components/openapi/RequestPanel.astro +34 -53
  276. package/src/components/openapi/example-panels.ts +110 -0
  277. package/src/components/openapi/helpers.ts +195 -20
  278. package/src/components/openapi/operation-model.ts +63 -26
  279. package/src/components/openapi/request.ts +13 -0
  280. package/src/components/openapi/snippets.ts +14 -2
  281. package/src/core/alert-diagnostics.ts +89 -0
  282. package/src/core/builtin-tags.ts +76 -0
  283. package/src/core/changelog-list.ts +87 -0
  284. package/src/core/closest-match.ts +41 -0
  285. package/src/core/code-fence-diagnostics.ts +267 -0
  286. package/src/core/code-fences.ts +43 -7
  287. package/src/core/component-diagnostics.ts +65 -12
  288. package/src/core/config-input.ts +39 -5
  289. package/src/core/config.ts +46 -1
  290. package/src/core/content-assets.ts +64 -144
  291. package/src/core/data.ts +10 -3
  292. package/src/core/diagnostics.ts +36 -3
  293. package/src/core/directive-diagnostics.ts +265 -47
  294. package/src/core/former-routes.ts +46 -0
  295. package/src/core/git-ignored.ts +53 -0
  296. package/src/core/github.ts +5 -4
  297. package/src/core/heading-markers.ts +156 -2
  298. package/src/core/hidden-pages.ts +64 -0
  299. package/src/core/includes.ts +181 -41
  300. package/src/core/last-modified.ts +47 -10
  301. package/src/core/links.ts +438 -83
  302. package/src/core/load-module.ts +98 -9
  303. package/src/core/manifest.ts +12 -9
  304. package/src/core/markdown-mirrors.ts +52 -0
  305. package/src/core/mcp-clients.ts +87 -0
  306. package/src/core/mdx-elements.ts +153 -0
  307. package/src/core/mdx-syntax.ts +377 -0
  308. package/src/core/meta-diagnostics.ts +134 -0
  309. package/src/core/meta.ts +154 -55
  310. package/src/core/nav-diagnostics.ts +41 -4
  311. package/src/core/navigation.ts +70 -10
  312. package/src/core/ordering-prefix.ts +22 -2
  313. package/src/core/pinned-ids.ts +141 -0
  314. package/src/core/project-graph.ts +221 -29
  315. package/src/core/redirect-patterns.ts +41 -0
  316. package/src/core/relative-files.ts +145 -0
  317. package/src/core/schema.ts +110 -6
  318. package/src/core/sources/mdx-remote.ts +75 -14
  319. package/src/core/sources/normalize.ts +885 -179
  320. package/src/core/sources/notion.ts +5 -4
  321. package/src/core/sources/obsidian.ts +2 -5
  322. package/src/core/sources/types.ts +17 -0
  323. package/src/core/static-files.ts +40 -0
  324. package/src/core/static-host.ts +125 -0
  325. package/src/core/svg-images.ts +98 -0
  326. package/src/core/syntax-diagnostics.ts +491 -0
  327. package/src/core/types.ts +43 -1
  328. package/src/deploy/artifacts.ts +2 -28
  329. package/src/deploy/generated-files.ts +128 -0
  330. package/src/deploy/platforms/vercel.ts +25 -1
  331. package/src/deploy/redirects.ts +43 -16
  332. package/src/deploy/sitemap.ts +2 -1
  333. package/src/markdown/base-links.ts +87 -6
  334. package/src/markdown/code-title.ts +29 -13
  335. package/src/markdown/content-assets.ts +210 -0
  336. package/src/markdown/directives.ts +69 -5
  337. package/src/markdown/fence-language.ts +106 -0
  338. package/src/markdown/fence-meta.ts +42 -0
  339. package/src/markdown/github-alerts.ts +172 -0
  340. package/src/markdown/heading-anchors.ts +265 -56
  341. package/src/markdown/include.ts +30 -3
  342. package/src/markdown/index.ts +145 -40
  343. package/src/markdown/prompt-text.ts +95 -0
  344. package/src/markdown/relative-links.ts +37 -2
  345. package/src/markdown/route-snapshot.ts +22 -12
  346. package/src/markdown/svg-fallback.ts +51 -0
  347. package/src/markdown/title-heading.ts +134 -0
  348. package/src/migrate/migrate.ts +127 -2
  349. package/src/og/card.ts +14 -4
  350. package/src/openapi/asyncapi.ts +14 -6
  351. package/src/openapi/checks.ts +160 -15
  352. package/src/openapi/model.ts +172 -33
  353. package/src/openapi/parse.ts +13 -4
  354. package/src/openapi/render-mdx.ts +55 -9
  355. package/src/openapi/source.ts +87 -6
  356. package/src/ratelimit/index.ts +2 -0
  357. package/src/ratelimit/memory.ts +1 -1
  358. package/src/ratelimit/runtime.ts +81 -0
  359. package/src/ratelimit/schema.ts +2 -0
  360. package/src/ratelimit/unkey.ts +61 -0
  361. package/src/registry/eject.ts +0 -1
  362. package/src/registry/registry.ts +1 -1
  363. package/src/search/documents.ts +10 -2
  364. package/src/sources/filesystem.ts +28 -3
  365. package/src/sources/mdx-remote.ts +7 -15
  366. package/src/theme/color.ts +14 -0
  367. package/src/theme/entry.ts +22 -1
  368. package/src/theme/palette.ts +74 -1
  369. package/dist/cli/chunk-0pmb4tr4.js.map +0 -35
  370. package/dist/cli/chunk-0w6hs479.js.map +0 -10
  371. package/dist/cli/chunk-150jk7km.js +0 -52
  372. package/dist/cli/chunk-150jk7km.js.map +0 -10
  373. package/dist/cli/chunk-36bxs9c7.js.map +0 -10
  374. package/dist/cli/chunk-53jqw8z0.js +0 -89
  375. package/dist/cli/chunk-53jqw8z0.js.map +0 -10
  376. package/dist/cli/chunk-60hz265r.js.map +0 -10
  377. package/dist/cli/chunk-desvm72z.js.map +0 -14
  378. package/dist/cli/chunk-dpy8qdjf.js.map +0 -17
  379. package/dist/cli/chunk-es8h0v3j.js +0 -441
  380. package/dist/cli/chunk-es8h0v3j.js.map +0 -11
  381. package/dist/cli/chunk-kdpzw38a.js.map +0 -11
  382. package/dist/cli/chunk-nbyvgnnm.js.map +0 -36
  383. package/dist/cli/chunk-s0bdnc7v.js.map +0 -10
  384. package/dist/cli/chunk-twy7psdz.js.map +0 -10
  385. package/dist/cli/chunk-ydp3065p.js +0 -64
  386. package/dist/cli/chunk-ydp3065p.js.map +0 -10
  387. /package/dist/cli/{chunk-y4msrwm9.js.map → chunk-080mfch6.js.map} +0 -0
  388. /package/dist/cli/{chunk-9qzgg6mj.js.map → chunk-34wap3ed.js.map} +0 -0
  389. /package/dist/cli/{chunk-1f882900.js.map → chunk-c5bjxq8v.js.map} +0 -0
  390. /package/dist/cli/{chunk-bxwhtvw8.js.map → chunk-f260jdrn.js.map} +0 -0
  391. /package/dist/cli/{chunk-74wfhv79.js.map → chunk-fnxgtjeq.js.map} +0 -0
  392. /package/dist/cli/{chunk-dhwtqnnd.js.map → chunk-gxhcf2hs.js.map} +0 -0
  393. /package/dist/cli/{chunk-tfycddbt.js.map → chunk-h0qwdbem.js.map} +0 -0
  394. /package/dist/cli/{chunk-j1x92mx9.js.map → chunk-j4nfgn2j.js.map} +0 -0
  395. /package/dist/cli/{chunk-ave1wcbr.js.map → chunk-qcyxkj44.js.map} +0 -0
  396. /package/dist/cli/{chunk-58bt3hag.js.map → chunk-x7bh6d9j.js.map} +0 -0
  397. /package/dist/cli/{chunk-hvcxng5f.js.map → chunk-ydqzkhjn.js.map} +0 -0
@@ -0,0 +1,189 @@
1
+ ---
2
+ // The changelog index's release list: every visible `type: changelog` entry,
3
+ // newest first, grouped by year, each row linking to its own page.
4
+ //
5
+ // The generated `/changelog` page renders this, and so can a hand-written page
6
+ // that wants prose above the list — an introduction, a feed link — which the
7
+ // generated page has no slot for. Only the list lives here: the heading and the
8
+ // description belong to whoever owns the page, so an authored page writes its
9
+ // own and the generated page keeps the translatable `changelog` strings.
10
+ import { getCollection } from "astro:content";
11
+ import data from "blume:data";
12
+
13
+ import {
14
+ compareChangelogDates,
15
+ groupChangelogYears,
16
+ parseChangelogDate,
17
+ uniqueAnchorIds,
18
+ } from "../../core/changelog-list.ts";
19
+ import { resolveDateFormatOptions } from "../../core/date-format.ts";
20
+ import { withMountedBase } from "../islands/base-path.ts";
21
+ import { contentStrings } from "./content-strings.ts";
22
+
23
+ const i18n = data.config.i18n;
24
+ // The list is always the default locale's entries, whatever locale the page
25
+ // sits in. Every other surface that publishes this list already works that way
26
+ // — the generated page, the `/changelog.md` mirror, llms-full.txt, MCP
27
+ // `get_page`, search — so a translated page listing its own locale's entries
28
+ // would be the one surface disagreeing with the rest about which releases
29
+ // exist. Releases are also commonly written once and left untranslated, in
30
+ // which case a per-locale list is the same list with fallback copies anyway.
31
+ // (The chrome around it still follows the reader: `strings` and the dates.)
32
+ const locale = i18n ? i18n.defaultLocale : null;
33
+ const { changelog: strings } = contentStrings(data, Astro.currentLocale);
34
+ // The page's own language, for the row dates and year headings. On the
35
+ // unprefixed generated `/changelog` Astro resolves the default locale.
36
+ const readerLocale = i18n ? (Astro.currentLocale ?? i18n.defaultLocale) : "en";
37
+
38
+ const dateFormatOptions = resolveDateFormatOptions(data.config.dateFormat);
39
+ // The year heads each group, so a row shows the rest of the date: a preset
40
+ // style keeps its month wording, a component format simply drops the year.
41
+ const { dateStyle, year: _year, ...dateComponents } = dateFormatOptions;
42
+ const rowDateFormat: Intl.DateTimeFormatOptions = dateStyle
43
+ ? {
44
+ ...dateComponents,
45
+ day: "numeric",
46
+ // `medium` abbreviates the month; every other preset spells it out, so a
47
+ // `long` site keeps reading "October 5" rather than dropping to "Oct 5".
48
+ month: dateStyle === "medium" ? "short" : "long",
49
+ }
50
+ : dateComponents;
51
+ const formatRowDate = (date: Date) =>
52
+ new Intl.DateTimeFormat(readerLocale, rowDateFormat).format(date);
53
+ const formatYear = (date: Date) =>
54
+ new Intl.DateTimeFormat(readerLocale, {
55
+ timeZone: dateFormatOptions.timeZone,
56
+ year: "numeric",
57
+ }).format(date);
58
+
59
+ const slugify = (text: string) =>
60
+ text
61
+ .toLowerCase()
62
+ .replaceAll(/[^a-z0-9]+/gu, "-")
63
+ .replaceAll(/^-+|-+$/gu, "") || "update";
64
+
65
+ // Map each entry to its own page. The collection entry id matches the route
66
+ // manifest's `entryId`, and the manifest carries a route per locale and per
67
+ // archived version, all sharing that id — so a map over every route would keep
68
+ // whichever came last: a Portuguese permalink on an unlocalized index, or a
69
+ // row pointing into last year's archived docs. Keep the current docs of the
70
+ // listed locale, never a fallback copy (which renders another locale's content
71
+ // at this URL), and list an entry only when it has such a route, so a
72
+ // translated changelog file does not add a second row for the same release.
73
+ // These are the conditions `ai/changelog-markdown.ts` filters pages by, so the
74
+ // rendered list and the Markdown mirror select the same releases.
75
+ //
76
+ // Keyed by collection and id together, the way the catch-all looks an entry up:
77
+ // both collections id an entry by its file path, so a `releases/v1-0-0.md` on
78
+ // disk and a staged release from a source named `releases` share an id while
79
+ // rendering at different routes.
80
+ const entryKey = (collection: string, id: string) => `${collection}:${id}`;
81
+ const routeByEntry = new Map<string, string>();
82
+ for (const route of data.routes) {
83
+ if (
84
+ route.version === "" &&
85
+ !route.fallback &&
86
+ (locale === null || route.locale === locale)
87
+ ) {
88
+ routeByEntry.set(entryKey(route.collection, route.entryId), route.path);
89
+ }
90
+ }
91
+
92
+ // Non-filesystem changelog sources (GitHub Releases) render through a parallel
93
+ // `staged` collection, which exists only when such a source is configured — the
94
+ // route manifest says whether to look, so the index folds those releases in
95
+ // without a generator flag, and a page that reads only `docs` can't quietly
96
+ // drop them.
97
+ const hasStaged = data.routes.some((route) => route.collection === "staged");
98
+ const entries = [
99
+ ...(await getCollection("docs")),
100
+ ...(hasStaged ? await getCollection("staged" as "docs") : []),
101
+ ].filter(
102
+ (entry) =>
103
+ entry.data.type === "changelog" &&
104
+ !entry.data.draft &&
105
+ !entry.data.sidebar?.hidden &&
106
+ // A route is required on every site, not only localized ones: without
107
+ // i18n the map still drops archived versions, and an entry with no
108
+ // current-docs route would otherwise be listed with a dead self-anchor
109
+ // while the Markdown mirror left it out.
110
+ routeByEntry.has(entryKey(entry.collection, entry.id))
111
+ );
112
+
113
+ const rows = entries
114
+ .map((entry) => {
115
+ const date = parseChangelogDate(entry.data.date ?? entry.data.changelog?.date);
116
+ const label =
117
+ entry.data.title ??
118
+ (entry.data.changelog?.version
119
+ ? `v${entry.data.changelog.version}`
120
+ : "Update");
121
+ const route = routeByEntry.get(entryKey(entry.collection, entry.id));
122
+ return {
123
+ date,
124
+ href: route ? withMountedBase(route) : null,
125
+ label,
126
+ tag: entry.data.changelog?.category ?? null,
127
+ };
128
+ })
129
+ .toSorted((a, b) => compareChangelogDates(a.date, b.date));
130
+
131
+ const ids = uniqueAnchorIds(rows.map((row) => slugify(row.label)));
132
+ const items = rows.map((row, index) => ({
133
+ date: row.date ? formatRowDate(row.date) : null,
134
+ dateTime: row.date ? row.date.toISOString().slice(0, 10) : null,
135
+ href: row.href,
136
+ id: ids[index] as string,
137
+ label: row.label,
138
+ tag: row.tag,
139
+ year: row.date ? formatYear(row.date) : null,
140
+ }));
141
+
142
+ const groups = groupChangelogYears(items, (item) => item.year);
143
+ ---
144
+
145
+ {
146
+ items.length === 0 ? (
147
+ <p>{strings.empty}</p>
148
+ ) : (
149
+ <div class="not-prose mt-10 divide-y divide-border border-border border-y">
150
+ {groups.map((group) => (
151
+ <section
152
+ aria-label={group.year ?? undefined}
153
+ class="grid md:grid-cols-[6rem_minmax(0,1fr)] md:gap-x-8"
154
+ >
155
+ <h2 class="mt-0! pt-3! font-medium! text-muted-foreground text-sm! leading-5! tabular-nums max-md:pb-1">
156
+ {group.year}
157
+ </h2>
158
+ <ul class="m-0! list-none divide-y divide-border p-0!">
159
+ {group.items.map((item) => (
160
+ <li class="m-0! p-0!" id={item.id}>
161
+ <a
162
+ class="group/release flex items-baseline gap-4 py-3 no-underline! hover:no-underline!"
163
+ href={item.href ?? `#${item.id}`}
164
+ >
165
+ <span class="min-w-0 flex-1 font-medium text-foreground text-sm transition-colors group-hover/release:text-accent">
166
+ {item.label}
167
+ </span>
168
+ {item.tag && (
169
+ <span class="shrink-0 rounded-full bg-muted px-2 py-0.5 font-medium text-[0.65rem] text-muted-foreground">
170
+ {item.tag}
171
+ </span>
172
+ )}
173
+ {item.date && (
174
+ <time
175
+ class="shrink-0 font-mono text-muted-foreground text-xs tabular-nums"
176
+ datetime={item.dateTime}
177
+ >
178
+ {item.date}
179
+ </time>
180
+ )}
181
+ </a>
182
+ </li>
183
+ ))}
184
+ </ul>
185
+ </section>
186
+ ))}
187
+ </div>
188
+ )
189
+ }
@@ -9,14 +9,16 @@
9
9
  // The theme's code styles are scoped to `.prose`, so the output is wrapped in a
10
10
  // `.prose` container (`max-w-none` so it fills its own column rather than the
11
11
  // prose reading measure); that's what makes the light/dark swap work on a bare
12
- // page without the manual `[data-theme]` CSS it would otherwise need.
12
+ // page without the manual `[data-theme]` CSS it would otherwise need. Without
13
+ // `code`, the children render as written: a fenced block wrapped in
14
+ // `<CodeBlock>` (Fern's and Mintlify's spelling) is already highlighted.
13
15
  import data from "blume:data";
14
16
 
15
- import { highlightCode } from "../../markdown/index.ts";
17
+ import { codeBlockHtml } from "./code-block.ts";
16
18
 
17
19
  interface Props {
18
- /** The source to highlight. */
19
- code: string;
20
+ /** The source to highlight; without it, the children render instead. */
21
+ code?: string;
20
22
  /** Shiki language id (default `txt`); an unknown one renders plain. */
21
23
  lang?: string;
22
24
  /** Show the brand language icon in the header (default on, like fences). */
@@ -25,12 +27,7 @@ interface Props {
25
27
  title?: string;
26
28
  }
27
29
 
28
- const { code, lang = "txt", icons, title } = Astro.props;
29
- const html = await highlightCode(code.replace(/\n+$/u, ""), lang, {
30
- icons,
31
- themes: data.config.codeThemes,
32
- title,
33
- });
30
+ const html = await codeBlockHtml(Astro.props, data.config.codeThemes);
34
31
  ---
35
32
 
36
- <div class="prose max-w-none"><Fragment set:html={html} /></div>
33
+ {html === null ? <slot /> : <div class="prose max-w-none"><Fragment set:html={html} /></div>}
@@ -110,6 +110,8 @@ const { content: strings } = contentStrings(data, Astro.currentLocale);
110
110
  </blume-tabs>
111
111
 
112
112
  <script>
113
+ import { languageLabel } from "../code-language.ts";
114
+
113
115
  const TRIGGER_CLASS =
114
116
  "-mb-px inline-flex shrink-0 cursor-pointer items-center gap-1.5 border-transparent border-b-2 bg-transparent px-1 py-2.5 font-medium text-muted-foreground text-xs transition-colors hover:text-foreground aria-[selected=true]:border-accent aria-[selected=true]:text-foreground";
115
117
  const SELECT_CLASS =
@@ -120,9 +122,20 @@ const { content: strings } = contentStrings(data, Astro.currentLocale);
120
122
  // in several <Tabs> instances on one page.
121
123
  let instanceCount = 0;
122
124
 
123
- // An untitled panel is numbered with its group's localized `Tab {n}`.
125
+ // An untitled code block (a CodeGroup fence) is named by its language, read
126
+ // from the fence's own id: the docs layout moves that to
127
+ // `data-blume-language` when it relabels `data-language` for the header.
128
+ const codeLanguage = (panel: HTMLElement) => {
129
+ const language = panel.matches("pre")
130
+ ? (panel.dataset.blumeLanguage ?? panel.dataset.language)
131
+ : undefined;
132
+ return language ? languageLabel(language) : undefined;
133
+ };
134
+
135
+ // Any other untitled panel is numbered with its group's localized `Tab {n}`.
124
136
  const panelTitle = (panel: HTMLElement, index: number) =>
125
137
  panel.dataset.title ??
138
+ codeLanguage(panel) ??
126
139
  (
127
140
  panel.closest<HTMLElement>("blume-tabs")?.dataset.i18nTab ?? "Tab {n}"
128
141
  ).replace("{n}", String(index + 1));
@@ -160,7 +173,8 @@ const { content: strings } = contentStrings(data, Astro.currentLocale);
160
173
  ).filter((panel) => panel.closest("blume-tabs") === this);
161
174
  // CodeGroup passes raw code fences instead of <Tab> elements, so adopt
162
175
  // the content wrapper's direct code blocks as panels. The tab label comes
163
- // from each block's title (the text after the language in the fence).
176
+ // from each block's title (the text after the language in the fence),
177
+ // or from its language when it has none.
164
178
  // A ts2js fence arrives as its own TypeScript/JavaScript group; it is
165
179
  // one panel too, labeled by the fence title both of its blocks carry.
166
180
  if (panels.length === 0) {
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { parseYouTubeId, youtubeEmbedUrl } from "./youtube.ts";
2
+ import { youtubeEmbedSrc } from "./youtube.ts";
3
3
 
4
4
  interface Props {
5
5
  /** A YouTube video id (e.g. `dQw4w9WgXcQ`). */
@@ -8,14 +8,16 @@ interface Props {
8
8
  start?: number;
9
9
  /** Accessible title for the embedded player. */
10
10
  title?: string;
11
- /** A full YouTube URL to extract the id from, as an alternative to `id`. */
11
+ /**
12
+ * A full YouTube URL to extract the id from, as an alternative to `id`; a
13
+ * playlist URL embeds the playlist.
14
+ */
12
15
  url?: string;
13
16
  }
14
17
 
15
18
  const { id, start, title = "YouTube video player", url } = Astro.props;
16
19
 
17
- const videoId = parseYouTubeId(id ?? url ?? "");
18
- const src = videoId ? youtubeEmbedUrl(videoId, { start }) : null;
20
+ const src = youtubeEmbedSrc(id ?? url ?? "", { start });
19
21
  ---
20
22
 
21
23
  {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The highlighting half of the `<CodeBlock>` content component, kept in a
3
+ * sibling `.ts` (like `badge-color.ts`) so it is unit-testable.
4
+ */
5
+ import { highlightCode } from "../../markdown/index.ts";
6
+ import type { CodeThemes } from "../../markdown/index.ts";
7
+
8
+ /** The props `<CodeBlock>` highlights from. */
9
+ export interface CodeBlockSource {
10
+ code?: string;
11
+ icons?: boolean;
12
+ lang?: string;
13
+ title?: string;
14
+ }
15
+
16
+ /**
17
+ * The highlighted HTML for a `<CodeBlock>`, or null when it has no `code`.
18
+ * Fern and Mintlify wrap a fenced block in `<CodeBlock>…</CodeBlock>`; the
19
+ * fence inside is already highlighted by the Markdown pipeline, so a
20
+ * `CodeBlock` without `code` renders its children as written rather than
21
+ * failing the page on the missing string.
22
+ */
23
+ export const codeBlockHtml = async (
24
+ { code, icons, lang = "txt", title }: CodeBlockSource,
25
+ themes?: CodeThemes
26
+ ): Promise<string | null> =>
27
+ code === undefined
28
+ ? null
29
+ : await highlightCode(code.replace(/\n+$/u, ""), lang, {
30
+ icons,
31
+ themes,
32
+ title,
33
+ });
@@ -9,7 +9,10 @@ export interface ContentStringsData {
9
9
  }
10
10
 
11
11
  /** The UI string groups a content component's chrome reads. */
12
- export type ContentStrings = Pick<UIStrings, "actions" | "content">;
12
+ export type ContentStrings = Pick<
13
+ UIStrings,
14
+ "actions" | "changelog" | "content"
15
+ >;
13
16
 
14
17
  /**
15
18
  * Labels for a component rendered inside page content (MDX), which gets no
@@ -29,6 +32,7 @@ export const contentStrings = (
29
32
  : data.ui;
30
33
  return {
31
34
  actions: { ...EN_UI.actions, ...dictionary.actions },
35
+ changelog: { ...EN_UI.changelog, ...dictionary.changelog },
32
36
  content: { ...EN_UI.content, ...dictionary.content },
33
37
  };
34
38
  };
@@ -29,20 +29,47 @@ export const rawHtmlAsTextPlugin = defineMdastPlugin({
29
29
  * `<p>a</p>\n<p>b</p>` would "unwrap" to `a</p>\n<p>b` — unbalanced HTML
30
30
  * injected via `set:html`.
31
31
  */
32
+ const SINGLE_PARAGRAPH = /^<p>(?<content>(?:(?!<\/p>)[\s\S])*)<\/p>$/u;
33
+
32
34
  export const unwrapParagraph = (html: string): string => {
33
35
  const trimmed = html.trim();
34
- const match = trimmed.match(/^<p>(?<content>(?:(?!<\/p>)[\s\S])*)<\/p>$/u);
35
- return match?.groups?.content ?? trimmed;
36
+ return SINGLE_PARAGRAPH.exec(trimmed)?.groups?.content ?? trimmed;
36
37
  };
37
38
 
38
- /**
39
- * Render a short Markdown text prop to inline HTML, its raw HTML shown as
40
- * text (see {@link rawHtmlAsTextPlugin}) and a single paragraph unwrapped.
41
- */
42
- export const renderInlineMarkdown = async (value: string): Promise<string> => {
39
+ /** Render Markdown to HTML, its raw HTML shown as text. */
40
+ const renderMarkdown = async (value: string): Promise<string> => {
43
41
  const processor = await createSatteriMarkdownProcessor({
44
42
  mdastPlugins: [rawHtmlAsTextPlugin],
45
43
  });
46
44
  const rendered = await processor.render(value);
47
- return unwrapParagraph(rendered.code);
45
+ return rendered.code;
46
+ };
47
+
48
+ /**
49
+ * Render a short Markdown text prop to inline HTML, its raw HTML shown as
50
+ * text (see {@link rawHtmlAsTextPlugin}) and a single paragraph unwrapped.
51
+ */
52
+ export const renderInlineMarkdown = async (value: string): Promise<string> =>
53
+ unwrapParagraph(await renderMarkdown(value));
54
+
55
+ // What inline Markdown a title can hold starts with: a backslash escape, a
56
+ // code span, emphasis or strikethrough, or a link.
57
+ const INLINE_SYNTAX = /[\\`*_~[]/u;
58
+
59
+ /**
60
+ * A title prop (a callout's, an accordion item's) as inline HTML, with its
61
+ * Markdown rendered — `` `code` ``, `**bold**`, `[links](/x)` — and its raw
62
+ * HTML shown as text. Undefined when the title renders as written instead:
63
+ * when it holds none of that syntax, so a plain title keeps the exact text it
64
+ * always rendered, and when it doesn't render as one paragraph (`1. Install`
65
+ * is no list).
66
+ */
67
+ export const renderInlineTitle = async (
68
+ value: string
69
+ ): Promise<string | undefined> => {
70
+ if (!INLINE_SYNTAX.test(value)) {
71
+ return undefined;
72
+ }
73
+ const html = await renderMarkdown(value);
74
+ return SINGLE_PARAGRAPH.exec(html.trim())?.groups?.content;
48
75
  };
@@ -8,9 +8,15 @@
8
8
  const BARE_ID = /^[\w-]{11}$/u;
9
9
 
10
10
  // Pull the id out of any common YouTube URL: youtu.be/<id>, watch?v=<id>,
11
- // /embed/<id>, /shorts/<id>, /live/<id>.
11
+ // /embed/<id>, /shorts/<id>, /live/<id>. A playlist embed's
12
+ // `/embed/videoseries` is 11 characters too, but it names no video.
12
13
  const URL_ID =
13
- /(?:youtu\.be\/|\/embed\/|\/shorts\/|\/live\/|[?&]v=)(?<id>[\w-]{11})/u;
14
+ /(?:youtu\.be\/|\/embed\/(?!videoseries\b)|\/shorts\/|\/live\/|[?&]v=)(?<id>[\w-]{11})/u;
15
+
16
+ // A playlist URL: the embed form, `/embed/videoseries?list=<id>`, or the
17
+ // playlist page, `/playlist?list=<id>`.
18
+ const PLAYLIST =
19
+ /\/(?:embed\/videoseries|playlist)\?(?:[^#]*&)?list=(?<list>[\w-]+)/u;
14
20
 
15
21
  /**
16
22
  * Resolve a YouTube video id from either a bare id or a full URL. Returns `null`
@@ -28,6 +34,18 @@ export const parseYouTubeId = (input: string): string | null => {
28
34
  return URL_ID.exec(value)?.groups?.id ?? null;
29
35
  };
30
36
 
37
+ /**
38
+ * The playlist id of a YouTube playlist URL (`/embed/videoseries?list=…` or
39
+ * `/playlist?list=…`), or `null` for anything else — a video URL that also
40
+ * carries a `list` (`watch?v=…&list=…`) is that video.
41
+ */
42
+ export const parseYouTubePlaylist = (input: string): string | null =>
43
+ PLAYLIST.exec(input.trim())?.groups?.list ?? null;
44
+
45
+ /** The query a `start` time adds to an embed URL, or nothing. */
46
+ const startParam = (start: number | undefined): Record<string, string> =>
47
+ start && start > 0 ? { start: String(Math.floor(start)) } : {};
48
+
31
49
  /**
32
50
  * Build a privacy-enhanced (`youtube-nocookie.com`) embed URL, optionally
33
51
  * starting at `start` seconds.
@@ -37,10 +55,34 @@ export const youtubeEmbedUrl = (
37
55
  options: { start?: number } = {}
38
56
  ): string => {
39
57
  const base = `https://www.youtube-nocookie.com/embed/${id}`;
40
- const { start } = options;
41
- if (start && start > 0) {
42
- const params = new URLSearchParams({ start: String(Math.floor(start)) });
43
- return `${base}?${params.toString()}`;
58
+ const params = new URLSearchParams(startParam(options.start));
59
+ return params.size > 0 ? `${base}?${params.toString()}` : base;
60
+ };
61
+
62
+ /**
63
+ * The privacy-enhanced embed URL for a playlist, optionally starting its
64
+ * first video at `start` seconds.
65
+ */
66
+ export const youtubePlaylistEmbedUrl = (
67
+ list: string,
68
+ options: { start?: number } = {}
69
+ ): string => {
70
+ const params = new URLSearchParams({ list, ...startParam(options.start) });
71
+ return `https://www.youtube-nocookie.com/embed/videoseries?${params.toString()}`;
72
+ };
73
+
74
+ /**
75
+ * The embed URL for a `<YouTube>`'s `id` or `url`: a video, else a playlist,
76
+ * else `null` when the input names neither.
77
+ */
78
+ export const youtubeEmbedSrc = (
79
+ input: string,
80
+ options: { start?: number } = {}
81
+ ): string | null => {
82
+ const id = parseYouTubeId(input);
83
+ if (id) {
84
+ return youtubeEmbedUrl(id, options);
44
85
  }
45
- return base;
86
+ const list = parseYouTubePlaylist(input);
87
+ return list ? youtubePlaylistEmbedUrl(list, options) : null;
46
88
  };
@@ -21,7 +21,11 @@ interface Props {
21
21
  }
22
22
 
23
23
  const { analytics } = Astro.props;
24
- const nodes = analyticsHead(import.meta.env.PROD ? (analytics ?? []) : []);
24
+ // A `script()` src in `public/` is served under the deployment base.
25
+ const nodes = analyticsHead(
26
+ import.meta.env.PROD ? (analytics ?? []) : [],
27
+ import.meta.env.BASE_URL
28
+ );
25
29
  const held = data.config.consent
26
30
  ? { "data-blume-consent": "analytics", type: "text/plain" }
27
31
  : {};
@@ -8,7 +8,7 @@ import { withBase, withMountedBase } from "../islands/base-path.ts";
8
8
  import Self from "./NavTree.astro";
9
9
  import NavTreeCache from "./NavTreeCache.astro";
10
10
  import NavTreeScript from "./NavTreeScript.astro";
11
- import { isGroupRowCurrent } from "./nav-utils.ts";
11
+ import { groupStartsOpen, isGroupRowCurrent } from "./nav-utils.ts";
12
12
 
13
13
  interface PagePanel {
14
14
  id: string;
@@ -388,8 +388,13 @@ const initialId =
388
388
  if (display === "group") {
389
389
  const active = hasActivePage(item);
390
390
  // Default every group closed; open only those on the active path (so
391
- // the current page's ancestors stay expanded) or forced open via meta.
392
- const open = active || item.collapsed === false;
391
+ // the current page's ancestors stay expanded), forced open via meta,
392
+ // or alone at the top of the sidebar.
393
+ const open = groupStartsOpen(
394
+ item,
395
+ active,
396
+ depth === 0 && items.length === 1
397
+ );
393
398
  return (
394
399
  <li class={spacing}>
395
400
  <details open={open}>