blume 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +133 -0
  3. package/README.md +4 -4
  4. package/dist/cli/{chunk-74wfhv79.js → chunk-05f21r8h.js} +1 -1
  5. package/dist/cli/{chunk-9qzgg6mj.js → chunk-1cm0h819.js} +3 -3
  6. package/dist/cli/{chunk-nbyvgnnm.js → chunk-1rkxzg3n.js} +104 -47
  7. package/dist/cli/chunk-1rkxzg3n.js.map +36 -0
  8. package/dist/cli/{chunk-desvm72z.js → chunk-2nr29y95.js} +22 -26
  9. package/dist/cli/chunk-2nr29y95.js.map +13 -0
  10. package/dist/cli/{chunk-32khwcb6.js → chunk-49r370mb.js} +42 -69
  11. package/dist/cli/{chunk-32khwcb6.js.map → chunk-49r370mb.js.map} +9 -10
  12. package/dist/cli/{chunk-ms9z66nz.js → chunk-4rftmttv.js} +2 -2
  13. package/dist/cli/{chunk-s0bdnc7v.js → chunk-79bzzkx4.js} +5 -4
  14. package/dist/cli/chunk-79bzzkx4.js.map +10 -0
  15. package/dist/cli/{chunk-hvcxng5f.js → chunk-7tekknev.js} +6 -6
  16. package/dist/cli/{chunk-dv2z0rzf.js → chunk-8b489v1b.js} +38 -13
  17. package/dist/cli/{chunk-dv2z0rzf.js.map → chunk-8b489v1b.js.map} +5 -4
  18. package/dist/cli/{chunk-0pmb4tr4.js → chunk-8f8xw4x2.js} +1195 -222
  19. package/dist/cli/chunk-8f8xw4x2.js.map +44 -0
  20. package/dist/cli/chunk-96snx8yy.js +329 -0
  21. package/dist/cli/chunk-96snx8yy.js.map +13 -0
  22. package/dist/cli/{chunk-ckrcm08h.js → chunk-a3ydrqny.js} +48 -62
  23. package/dist/cli/{chunk-ckrcm08h.js.map → chunk-a3ydrqny.js.map} +4 -5
  24. package/dist/cli/{chunk-bxwhtvw8.js → chunk-ahc5gc9h.js} +5 -5
  25. package/dist/cli/{chunk-dpy8qdjf.js → chunk-as09pafp.js} +73 -182
  26. package/dist/cli/chunk-as09pafp.js.map +18 -0
  27. package/dist/cli/{chunk-tfycddbt.js → chunk-bjk964fp.js} +2 -2
  28. package/dist/cli/{chunk-gfc5fqvf.js → chunk-br0n8n3e.js} +18 -18
  29. package/dist/cli/{chunk-gfc5fqvf.js.map → chunk-br0n8n3e.js.map} +1 -1
  30. package/dist/cli/{chunk-297ajdcv.js → chunk-c69tw29j.js} +6 -6
  31. package/dist/cli/{chunk-297ajdcv.js.map → chunk-c69tw29j.js.map} +3 -3
  32. package/dist/cli/chunk-cn3dy3hz.js +146 -0
  33. package/dist/cli/chunk-cn3dy3hz.js.map +11 -0
  34. package/dist/cli/{chunk-124fvmzq.js → chunk-dm4mg9bj.js} +11064 -8742
  35. package/dist/cli/{chunk-124fvmzq.js.map → chunk-dm4mg9bj.js.map} +78 -64
  36. package/dist/cli/{chunk-dqhg54mr.js → chunk-dmx6cezx.js} +1 -1
  37. package/dist/cli/{chunk-60hz265r.js → chunk-dpcahcrb.js} +24 -11
  38. package/dist/cli/chunk-dpcahcrb.js.map +10 -0
  39. package/dist/cli/{chunk-a57yh0h9.js → chunk-dyk3r7jw.js} +7 -7
  40. package/dist/cli/{chunk-a57yh0h9.js.map → chunk-dyk3r7jw.js.map} +1 -1
  41. package/dist/cli/{chunk-twy7psdz.js → chunk-enk76qd5.js} +14 -10
  42. package/dist/cli/chunk-enk76qd5.js.map +10 -0
  43. package/dist/cli/{chunk-k3gr1nm9.js → chunk-fqy4n3q2.js} +13 -16
  44. package/dist/cli/{chunk-k3gr1nm9.js.map → chunk-fqy4n3q2.js.map} +3 -3
  45. package/dist/cli/{chunk-sc93apfs.js → chunk-frk2wdq1.js} +17 -17
  46. package/dist/cli/{chunk-sc93apfs.js.map → chunk-frk2wdq1.js.map} +2 -2
  47. package/dist/cli/{chunk-kdpzw38a.js → chunk-gprwej7h.js} +95 -7
  48. package/dist/cli/chunk-gprwej7h.js.map +11 -0
  49. package/dist/cli/chunk-kkrteb15.js +157 -0
  50. package/dist/cli/chunk-kkrteb15.js.map +10 -0
  51. package/dist/cli/{chunk-ave1wcbr.js → chunk-m9bp2hc1.js} +1 -1
  52. package/dist/cli/{chunk-58bt3hag.js → chunk-mrvh4c88.js} +4 -4
  53. package/dist/cli/{chunk-5cmts6dv.js → chunk-p8zg3a2h.js} +7 -7
  54. package/dist/cli/{chunk-5cmts6dv.js.map → chunk-p8zg3a2h.js.map} +1 -1
  55. package/dist/cli/{chunk-0w6hs479.js → chunk-rkqae7kh.js} +30 -18
  56. package/dist/cli/chunk-rkqae7kh.js.map +10 -0
  57. package/dist/cli/{chunk-bqxaqn5p.js → chunk-t5xx74rd.js} +8 -8
  58. package/dist/cli/{chunk-bqxaqn5p.js.map → chunk-t5xx74rd.js.map} +1 -1
  59. package/dist/cli/{chunk-1f882900.js → chunk-th0pgaj5.js} +2 -2
  60. package/dist/cli/{chunk-dhwtqnnd.js → chunk-vnbvhgmd.js} +2 -2
  61. package/dist/cli/{chunk-gxtg6ed2.js → chunk-vzy9rh6y.js} +21 -21
  62. package/dist/cli/{chunk-gxtg6ed2.js.map → chunk-vzy9rh6y.js.map} +3 -3
  63. package/dist/cli/{chunk-j1x92mx9.js → chunk-w6e455pz.js} +1 -1
  64. package/dist/cli/chunk-wehv26ry.js +155 -0
  65. package/dist/cli/chunk-wehv26ry.js.map +11 -0
  66. package/dist/cli/{chunk-y4msrwm9.js → chunk-xgq4eytj.js} +15 -15
  67. package/dist/cli/{chunk-36bxs9c7.js → chunk-xt0qvz1t.js} +27 -23
  68. package/dist/cli/chunk-xt0qvz1t.js.map +10 -0
  69. package/dist/cli/index.js +78 -42
  70. package/dist/cli/index.js.map +7 -6
  71. package/dist/types/ai/changelog-components.d.ts +18 -0
  72. package/dist/types/ai/changelog-markdown.d.ts +12 -0
  73. package/dist/types/ai/openapi-components.d.ts +7 -7
  74. package/dist/types/ai/operation-details.d.ts +39 -0
  75. package/dist/types/ai/relative-links.d.ts +3 -1
  76. package/dist/types/ai/serializers.d.ts +6 -4
  77. package/dist/types/analytics/head.d.ts +4 -2
  78. package/dist/types/analytics/script.d.ts +8 -2
  79. package/dist/types/components/content/youtube.d.ts +20 -0
  80. package/dist/types/components/layout/nav-utils.d.ts +8 -0
  81. package/dist/types/components/openapi/example-panels.d.ts +33 -0
  82. package/dist/types/components/openapi/helpers.d.ts +53 -11
  83. package/dist/types/components/openapi/snippets.d.ts +7 -1
  84. package/dist/types/core/alert-diagnostics.d.ts +10 -0
  85. package/dist/types/core/builtin-tags.d.ts +16 -0
  86. package/dist/types/core/changelog-list.d.ts +48 -0
  87. package/dist/types/core/closest-match.d.ts +6 -0
  88. package/dist/types/core/code-fence-diagnostics.d.ts +18 -0
  89. package/dist/types/core/code-fences.d.ts +8 -1
  90. package/dist/types/core/config-input.d.ts +1 -1
  91. package/dist/types/core/content-assets.d.ts +16 -18
  92. package/dist/types/core/diagnostics.d.ts +14 -0
  93. package/dist/types/core/directive-diagnostics.d.ts +22 -8
  94. package/dist/types/core/former-routes.d.ts +14 -0
  95. package/dist/types/core/github.d.ts +5 -4
  96. package/dist/types/core/heading-markers.d.ts +65 -2
  97. package/dist/types/core/hidden-pages.d.ts +3 -0
  98. package/dist/types/core/includes.d.ts +23 -0
  99. package/dist/types/core/last-modified.d.ts +18 -6
  100. package/dist/types/core/links.d.ts +42 -2
  101. package/dist/types/core/load-module.d.ts +19 -4
  102. package/dist/types/core/manifest.d.ts +14 -1
  103. package/dist/types/core/markdown-mirrors.d.ts +22 -0
  104. package/dist/types/core/mdx-elements.d.ts +32 -0
  105. package/dist/types/core/mdx-syntax.d.ts +100 -0
  106. package/dist/types/core/meta-diagnostics.d.ts +7 -0
  107. package/dist/types/core/meta.d.ts +32 -5
  108. package/dist/types/core/nav-diagnostics.d.ts +9 -2
  109. package/dist/types/core/navigation.d.ts +13 -0
  110. package/dist/types/core/ordering-prefix.d.ts +6 -0
  111. package/dist/types/core/pinned-ids.d.ts +23 -0
  112. package/dist/types/core/project-graph.d.ts +9 -1
  113. package/dist/types/core/redirect-patterns.d.ts +18 -0
  114. package/dist/types/core/relative-files.d.ts +31 -0
  115. package/dist/types/core/schema.d.ts +267 -7
  116. package/dist/types/core/sources/normalize.d.ts +101 -9
  117. package/dist/types/core/sources/types.d.ts +16 -0
  118. package/dist/types/core/static-files.d.ts +10 -0
  119. package/dist/types/core/static-host.d.ts +42 -0
  120. package/dist/types/core/svg-images.d.ts +26 -0
  121. package/dist/types/core/syntax-diagnostics.d.ts +14 -0
  122. package/dist/types/core/types.d.ts +42 -1
  123. package/dist/types/deploy/generated-files.d.ts +41 -0
  124. package/dist/types/deploy/redirects.d.ts +21 -7
  125. package/dist/types/markdown/code-title.d.ts +9 -5
  126. package/dist/types/markdown/directives.d.ts +8 -0
  127. package/dist/types/markdown/fence-language.d.ts +51 -0
  128. package/dist/types/markdown/fence-meta.d.ts +5 -0
  129. package/dist/types/markdown/github-alerts.d.ts +35 -0
  130. package/dist/types/openapi/asyncapi.d.ts +6 -5
  131. package/dist/types/openapi/checks.d.ts +10 -5
  132. package/dist/types/openapi/model.d.ts +55 -8
  133. package/dist/types/openapi/parse.d.ts +4 -1
  134. package/dist/types/sources/filesystem.d.ts +14 -4
  135. package/dist/types/sources/mdx-remote.d.ts +6 -3
  136. package/dist/types/sources/registry.d.ts +3 -3
  137. package/dist/types/theme/color.d.ts +33 -0
  138. package/dist/types/theme/palette.d.ts +122 -0
  139. package/docs/01-quickstart.mdx +1 -1
  140. package/docs/02-deployment.mdx +23 -8
  141. package/docs/04-migrating.mdx +16 -4
  142. package/docs/advanced/changelog.mdx +27 -1
  143. package/docs/advanced/skills.mdx +1 -1
  144. package/docs/cli/audit.mdx +1 -1
  145. package/docs/cli/doctor.mdx +2 -2
  146. package/docs/cli/evals.mdx +1 -1
  147. package/docs/cli/index.mdx +4 -2
  148. package/docs/cli/validate.mdx +10 -7
  149. package/docs/configuration/analytics.mdx +2 -0
  150. package/docs/configuration/customization.mdx +3 -1
  151. package/docs/configuration/index.mdx +2 -2
  152. package/docs/configuration/theming.mdx +5 -1
  153. package/docs/content/components.mdx +26 -6
  154. package/docs/content/frontmatter.mdx +1 -1
  155. package/docs/content/includes.mdx +6 -2
  156. package/docs/content/index.mdx +11 -1
  157. package/docs/content/islands.mdx +2 -0
  158. package/docs/content/meta.mdx +9 -3
  159. package/docs/content/navigation.mdx +14 -6
  160. package/docs/content/sources/obsidian.mdx +1 -1
  161. package/docs/content/sources/remote-mdx.mdx +3 -1
  162. package/docs/content/syntax.mdx +76 -8
  163. package/docs/content/variables.mdx +3 -1
  164. package/docs/discoverability/agent-discovery.mdx +1 -1
  165. package/docs/discoverability/json-api.mdx +1 -1
  166. package/docs/discoverability/llms-txt.mdx +1 -1
  167. package/docs/discoverability/markdown.mdx +2 -2
  168. package/docs/discoverability/open-graph.mdx +1 -1
  169. package/docs/discoverability/sitemap-and-robots.mdx +1 -1
  170. package/docs/references/graphql.mdx +2 -0
  171. package/docs/references/openapi.mdx +45 -7
  172. package/package.json +10 -9
  173. package/skills/blume/SKILL.md +2 -2
  174. package/skills/blume-migrate/SKILL.md +49 -27
  175. package/skills/blume-migrate/references/docsify.md +262 -0
  176. package/skills/blume-migrate/references/docus.md +298 -0
  177. package/skills/blume-migrate/references/docusaurus.md +3 -3
  178. package/skills/blume-migrate/references/fern.md +327 -0
  179. package/skills/blume-migrate/references/fumadocs.md +1 -1
  180. package/skills/blume-migrate/references/gitbook.md +267 -0
  181. package/skills/blume-migrate/references/github-wiki.md +259 -0
  182. package/skills/blume-migrate/references/jekyll.md +253 -0
  183. package/skills/blume-migrate/references/mdbook.md +256 -0
  184. package/skills/blume-migrate/references/mintlify.md +5 -5
  185. package/skills/blume-migrate/references/mkdocs.md +236 -0
  186. package/skills/blume-migrate/references/monorepo.md +20 -8
  187. package/skills/blume-migrate/references/nextra.md +2 -2
  188. package/skills/blume-migrate/references/readme.md +257 -0
  189. package/skills/blume-migrate/references/redocly.md +198 -0
  190. package/skills/blume-migrate/references/starlight.md +3 -3
  191. package/skills/blume-migrate/references/vitepress.md +204 -0
  192. package/skills/blume-migrate/references/vuepress.md +271 -0
  193. package/skills/blume-migrate/scripts/docsify-codemod.mjs +2387 -0
  194. package/skills/blume-migrate/scripts/docus-codemod.mjs +2542 -0
  195. package/skills/blume-migrate/scripts/fern-codemod.mjs +2633 -0
  196. package/skills/blume-migrate/scripts/github-wiki-codemod.mjs +2808 -0
  197. package/skills/blume-migrate/scripts/include-excerpts.mjs +649 -0
  198. package/skills/blume-migrate/scripts/jekyll-codemod.mjs +3649 -0
  199. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +17 -13
  200. package/skills/blume-migrate/scripts/mkdocs-codemod.mjs +2526 -0
  201. package/skills/blume-migrate/scripts/operation-routes.mjs +508 -0
  202. package/skills/blume-migrate/scripts/pin-heading-ids.mjs +1049 -0
  203. package/skills/blume-migrate/scripts/readme-codemod.mjs +4139 -0
  204. package/skills/blume-migrate/scripts/vitepress-codemod.mjs +1511 -0
  205. package/src/ai/changelog-components.ts +24 -0
  206. package/src/ai/changelog-markdown.ts +45 -41
  207. package/src/ai/component-markdown.ts +22 -9
  208. package/src/ai/llms.ts +36 -5
  209. package/src/ai/markdown.ts +2 -2
  210. package/src/ai/openapi-components.ts +25 -9
  211. package/src/ai/operation-details.ts +208 -0
  212. package/src/ai/relative-links.ts +48 -8
  213. package/src/ai/serializers.ts +7 -6
  214. package/src/ai/site-skill.ts +6 -2
  215. package/src/analytics/head.ts +11 -5
  216. package/src/analytics/script.ts +13 -3
  217. package/src/astro/generate.ts +32 -6
  218. package/src/astro/index.ts +1 -0
  219. package/src/astro/integration.ts +27 -1
  220. package/src/astro/mdx-source-errors.ts +46 -0
  221. package/src/astro/templates.ts +89 -185
  222. package/src/audit/checks/redirects.ts +5 -1
  223. package/src/audit/redirects.ts +36 -19
  224. package/src/audit/report.ts +22 -9
  225. package/src/audit/run.ts +12 -28
  226. package/src/audit/terms.ts +23 -0
  227. package/src/audit/url.ts +2 -2
  228. package/src/cli/build-failure.ts +256 -7
  229. package/src/cli/command-meta.ts +1 -1
  230. package/src/cli/commands/audit.ts +1 -1
  231. package/src/cli/commands/build.ts +12 -3
  232. package/src/cli/commands/check.ts +13 -2
  233. package/src/cli/commands/dev.ts +1 -1
  234. package/src/cli/commands/doctor.ts +4 -2
  235. package/src/cli/commands/init.ts +3 -3
  236. package/src/cli/commands/preview.ts +17 -1
  237. package/src/cli/commands/validate.ts +22 -10
  238. package/src/cli/prepare.ts +1 -1
  239. package/src/cli/preview-pages.ts +161 -0
  240. package/src/cli/unknown-flags.ts +1 -42
  241. package/src/components/code-copy.ts +51 -0
  242. package/src/components/code-language.ts +75 -0
  243. package/src/components/content/AccordionItem.astro +16 -1
  244. package/src/components/content/Callout.astro +11 -1
  245. package/src/components/content/Changelog.astro +189 -0
  246. package/src/components/content/CodeBlock.astro +8 -11
  247. package/src/components/content/Tabs.astro +16 -2
  248. package/src/components/content/YouTube.astro +6 -4
  249. package/src/components/content/code-block.ts +33 -0
  250. package/src/components/content/content-strings.ts +5 -1
  251. package/src/components/content/inline-markdown.ts +35 -8
  252. package/src/components/content/youtube.ts +49 -7
  253. package/src/components/layout/Analytics.astro +5 -1
  254. package/src/components/layout/NavTree.astro +8 -3
  255. package/src/components/layout/RootLayout.astro +18 -31
  256. package/src/components/layout/nav-utils.ts +14 -0
  257. package/src/components/layout/search/pagefind.ts +2 -1
  258. package/src/components/openapi/Operation.astro +4 -0
  259. package/src/components/openapi/PanelTabs.astro +9 -3
  260. package/src/components/openapi/RequestBody.astro +42 -8
  261. package/src/components/openapi/RequestPanel.astro +34 -53
  262. package/src/components/openapi/example-panels.ts +110 -0
  263. package/src/components/openapi/helpers.ts +195 -20
  264. package/src/components/openapi/operation-model.ts +63 -26
  265. package/src/components/openapi/request.ts +13 -0
  266. package/src/components/openapi/snippets.ts +14 -2
  267. package/src/core/alert-diagnostics.ts +89 -0
  268. package/src/core/builtin-tags.ts +76 -0
  269. package/src/core/changelog-list.ts +87 -0
  270. package/src/core/closest-match.ts +41 -0
  271. package/src/core/code-fence-diagnostics.ts +267 -0
  272. package/src/core/code-fences.ts +43 -7
  273. package/src/core/component-diagnostics.ts +65 -12
  274. package/src/core/config-input.ts +1 -1
  275. package/src/core/config.ts +46 -1
  276. package/src/core/content-assets.ts +64 -144
  277. package/src/core/diagnostics.ts +36 -3
  278. package/src/core/directive-diagnostics.ts +265 -47
  279. package/src/core/former-routes.ts +46 -0
  280. package/src/core/git-ignored.ts +53 -0
  281. package/src/core/github.ts +5 -4
  282. package/src/core/heading-markers.ts +156 -2
  283. package/src/core/hidden-pages.ts +64 -0
  284. package/src/core/includes.ts +181 -41
  285. package/src/core/last-modified.ts +47 -10
  286. package/src/core/links.ts +438 -83
  287. package/src/core/load-module.ts +98 -9
  288. package/src/core/manifest.ts +12 -9
  289. package/src/core/markdown-mirrors.ts +52 -0
  290. package/src/core/mdx-elements.ts +153 -0
  291. package/src/core/mdx-syntax.ts +377 -0
  292. package/src/core/meta-diagnostics.ts +134 -0
  293. package/src/core/meta.ts +154 -55
  294. package/src/core/nav-diagnostics.ts +16 -4
  295. package/src/core/navigation.ts +70 -10
  296. package/src/core/ordering-prefix.ts +22 -2
  297. package/src/core/pinned-ids.ts +141 -0
  298. package/src/core/project-graph.ts +221 -29
  299. package/src/core/redirect-patterns.ts +41 -0
  300. package/src/core/relative-files.ts +145 -0
  301. package/src/core/schema.ts +17 -3
  302. package/src/core/sources/mdx-remote.ts +75 -14
  303. package/src/core/sources/normalize.ts +885 -179
  304. package/src/core/sources/notion.ts +5 -4
  305. package/src/core/sources/obsidian.ts +2 -5
  306. package/src/core/sources/types.ts +17 -0
  307. package/src/core/static-files.ts +40 -0
  308. package/src/core/static-host.ts +125 -0
  309. package/src/core/svg-images.ts +98 -0
  310. package/src/core/syntax-diagnostics.ts +491 -0
  311. package/src/core/types.ts +43 -1
  312. package/src/deploy/artifacts.ts +2 -28
  313. package/src/deploy/generated-files.ts +128 -0
  314. package/src/deploy/platforms/vercel.ts +25 -1
  315. package/src/deploy/redirects.ts +43 -16
  316. package/src/deploy/sitemap.ts +2 -1
  317. package/src/markdown/base-links.ts +87 -6
  318. package/src/markdown/code-title.ts +29 -13
  319. package/src/markdown/content-assets.ts +210 -0
  320. package/src/markdown/directives.ts +69 -5
  321. package/src/markdown/fence-language.ts +106 -0
  322. package/src/markdown/fence-meta.ts +42 -0
  323. package/src/markdown/github-alerts.ts +172 -0
  324. package/src/markdown/heading-anchors.ts +265 -56
  325. package/src/markdown/include.ts +30 -3
  326. package/src/markdown/index.ts +145 -40
  327. package/src/markdown/prompt-text.ts +95 -0
  328. package/src/markdown/relative-links.ts +37 -2
  329. package/src/markdown/route-snapshot.ts +22 -12
  330. package/src/markdown/svg-fallback.ts +51 -0
  331. package/src/markdown/title-heading.ts +134 -0
  332. package/src/migrate/migrate.ts +127 -2
  333. package/src/og/card.ts +14 -4
  334. package/src/openapi/asyncapi.ts +14 -6
  335. package/src/openapi/checks.ts +160 -15
  336. package/src/openapi/model.ts +172 -33
  337. package/src/openapi/parse.ts +13 -4
  338. package/src/openapi/render-mdx.ts +55 -9
  339. package/src/openapi/source.ts +87 -6
  340. package/src/registry/eject.ts +0 -1
  341. package/src/search/documents.ts +10 -2
  342. package/src/sources/filesystem.ts +28 -3
  343. package/src/sources/mdx-remote.ts +7 -15
  344. package/src/theme/color.ts +14 -0
  345. package/src/theme/entry.ts +22 -1
  346. package/src/theme/palette.ts +74 -1
  347. package/dist/cli/chunk-0pmb4tr4.js.map +0 -35
  348. package/dist/cli/chunk-0w6hs479.js.map +0 -10
  349. package/dist/cli/chunk-150jk7km.js +0 -52
  350. package/dist/cli/chunk-150jk7km.js.map +0 -10
  351. package/dist/cli/chunk-36bxs9c7.js.map +0 -10
  352. package/dist/cli/chunk-53jqw8z0.js +0 -89
  353. package/dist/cli/chunk-53jqw8z0.js.map +0 -10
  354. package/dist/cli/chunk-60hz265r.js.map +0 -10
  355. package/dist/cli/chunk-desvm72z.js.map +0 -14
  356. package/dist/cli/chunk-dpy8qdjf.js.map +0 -17
  357. package/dist/cli/chunk-es8h0v3j.js +0 -441
  358. package/dist/cli/chunk-es8h0v3j.js.map +0 -11
  359. package/dist/cli/chunk-kdpzw38a.js.map +0 -11
  360. package/dist/cli/chunk-nbyvgnnm.js.map +0 -36
  361. package/dist/cli/chunk-s0bdnc7v.js.map +0 -10
  362. package/dist/cli/chunk-twy7psdz.js.map +0 -10
  363. package/dist/cli/chunk-ydp3065p.js +0 -64
  364. package/dist/cli/chunk-ydp3065p.js.map +0 -10
  365. /package/dist/cli/{chunk-74wfhv79.js.map → chunk-05f21r8h.js.map} +0 -0
  366. /package/dist/cli/{chunk-9qzgg6mj.js.map → chunk-1cm0h819.js.map} +0 -0
  367. /package/dist/cli/{chunk-ms9z66nz.js.map → chunk-4rftmttv.js.map} +0 -0
  368. /package/dist/cli/{chunk-hvcxng5f.js.map → chunk-7tekknev.js.map} +0 -0
  369. /package/dist/cli/{chunk-bxwhtvw8.js.map → chunk-ahc5gc9h.js.map} +0 -0
  370. /package/dist/cli/{chunk-tfycddbt.js.map → chunk-bjk964fp.js.map} +0 -0
  371. /package/dist/cli/{chunk-dqhg54mr.js.map → chunk-dmx6cezx.js.map} +0 -0
  372. /package/dist/cli/{chunk-ave1wcbr.js.map → chunk-m9bp2hc1.js.map} +0 -0
  373. /package/dist/cli/{chunk-58bt3hag.js.map → chunk-mrvh4c88.js.map} +0 -0
  374. /package/dist/cli/{chunk-1f882900.js.map → chunk-th0pgaj5.js.map} +0 -0
  375. /package/dist/cli/{chunk-dhwtqnnd.js.map → chunk-vnbvhgmd.js.map} +0 -0
  376. /package/dist/cli/{chunk-j1x92mx9.js.map → chunk-w6e455pz.js.map} +0 -0
  377. /package/dist/cli/{chunk-y4msrwm9.js.map → chunk-xgq4eytj.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}>
@@ -910,14 +910,16 @@ const localeRedirect = localeTargets
910
910
  <slot />
911
911
  </article>
912
912
  {
913
- !isApiOperation && showChrome && (
913
+ showChrome && (
914
914
  <div
915
915
  class:list={[
916
916
  // Aligned with the content column, not centered under the
917
- // prose measure: a rail page's, a wide page's, or a center
918
- // page's wider one.
919
- apiRail && "xl:pe-[30.5rem]",
920
- (apiRail || modeLayout.width === "full") && "[&>*]:max-w-none",
917
+ // prose measure: an API page's (beside its example or
918
+ // request column), a wide page's, or a center page's wider
919
+ // one.
920
+ isApiLayout && "xl:pe-[30.5rem]",
921
+ (isApiLayout || modeLayout.width === "full") &&
922
+ "[&>*]:max-w-none",
921
923
  modeLayout.width === "center" && "[&>*]:max-w-[54rem]",
922
924
  ]}
923
925
  data-blume-page-end
@@ -1005,6 +1007,8 @@ const localeRedirect = localeTargets
1005
1007
  ></button>
1006
1008
  <script>
1007
1009
  import type { Zoom } from "medium-zoom";
1010
+ import { copyableCode } from "../code-copy.ts";
1011
+ import { languageLabel } from "../code-language.ts";
1008
1012
  import { copyText, createCopyFlash } from "../copy-feedback.ts";
1009
1013
  import { syncDrawerInert } from "./drawer-inert.ts";
1010
1014
  import { chromeIcons as icons } from "../../theme/chrome-icons.ts";
@@ -1081,27 +1085,6 @@ const localeRedirect = localeTargets
1081
1085
  return panel.matches("pre") ? panel : panel.querySelector("pre");
1082
1086
  };
1083
1087
 
1084
- const languageLabels: Record<string, string> = {
1085
- astro: "Astro",
1086
- bash: "Bash",
1087
- css: "CSS",
1088
- html: "HTML",
1089
- js: "JavaScript",
1090
- json: "JSON",
1091
- jsx: "JSX",
1092
- md: "Markdown",
1093
- mdx: "MDX",
1094
- plaintext: "Text",
1095
- sh: "Shell",
1096
- shell: "Shell",
1097
- ts: "TypeScript",
1098
- tsx: "TSX",
1099
- txt: "Text",
1100
- yaml: "YAML",
1101
- yml: "YAML",
1102
- zsh: "Zsh",
1103
- };
1104
-
1105
1088
  // Per-page setup, run on the initial load and again after every
1106
1089
  // client-router swap (which replaces the body with server-rendered
1107
1090
  // markup that has none of this applied). Everything in here binds to
@@ -1176,7 +1159,14 @@ const localeRedirect = localeTargets
1176
1159
  };
1177
1160
  const flash = createCopyFlash(setChecked, copiedLabel);
1178
1161
  button.addEventListener("click", async () => {
1179
- if (await copyText(codeText(readPre()))) {
1162
+ // A terminal session copies its commands alone (see
1163
+ // code-copy.ts), keyed off the fence's own language id.
1164
+ const pre = readPre();
1165
+ const language =
1166
+ pre instanceof HTMLElement
1167
+ ? (pre.dataset.blumeLanguage ?? pre.dataset.language)
1168
+ : undefined;
1169
+ if (await copyText(copyableCode(codeText(pre), language))) {
1180
1170
  flash();
1181
1171
  }
1182
1172
  });
@@ -1277,10 +1267,7 @@ const localeRedirect = localeTargets
1277
1267
  if (pre instanceof HTMLElement) {
1278
1268
  pre.dataset.blumeLanguage = language;
1279
1269
  }
1280
- pre.setAttribute(
1281
- "data-language",
1282
- languageLabels[language.toLowerCase()] ?? language
1283
- );
1270
+ pre.setAttribute("data-language", languageLabel(language));
1284
1271
  }
1285
1272
  pre.classList.add("group", "relative");
1286
1273
  // The code element is the scroll container (see the theme entry), but
@@ -273,6 +273,20 @@ export const getPagination = (flat: FlatPage[], route: string) => {
273
273
  /** A group node in the sidebar tree. */
274
274
  export type NavGroup = Extract<NavNode, { kind: "group" }>;
275
275
 
276
+ /**
277
+ * Whether a `group`-mode disclosure renders open: it holds the current page,
278
+ * its meta forces it open (`collapsed: false`), or it is the only row at the
279
+ * top of the sidebar, where starting closed would leave a sidebar of one row
280
+ * to click before any link shows (a wrapper folder around every page). An
281
+ * explicit `collapsed: true` keeps that lone group closed.
282
+ */
283
+ export const groupStartsOpen = (
284
+ group: NavGroup,
285
+ active: boolean,
286
+ lone: boolean
287
+ ): boolean =>
288
+ active || group.collapsed === false || (lone && group.collapsed !== true);
289
+
276
290
  /**
277
291
  * The group whose own page `route` is, when that page lists the group's
278
292
  * pages: its `directory` (inherited when the group sets none) is `card` or
@@ -32,7 +32,8 @@ interface PagefindEntry {
32
32
 
33
33
  // Pagefind names each page after its built file (`quickstart/index.html`), so
34
34
  // its URLs end in a slash. Blume serves pages without one (`trailingSlash:
35
- // "never"`): hosts redirect the slashed URL, and `blume preview` 404s it.
35
+ // "never"`): Vercel, `blume dev`, and `blume preview` redirect the slashed
36
+ // URL, so linking it costs a hop.
36
37
  const TRAILING_SLASH = /(?<=.)\/(?=[#?]|$)/u;
37
38
 
38
39
  /**