blume 2.0.2 → 2.0.3

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 (293) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/cli/{chunk-6k4ftwze.js → chunk-1d7ve1dm.js} +1 -1
  3. package/dist/cli/{chunk-zp79m0ts.js → chunk-2eytanqx.js} +2 -2
  4. package/dist/cli/{chunk-8g8ytmgx.js → chunk-2hsdwb9n.js} +19 -19
  5. package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-2hsdwb9n.js.map} +1 -1
  6. package/dist/cli/{chunk-g698a744.js → chunk-2z928egk.js} +5 -5
  7. package/dist/cli/{chunk-ppzjqwx2.js → chunk-35d4wj9f.js} +17 -18
  8. package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-35d4wj9f.js.map} +3 -3
  9. package/dist/cli/{chunk-mqc662a6.js → chunk-364znk6q.js} +2 -2
  10. package/dist/cli/{chunk-gs7r695n.js → chunk-3em5wd2y.js} +21 -8
  11. package/dist/cli/{chunk-gs7r695n.js.map → chunk-3em5wd2y.js.map} +3 -3
  12. package/dist/cli/{chunk-k7pj68a8.js → chunk-5f86nr5m.js} +15 -15
  13. package/dist/cli/{chunk-hr8ne106.js → chunk-5m5nmvyq.js} +68 -43
  14. package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
  15. package/dist/cli/{chunk-91ws1n6j.js → chunk-5xvm6tfj.js} +14 -14
  16. package/dist/cli/{chunk-w4bxdvsa.js → chunk-6dsbexzp.js} +14 -14
  17. package/dist/cli/{chunk-00gs3wqs.js → chunk-8ktnccpt.js} +1 -1
  18. package/dist/cli/{chunk-j85scx15.js → chunk-9t7a85s3.js} +130 -44
  19. package/dist/cli/chunk-9t7a85s3.js.map +10 -0
  20. package/dist/cli/{chunk-bbnwccaz.js → chunk-a58773jm.js} +2 -2
  21. package/dist/cli/{chunk-nfcyttvj.js → chunk-acanzt5p.js} +9 -9
  22. package/dist/cli/{chunk-nfcyttvj.js.map → chunk-acanzt5p.js.map} +1 -1
  23. package/dist/cli/{chunk-wdrt2k2v.js → chunk-akbpwfxc.js} +90 -26
  24. package/dist/cli/chunk-akbpwfxc.js.map +10 -0
  25. package/dist/cli/{chunk-sqw4ekg1.js → chunk-b07cmahc.js} +2 -2
  26. package/dist/cli/{chunk-xh43dwgw.js → chunk-c8chx29p.js} +42 -29
  27. package/dist/cli/{chunk-xh43dwgw.js.map → chunk-c8chx29p.js.map} +9 -9
  28. package/dist/cli/{chunk-7mbqtmgb.js → chunk-crgn1q09.js} +21 -12
  29. package/dist/cli/chunk-crgn1q09.js.map +10 -0
  30. package/dist/cli/chunk-e04dxsz1.js +39 -0
  31. package/dist/cli/chunk-e04dxsz1.js.map +10 -0
  32. package/dist/cli/{chunk-4e9b9ra6.js → chunk-ey84smr6.js} +3 -3
  33. package/dist/cli/{chunk-d1v5rhy0.js → chunk-g4hq16wv.js} +13 -13
  34. package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
  35. package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
  36. package/dist/cli/{chunk-pbg5a4s3.js → chunk-hm3vjy5s.js} +64 -45
  37. package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
  38. package/dist/cli/{chunk-5r8g91qn.js → chunk-j85vccga.js} +306 -107
  39. package/dist/cli/chunk-j85vccga.js.map +36 -0
  40. package/dist/cli/{chunk-n1yg3tj3.js → chunk-p3v96n38.js} +6 -6
  41. package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-p3v96n38.js.map} +3 -3
  42. package/dist/cli/{chunk-7vtckvaw.js → chunk-p73c0m7w.js} +14 -14
  43. package/dist/cli/{chunk-7vtckvaw.js.map → chunk-p73c0m7w.js.map} +1 -1
  44. package/dist/cli/{chunk-qkb5a8sa.js → chunk-pehfxfta.js} +3 -3
  45. package/dist/cli/{chunk-v6ya5kcb.js → chunk-pv29h0wf.js} +1223 -978
  46. package/dist/cli/{chunk-v6ya5kcb.js.map → chunk-pv29h0wf.js.map} +47 -46
  47. package/dist/cli/{chunk-6dtt0zfn.js → chunk-q58y5e6a.js} +10 -10
  48. package/dist/cli/{chunk-6dtt0zfn.js.map → chunk-q58y5e6a.js.map} +3 -3
  49. package/dist/cli/{chunk-fsmrqk8a.js → chunk-r20tn01b.js} +1 -1
  50. package/dist/cli/{chunk-bfwp9vp6.js → chunk-r9rcc4w7.js} +7 -7
  51. package/dist/cli/{chunk-h7k3nq3v.js → chunk-tkacnehg.js} +2 -2
  52. package/dist/cli/{chunk-h2ez8dzb.js → chunk-tzmab476.js} +4 -4
  53. package/dist/cli/{chunk-3yce002v.js → chunk-vg9r4eb9.js} +6 -6
  54. package/dist/cli/{chunk-3yce002v.js.map → chunk-vg9r4eb9.js.map} +4 -4
  55. package/dist/cli/{chunk-hqp2ajnh.js → chunk-wrr3j9w9.js} +16 -17
  56. package/dist/cli/{chunk-hqp2ajnh.js.map → chunk-wrr3j9w9.js.map} +7 -7
  57. package/dist/cli/{chunk-ddndchfr.js → chunk-yfyb25rh.js} +38 -26
  58. package/dist/cli/chunk-yfyb25rh.js.map +14 -0
  59. package/dist/cli/index.js +20 -18
  60. package/dist/cli/index.js.map +3 -3
  61. package/dist/types/ai/link-headers.d.ts +8 -1
  62. package/dist/types/ai/openapi-components.d.ts +5 -2
  63. package/dist/types/ai/relative-links.d.ts +9 -7
  64. package/dist/types/ai/skills.d.ts +4 -1
  65. package/dist/types/ai/tar.d.ts +1 -3
  66. package/dist/types/analytics/index.d.ts +2 -0
  67. package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
  68. package/dist/types/analytics/schema.d.ts +14 -0
  69. package/dist/types/astro/integration.d.ts +3 -2
  70. package/dist/types/core/base-path.d.ts +21 -7
  71. package/dist/types/core/config-input.d.ts +9 -7
  72. package/dist/types/core/directive-diagnostics.d.ts +12 -0
  73. package/dist/types/core/heading-markers.d.ts +5 -7
  74. package/dist/types/core/i18n.d.ts +2 -0
  75. package/dist/types/core/last-modified.d.ts +10 -0
  76. package/dist/types/core/meta.d.ts +8 -0
  77. package/dist/types/core/schema.d.ts +7 -0
  78. package/dist/types/core/sources/lower.d.ts +29 -16
  79. package/dist/types/core/sources/normalize.d.ts +13 -1
  80. package/dist/types/core/sources/watch.d.ts +12 -5
  81. package/dist/types/core/standard-schema.d.ts +5 -0
  82. package/dist/types/deploy/artifacts.d.ts +6 -4
  83. package/dist/types/deploy/headers.d.ts +5 -0
  84. package/dist/types/deploy/platforms/types.d.ts +8 -0
  85. package/dist/types/deploy/redirects.d.ts +24 -13
  86. package/dist/types/markdown/directives.d.ts +62 -0
  87. package/dist/types/markdown/features.d.ts +21 -0
  88. package/dist/types/markdown/mdast.d.ts +63 -0
  89. package/dist/types/openapi/asyncapi.d.ts +4 -2
  90. package/docs/02-deployment.mdx +4 -2
  91. package/docs/08-faq.mdx +1 -1
  92. package/docs/advanced/custom-pages.mdx +3 -3
  93. package/docs/cli/audit.mdx +17 -1
  94. package/docs/cli/evals.mdx +3 -3
  95. package/docs/cli/translate.mdx +3 -3
  96. package/docs/cli/version.mdx +1 -1
  97. package/docs/configuration/analytics.mdx +20 -1
  98. package/docs/configuration/customization.mdx +2 -0
  99. package/docs/configuration/search.mdx +1 -1
  100. package/docs/content/components.mdx +1 -1
  101. package/docs/content/frontmatter.mdx +1 -1
  102. package/docs/content/i18n.mdx +2 -0
  103. package/docs/content/index.mdx +4 -2
  104. package/docs/content/islands.mdx +1 -1
  105. package/docs/content/meta.mdx +3 -1
  106. package/docs/content/sources.mdx +1 -5
  107. package/docs/content/syntax.mdx +14 -0
  108. package/docs/content/versioning.mdx +1 -1
  109. package/docs/discoverability/agent-discovery.mdx +1 -1
  110. package/docs/discoverability/index.mdx +2 -1
  111. package/docs/discoverability/markdown.mdx +2 -2
  112. package/docs/discoverability/open-graph.mdx +5 -3
  113. package/docs/discoverability/rss.mdx +1 -1
  114. package/docs/references/asyncapi.mdx +1 -1
  115. package/docs/references/graphql.mdx +1 -1
  116. package/docs/references/openapi.mdx +5 -3
  117. package/package.json +1 -1
  118. package/skills/blume-migrate/references/fumadocs.md +1 -1
  119. package/skills/blume-migrate/references/nextra.md +1 -1
  120. package/skills/blume-migrate/scripts/mintlify-codemod.mjs +7 -2
  121. package/src/ai/agent-readability.ts +2 -2
  122. package/src/ai/ai-catalog.ts +6 -2
  123. package/src/ai/api/handlers.ts +2 -2
  124. package/src/ai/api/spec.ts +2 -2
  125. package/src/ai/api-catalog.ts +6 -2
  126. package/src/ai/changelog-markdown.ts +2 -2
  127. package/src/ai/link-headers.ts +14 -5
  128. package/src/ai/llms.ts +18 -7
  129. package/src/ai/mcp/discovery.ts +2 -2
  130. package/src/ai/mcp/query.ts +2 -2
  131. package/src/ai/mcp/server.ts +2 -2
  132. package/src/ai/openapi-components.ts +22 -6
  133. package/src/ai/relative-links.ts +42 -26
  134. package/src/ai/serializers.ts +2 -1
  135. package/src/ai/skills.ts +14 -1
  136. package/src/ai/tar.ts +139 -12
  137. package/src/analytics/head.ts +4 -0
  138. package/src/analytics/index.ts +5 -0
  139. package/src/analytics/one-dollar-stats.ts +86 -0
  140. package/src/analytics/schema.ts +2 -0
  141. package/src/astro/generate.ts +1 -1
  142. package/src/astro/include-hmr.ts +31 -12
  143. package/src/astro/include-refresh.ts +19 -8
  144. package/src/astro/integration.ts +108 -14
  145. package/src/astro/runtime-modules.ts +28 -13
  146. package/src/astro/templates.ts +81 -37
  147. package/src/audit/checks/links.ts +8 -1
  148. package/src/audit/graph.ts +3 -1
  149. package/src/audit/run.ts +12 -9
  150. package/src/audit/snapshot.ts +5 -0
  151. package/src/cli/args.ts +32 -0
  152. package/src/cli/commands/audit.ts +4 -1
  153. package/src/cli/commands/doctor.ts +3 -1
  154. package/src/cli/commands/eval.ts +19 -11
  155. package/src/cli/commands/translate.ts +9 -12
  156. package/src/cli/commands/validate.ts +3 -1
  157. package/src/cli/dev-lock.ts +157 -37
  158. package/src/cli/report-format.ts +11 -6
  159. package/src/components/colors.ts +19 -0
  160. package/src/components/content/Badge.astro +5 -3
  161. package/src/components/content/Component.astro +2 -2
  162. package/src/components/content/Tab.astro +0 -1
  163. package/src/components/content/Tabs.astro +4 -0
  164. package/src/components/content/badge-color.ts +5 -2
  165. package/src/components/content/base-href.ts +13 -36
  166. package/src/components/islands/assistant.tsx +21 -4
  167. package/src/components/islands/base-path.ts +47 -10
  168. package/src/components/islands/hooks.ts +35 -19
  169. package/src/components/islands/webmcp.ts +4 -2
  170. package/src/components/layout/Breadcrumbs.astro +5 -2
  171. package/src/components/layout/DiscoveryLinks.astro +9 -5
  172. package/src/components/layout/Header.astro +2 -2
  173. package/src/components/layout/LanguageSwitcher.astro +2 -2
  174. package/src/components/layout/NavSelector.astro +2 -2
  175. package/src/components/layout/NavTabMenu.astro +3 -3
  176. package/src/components/layout/NavTree.astro +16 -6
  177. package/src/components/layout/PageActions.astro +4 -3
  178. package/src/components/layout/PageLayout.astro +7 -6
  179. package/src/components/layout/Pagination.astro +3 -3
  180. package/src/components/layout/RootLayout.astro +5 -5
  181. package/src/components/layout/Search.astro +33 -13
  182. package/src/components/layout/VersionBanner.astro +2 -2
  183. package/src/components/layout/analytics-client.ts +12 -0
  184. package/src/components/layout/toc-active.ts +41 -0
  185. package/src/components/layout/toc-element.ts +8 -14
  186. package/src/components/openapi/ApiTagOperations.astro +2 -2
  187. package/src/components/openapi/AsyncApiOperation.astro +7 -4
  188. package/src/components/openapi/GraphqlChip.astro +2 -2
  189. package/src/components/openapi/Operation.astro +2 -0
  190. package/src/components/openapi/Playground.astro +4 -4
  191. package/src/components/openapi/RequestPanel.astro +5 -2
  192. package/src/components/openapi/SchemaTable.astro +7 -0
  193. package/src/components/openapi/async.ts +38 -6
  194. package/src/components/openapi/helpers.ts +19 -9
  195. package/src/components/openapi/message-composer.ts +6 -1
  196. package/src/components/openapi/message.ts +17 -2
  197. package/src/components/openapi/operation-model.ts +81 -11
  198. package/src/components/openapi/panel.ts +4 -2
  199. package/src/components/openapi/param-style.ts +181 -0
  200. package/src/components/openapi/playground-client.ts +73 -16
  201. package/src/components/openapi/request.ts +190 -26
  202. package/src/components/openapi/schema-tree.ts +30 -24
  203. package/src/components/openapi/snippets.ts +85 -6
  204. package/src/components/openapi/ws-client.ts +18 -2
  205. package/src/core/base-path.ts +62 -16
  206. package/src/core/config-input.ts +9 -7
  207. package/src/core/diagnostics.ts +2 -0
  208. package/src/core/directive-diagnostics.ts +99 -0
  209. package/src/core/frontmatter.ts +21 -18
  210. package/src/core/heading-markers.ts +5 -18
  211. package/src/core/i18n.ts +31 -3
  212. package/src/core/last-modified.ts +25 -3
  213. package/src/core/locale-links.ts +5 -1
  214. package/src/core/meta.ts +33 -19
  215. package/src/core/navigation.ts +28 -8
  216. package/src/core/project-graph.ts +27 -5
  217. package/src/core/schema.ts +24 -4
  218. package/src/core/sources/contentful-rich-text.ts +27 -20
  219. package/src/core/sources/filesystem.ts +20 -2
  220. package/src/core/sources/github-releases.ts +25 -6
  221. package/src/core/sources/lexical.ts +23 -18
  222. package/src/core/sources/lower.ts +201 -34
  223. package/src/core/sources/mdx-remote.ts +51 -16
  224. package/src/core/sources/normalize.ts +325 -90
  225. package/src/core/sources/notion.ts +52 -25
  226. package/src/core/sources/obsidian.ts +23 -6
  227. package/src/core/sources/portable-text.ts +38 -21
  228. package/src/core/sources/strapi-blocks.ts +20 -16
  229. package/src/core/sources/watch.ts +20 -7
  230. package/src/core/standard-schema.ts +10 -6
  231. package/src/core/version-cut.ts +52 -15
  232. package/src/deploy/artifacts.ts +27 -6
  233. package/src/deploy/cloudflare-negotiation.ts +4 -12
  234. package/src/deploy/headers.ts +8 -4
  235. package/src/deploy/node-headers.ts +1 -1
  236. package/src/deploy/platforms/cloudflare.ts +12 -3
  237. package/src/deploy/platforms/netlify.ts +1 -0
  238. package/src/deploy/platforms/node.ts +1 -0
  239. package/src/deploy/platforms/static.ts +1 -0
  240. package/src/deploy/platforms/types.ts +8 -0
  241. package/src/deploy/platforms/vercel.ts +1 -0
  242. package/src/deploy/redirects.ts +54 -18
  243. package/src/deploy/robots.ts +2 -2
  244. package/src/deploy/rss.ts +4 -3
  245. package/src/deploy/sitemap.ts +7 -5
  246. package/src/markdown/base-links.ts +34 -36
  247. package/src/markdown/directives.ts +242 -36
  248. package/src/markdown/features.ts +17 -0
  249. package/src/markdown/index.ts +7 -6
  250. package/src/markdown/mdast.ts +5 -2
  251. package/src/markdown/relative-links.ts +12 -4
  252. package/src/og/card.ts +129 -5
  253. package/src/og/derive.ts +41 -31
  254. package/src/og/index.ts +1 -0
  255. package/src/openapi/asyncapi.ts +4 -2
  256. package/src/openapi/model.ts +22 -14
  257. package/src/openapi/proxy.ts +63 -10
  258. package/src/openapi/render-mdx.ts +41 -2
  259. package/src/registry/eject.ts +181 -24
  260. package/src/search/adapters/version-scope.ts +30 -0
  261. package/src/search/documents.ts +67 -26
  262. package/src/search/popular.ts +2 -1
  263. package/src/seo/jsonld.ts +7 -3
  264. package/src/translate/meta.ts +68 -24
  265. package/src/translate/run.ts +3 -3
  266. package/src/translate/validate.ts +4 -1
  267. package/src/translate/work-list.ts +56 -17
  268. package/dist/cli/chunk-5r8g91qn.js.map +0 -34
  269. package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
  270. package/dist/cli/chunk-ddndchfr.js.map +0 -14
  271. package/dist/cli/chunk-esh98wmb.js +0 -23
  272. package/dist/cli/chunk-esh98wmb.js.map +0 -10
  273. package/dist/cli/chunk-hr8ne106.js.map +0 -13
  274. package/dist/cli/chunk-j85scx15.js.map +0 -10
  275. package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
  276. package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
  277. /package/dist/cli/{chunk-6k4ftwze.js.map → chunk-1d7ve1dm.js.map} +0 -0
  278. /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-2eytanqx.js.map} +0 -0
  279. /package/dist/cli/{chunk-g698a744.js.map → chunk-2z928egk.js.map} +0 -0
  280. /package/dist/cli/{chunk-mqc662a6.js.map → chunk-364znk6q.js.map} +0 -0
  281. /package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5f86nr5m.js.map} +0 -0
  282. /package/dist/cli/{chunk-91ws1n6j.js.map → chunk-5xvm6tfj.js.map} +0 -0
  283. /package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-6dsbexzp.js.map} +0 -0
  284. /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-8ktnccpt.js.map} +0 -0
  285. /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-a58773jm.js.map} +0 -0
  286. /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-b07cmahc.js.map} +0 -0
  287. /package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-ey84smr6.js.map} +0 -0
  288. /package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-g4hq16wv.js.map} +0 -0
  289. /package/dist/cli/{chunk-qkb5a8sa.js.map → chunk-pehfxfta.js.map} +0 -0
  290. /package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-r20tn01b.js.map} +0 -0
  291. /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-r9rcc4w7.js.map} +0 -0
  292. /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-tkacnehg.js.map} +0 -0
  293. /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-tzmab476.js.map} +0 -0
@@ -8,11 +8,11 @@
8
8
  * generated pages through aliases or relative paths. `generateRuntime` now
9
9
  * publishes them here and `runtimeModulesPlugin` serves them to Vite as
10
10
  * virtual modules, so a regeneration never round-trips the disk or waits on a
11
- * file watcher: publishing invalidates the changed modules in every live dev
12
- * server (Vite walks the importers, so the pages that render them re-evaluate
13
- * on the next request) and asks the browser to reload — the same effect a JSON
14
- * file change used to reach through the watcher, minus the write and the
15
- * watch debounce.
11
+ * file watcher: publishing invalidates the changed modules in every Vite
12
+ * environment of every live dev server (Vite walks the importers, so the
13
+ * pages that render them re-evaluate on the next request) and asks the
14
+ * browser to reload — the same effect a JSON file change used to reach
15
+ * through the watcher, minus the write and the watch debounce.
16
16
  *
17
17
  * The registry hangs off `globalThis`, not module state. On a published
18
18
  * install the CLI bundle (`dist/cli`) carries its own copy of this module,
@@ -61,18 +61,29 @@ interface RuntimeModuleNode {
61
61
  id: string | null;
62
62
  }
63
63
 
64
+ /** One Vite environment's module graph, as far as the registry touches it. */
65
+ interface RuntimeModuleGraph {
66
+ getModuleById: (id: string) => RuntimeModuleNode | undefined;
67
+ invalidateModule: (mod: RuntimeModuleNode) => void;
68
+ }
69
+
64
70
  /**
65
71
  * The Vite dev-server slice the registry touches (structurally typed, like
66
72
  * every Blume-authored Vite plugin — see `includeHmrPlugin`).
67
73
  */
68
74
  export interface RuntimeModuleServer {
75
+ /**
76
+ * Every Vite environment, each with its own module graph. `moduleGraph` on
77
+ * the server itself is Vite's backward-compatible view of only `client`
78
+ * and `ssr`, and those are not the only environments that render: with
79
+ * `@astrojs/cloudflare` the `ssr` environment runs in workerd, so Astro
80
+ * renders the prerendered pages (every content page) in its `prerender`
81
+ * environment instead.
82
+ */
83
+ environments: Record<string, { moduleGraph: RuntimeModuleGraph }>;
69
84
  httpServer?: {
70
85
  once: (event: "close", listener: () => void) => void;
71
86
  } | null;
72
- moduleGraph: {
73
- getModuleById: (id: string) => RuntimeModuleNode | undefined;
74
- invalidateModule: (mod: RuntimeModuleNode) => void;
75
- };
76
87
  ws: { send: (payload: { type: "full-reload" }) => void };
77
88
  }
78
89
 
@@ -131,10 +142,14 @@ export const publishRuntimeModules = (
131
142
  return changed;
132
143
  }
133
144
  for (const server of servers) {
134
- for (const id of changed) {
135
- const mod = server.moduleGraph.getModuleById(`${RESOLVED_PREFIX}${id}`);
136
- if (mod) {
137
- server.moduleGraph.invalidateModule(mod);
145
+ // Every environment that loaded a module holds its own copy, and any of
146
+ // them may be the one rendering pages on this adapter.
147
+ for (const { moduleGraph } of Object.values(server.environments)) {
148
+ for (const id of changed) {
149
+ const mod = moduleGraph.getModuleById(`${RESOLVED_PREFIX}${id}`);
150
+ if (mod) {
151
+ moduleGraph.invalidateModule(mod);
152
+ }
138
153
  }
139
154
  }
140
155
  server.ws.send({ type: "full-reload" });
@@ -19,9 +19,12 @@ import { deployPlatform } from "../deploy/platforms/index.ts";
19
19
  import { adapterRoot, distDir } from "../deploy/platforms/paths.ts";
20
20
  import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
21
21
  import type { OgCache } from "../og/cache.ts";
22
- import type { OgFont, OgFontFamilies } from "../og/card.ts";
22
+ import type { OgFont, OgFontFamilies, OgGoogleFont } from "../og/card.ts";
23
23
  import type { MixedbreadOptions } from "../search/adapters/mixedbread.ts";
24
- import type { ResolvedSearchAdapter } from "../search/adapters/registry.ts";
24
+ import type {
25
+ ResolvedSearchAdapter,
26
+ SearchAdapterKind,
27
+ } from "../search/adapters/registry.ts";
25
28
  import { buildFontEntries, fontLocaleCodes } from "../theme/fonts.ts";
26
29
  import { importSpecifier, wrapperPropsType } from "./component-slots.ts";
27
30
  import type { ExampleSpec } from "./examples.ts";
@@ -398,14 +401,32 @@ export interface ClientFeatures {
398
401
  /** Every feature on: what a checkout that predates the detection shipped. */
399
402
  const ALL_CLIENT_FEATURES: ClientFeatures = { epub: true, mermaid: true };
400
403
 
404
+ /**
405
+ * The npm module each browser-queried search adapter's client imports (see
406
+ * `components/layout/search/`). The `<Search>` dialog lazy-imports that client
407
+ * on first open, after the dev dep optimizer's startup run, so the first
408
+ * search of a fresh `blume dev` session discovered the library, re-ran the
409
+ * optimizer, and reloaded the page. Pagefind loads its script from the build
410
+ * output and Mixedbread queries through the server, so neither has one.
411
+ */
412
+ const SEARCH_CLIENT_DEPS: Partial<Record<SearchAdapterKind, string>> = {
413
+ algolia: "algoliasearch/lite",
414
+ flexsearch: "flexsearch",
415
+ orama: "@orama/orama",
416
+ "orama-cloud": "@oramacloud/client",
417
+ typesense: "typesense",
418
+ };
419
+
401
420
  const resolveOptimizeDeps = (options: {
402
421
  aliases: Record<string, string> | undefined;
403
422
  context: ProjectContext;
404
423
  features: ClientFeatures;
405
424
  needsReact: boolean;
406
425
  reactCompilerPath: string | null | undefined;
426
+ searchKind: SearchAdapterKind;
407
427
  }): OptimizeDepsConfig => {
408
428
  const { context, features } = options;
429
+ const searchDep = SEARCH_CLIENT_DEPS[options.searchKind];
409
430
  const optimizeDepsEntries = [
410
431
  ...(context.pagesRoot ? [`${context.pagesRoot}/**/*.astro`] : []),
411
432
  `${context.root}/islands/**/*.{jsx,svelte,tsx,vue}`,
@@ -418,6 +439,8 @@ const resolveOptimizeDeps = (options: {
418
439
  // Mermaid costs the dev server seconds at startup for nothing otherwise.
419
440
  ...(features.mermaid ? ["blume > mermaid"] : []),
420
441
  ...(features.epub ? ["blume > epub-gen-memory/bundle"] : []),
442
+ // Only the configured search adapter's client library.
443
+ ...(searchDep ? [`blume > ${searchDep}`] : []),
421
444
  // Astro's own client-router/prefetch virtual modules are deliberately NOT
422
445
  // forced in here: they read Vite `define`-injected constants
423
446
  // (__PREFETCH_PREFETCH_ALL__ and friends) that a pre-bundled copy loses,
@@ -538,7 +561,7 @@ const blumeIntegrationOptions = (options: {
538
561
  buildArtifactsRoot: ".",
539
562
  contentRoutes: options.contentRoutes,
540
563
  homeLinkHeader:
541
- buildHomeLinkHeader(options.config, options.contentRoutes) ??
564
+ buildHomeLinkHeader(options.config, options.contentRoutes, "dev") ??
542
565
  undefined,
543
566
  pages: options.pages,
544
567
  }
@@ -623,6 +646,7 @@ export const astroConfigTemplate = (options: {
623
646
  features,
624
647
  needsReact,
625
648
  reactCompilerPath: options.reactCompilerPath,
649
+ searchKind: config.search.provider.kind,
626
650
  });
627
651
 
628
652
  const {
@@ -660,7 +684,8 @@ export const astroConfigTemplate = (options: {
660
684
  const basedRedirects = applyBaseToAstroRedirects(
661
685
  config.redirects,
662
686
  config.basePath,
663
- deployment.options.base ?? ""
687
+ deployment.options.base ?? "",
688
+ new Set(contentRoutes)
664
689
  );
665
690
  const redirectsOption =
666
691
  basedRedirects.length > 0
@@ -892,6 +917,10 @@ ${userConfigSetup}export default defineConfig({
892
917
  // \`/bundle\` subpath that is actually imported: optimizing the package
893
918
  // root leaves that entry out. Production (Rollup) already handles the
894
919
  // interop, so all of this only affects dev.
920
+ //
921
+ // The search adapter's client library rides the list for the same reason
922
+ // as the compiler runtime: the search dialog lazy-imports it on first
923
+ // open, and discovering it then re-optimizes and reloads the page.
895
924
  optimizeDeps: {
896
925
  entries: ${JSON.stringify(optimizeDepsEntries)},
897
926
  include: ${JSON.stringify(optimizeDepsInclude)},
@@ -1246,9 +1275,15 @@ export const askEndpointTemplate = (
1246
1275
  "messages",
1247
1276
  ...backend.template.fields,
1248
1277
  ];
1278
+ // The request's signal aborts when the reader closes the panel mid-answer
1279
+ // (the client drops the connection): passing it on stops the model call,
1280
+ // which otherwise kept generating, and billing, to completion into a closed
1281
+ // connection. `streamText` ends an aborted call through its abort path, not
1282
+ // `onError`, so the reader leaving is never logged as a provider error.
1249
1283
  const call = ` const result = streamText({
1250
1284
  ${streamFields.join(",\n ")},
1251
1285
  ${onError}
1286
+ abortSignal: request.signal,
1252
1287
  });`;
1253
1288
  const stream = grounded
1254
1289
  ? ` const instructions =
@@ -1792,7 +1827,7 @@ export const navFragmentTemplate = (): string =>
1792
1827
  `---
1793
1828
  // Generated by Blume. Do not edit.
1794
1829
  import NavTree from "blume/components/layout/NavTree.astro";
1795
- import { withBase } from "blume/components/islands/base-path.ts";
1830
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
1796
1831
  import {
1797
1832
  hiddenDefaultLocale,
1798
1833
  navGroupIds,
@@ -1831,7 +1866,7 @@ if (!(variant && node && node.kind === "group")) {
1831
1866
  }
1832
1867
  const strings =
1833
1868
  locale === "default" ? data.ui.nav : (data.uiByLocale[locale] ?? data.ui).nav;
1834
- const fragmentBase = withBase(\`/blume-nav/\${version}/\${locale}\`);
1869
+ const fragmentBase = withMountedBase(\`/blume-nav/\${version}/\${locale}\`);
1835
1870
  ---
1836
1871
 
1837
1872
  <NavTree
@@ -1855,6 +1890,7 @@ export const ogEndpointTemplate = (
1855
1890
  * Absent, every card renders on every build.
1856
1891
  */
1857
1892
  cache?: OgCache;
1893
+ fallbacks?: OgGoogleFont[];
1858
1894
  families?: OgFontFamilies;
1859
1895
  fonts?: OgFont[];
1860
1896
  /**
@@ -1868,7 +1904,7 @@ export const ogEndpointTemplate = (
1868
1904
  ): string =>
1869
1905
  `// Generated by Blume. Do not edit.
1870
1906
  import { cachedOgImage } from "blume/og";
1871
- import type { OgCache, OgFont, OgFontFamilies } from "blume/og";
1907
+ import type { OgCache, OgFont, OgFontFamilies, OgGoogleFont } from "blume/og";
1872
1908
  import data from "blume:data";
1873
1909
 
1874
1910
  export const prerender = true;
@@ -1886,6 +1922,9 @@ const fonts: OgFont[] = ${JSON.stringify(og.fonts ?? [])};
1886
1922
  const families: OgFontFamilies | undefined = ${
1887
1923
  og.families ? JSON.stringify(og.families) : "undefined"
1888
1924
  };
1925
+ // Script fallbacks (Noto families, the configured locales' first). A card
1926
+ // fetches them only when its text has a glyph Takumi's built-in font lacks.
1927
+ const fallbacks: OgGoogleFont[] = ${JSON.stringify(og.fallbacks ?? [])};
1889
1928
 
1890
1929
  // Rendered cards are kept on disk between builds, keyed by everything that
1891
1930
  // decides their pixels, so a rebuild renders only the cards whose title,
@@ -1958,6 +1997,7 @@ export async function GET({ props }: { props: CardProps }) {
1958
1997
  accent: data.config.og.palette?.accent ?? data.config.theme.accent.light,
1959
1998
  brand: data.config.title,
1960
1999
  description: props.description ?? data.config.og.description,
2000
+ fallbacks,
1961
2001
  families,
1962
2002
  fonts,
1963
2003
  logo: data.config.og.logo,
@@ -2153,8 +2193,8 @@ export const catchAllPageTemplate = (options: {
2153
2193
  import { getEntry, render } from "astro:content";
2154
2194
  import type { CollectionKey } from "astro:content";
2155
2195
  import RootLayout from "blume/components/layout/RootLayout.astro";
2156
- import { withBase } from "blume/components/islands/base-path.ts";
2157
- import { stripBasePath, withBasePath } from "blume/core/base-path.ts";
2196
+ import { withBase, withMountedBase } from "blume/components/islands/base-path.ts";
2197
+ import { mountBasePath, stripBasePath } from "blume/core/base-path.ts";
2158
2198
  import { resolveSlot } from "blume/components/layout/overrides.ts";
2159
2199
  ${componentImports}
2160
2200
  import data from "blume:data";
@@ -2219,12 +2259,16 @@ const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
2219
2259
  const ogPath = data.config.og.enabled
2220
2260
  ? encodeURI(\`/og/\${route === "/" ? "index" : route.slice(1)}.png\`)
2221
2261
  : null;
2222
- const ogRel = seo.image ?? ogPath;
2223
2262
  // Absolute URLs also carry the deployment base (the page is served under it):
2224
2263
  // \`site + base + path\`. Only absolutize root-relative paths: \`seo.image\` may be
2225
- // an external URL, which passes through verbatim (mirrors PageLayout).
2226
- const ogImage =
2227
- ogRel && base && ogRel.startsWith("/") ? \`\${base}\${withBase(ogRel)}\` : ogRel;
2264
+ // an external URL, which passes through verbatim (mirrors PageLayout). An
2265
+ // authored \`seo.image\` keeps a base written by hand; the generated card is
2266
+ // always mounted under it.
2267
+ const absoluteOg = (path: string, based: (path: string) => string) =>
2268
+ base && path.startsWith("/") ? \`\${base}\${based(path)}\` : path;
2269
+ const ogImage = seo.image
2270
+ ? absoluteOg(seo.image, withBase)
2271
+ : ogPath && absoluteOg(ogPath, withMountedBase);
2228
2272
  // Blume's generated card has known dimensions the layout can declare; a user's
2229
2273
  // \`seo.image\` could be any size or format, so it gets none.
2230
2274
  const ogGenerated = !seo.image && Boolean(ogPath);
@@ -2233,7 +2277,7 @@ const ogGenerated = !seo.image && Boolean(ogPath);
2233
2277
  // itself (a guest post crediting its own author) over the configured default.
2234
2278
  const x = { ...data.config.x, ...(seo.x?.creator ? { creator: seo.x.creator } : {}) };
2235
2279
 
2236
- const basedRoute = withBase(route);
2280
+ const basedRoute = withMountedBase(route);
2237
2281
 
2238
2282
  // Locale resolution. With i18n on, pick the active locale's nav + dictionary,
2239
2283
  // build hreflang alternates, and derive the language-switcher targets.
@@ -2281,7 +2325,7 @@ const contentDir = i18n
2281
2325
  : "ltr";
2282
2326
  // The root route keeps its trailing slash (\`https://site/\`) so canonical and
2283
2327
  // hreflang URLs byte-match the sitemap's <loc> for the home page.
2284
- const absolute = (path: string) => base + withBase(path);
2328
+ const absolute = (path: string) => base + withMountedBase(path);
2285
2329
 
2286
2330
  // A fallback page renders the fallback locale's page at this locale's URL, so
2287
2331
  // its canonical names the page it copies, and search engines never rank the
@@ -2326,7 +2370,7 @@ const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
2326
2370
  // carries sits *after* the base (\`/docs/ja/guide\`), while \`localePrefix\` is
2327
2371
  // base-less (\`/ja\`). Stripping and re-adding a locale therefore happen in
2328
2372
  // base-less space, with the base re-applied at the end — the same
2329
- // \`withBasePath(basePath, localizeRoute(...))\` composition the manifest uses to
2373
+ // \`mountBasePath(basePath, localizeRoute(...))\` composition the manifest uses to
2330
2374
  // build every real route. Done in based space, \`stripLocale\` matches nothing
2331
2375
  // and \`localizeRoute\` prepends a second prefix (\`/ja/docs/ja/guide\`). Only a
2332
2376
  // switcher entry for a locale with no real translation reaches this fallback:
@@ -2334,7 +2378,7 @@ const xDefault = defaultAlt && base ? absolute(defaultAlt.path) : null;
2334
2378
  // generated reference (which has no \`alternates\` at all) for every locale
2335
2379
  // but its own.
2336
2380
  const mountLocalized = (logical: string, codeArg: string) =>
2337
- withBasePath(data.config.basePath, localizeRoute(logical, codeArg));
2381
+ mountBasePath(data.config.basePath, localizeRoute(logical, codeArg));
2338
2382
  const logicalRoute = i18n
2339
2383
  ? stripLocale(stripBasePath(data.config.basePath, route), locale)
2340
2384
  : route;
@@ -2363,7 +2407,7 @@ const versionRootFor = (id: string) => {
2363
2407
  const logical = id ? \`/\${id}\` : "/";
2364
2408
  return i18n
2365
2409
  ? mountLocalized(logical, locale)
2366
- : withBasePath(data.config.basePath, logical);
2410
+ : mountBasePath(data.config.basePath, logical);
2367
2411
  };
2368
2412
  const samePageSwitch = versionsConfig
2369
2413
  ? versionsConfig.switcher.redirect === "same-page"
@@ -2463,7 +2507,7 @@ const LayoutComponent = resolveSlot(layoutOverrides.Layout, RootLayout);
2463
2507
  exportEpub={${options.exportEpub}}${
2464
2508
  options.navFragments
2465
2509
  ? `
2466
- navFragmentBase={withBase(\`/blume-nav/\${version || "current"}/\${i18n && localePrefix(locale) ? locale : "default"}\`)}`
2510
+ navFragmentBase={withMountedBase(\`/blume-nav/\${version || "current"}/\${i18n && localePrefix(locale) ? locale : "default"}\`)}`
2467
2511
  : ""
2468
2512
  }
2469
2513
  openInChat={data.config.openInChat}
@@ -2516,7 +2560,7 @@ export const changelogIndexTemplate = (options: {
2516
2560
  // Generated by Blume. Do not edit.
2517
2561
  import { getCollection } from "astro:content";
2518
2562
  import RootLayout from "blume/components/layout/RootLayout.astro";
2519
- import { withBase } from "blume/components/islands/base-path.ts";
2563
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
2520
2564
  import { resolveSlot } from "blume/components/layout/overrides.ts";
2521
2565
  import { resolveDateFormatOptions } from "blume/core/date-format.ts";
2522
2566
  import { layoutOverrides } from "../generated/components.ts";
@@ -2615,7 +2659,7 @@ const items = changelogEntries.map((entry) => {
2615
2659
  return {
2616
2660
  date: date ? formatRowDate(date) : null,
2617
2661
  dateTime: date ? date.toISOString().slice(0, 10) : null,
2618
- href: route ? withBase(route) : null,
2662
+ href: route ? withMountedBase(route) : null,
2619
2663
  id: slugify(label),
2620
2664
  label,
2621
2665
  tag: entry.data.changelog?.category ?? null,
@@ -2651,14 +2695,14 @@ for (const item of items) {
2651
2695
 
2652
2696
  const base = data.config.site ? data.config.site.replace(/\\/$/, "") : null;
2653
2697
  // The canonical URL carries the deployment base (the page is served under it),
2654
- // matching how the catch-all canonicalizes via \`withBase(route)\`.
2655
- const basedRoute = withBase("/changelog");
2698
+ // matching how the catch-all canonicalizes via \`withMountedBase(route)\`.
2699
+ const basedRoute = withMountedBase("/changelog");
2656
2700
  const canonical = base ? base + basedRoute : null;
2657
2701
 
2658
2702
  // The generated OG card for this route (the /og endpoint emits it alongside
2659
2703
  // the content-route cards), absolutized like the catch-all's so crawlers get
2660
2704
  // a full URL when the site is known.
2661
- const ogPath = data.config.og.enabled ? withBase("/og/changelog.png") : null;
2705
+ const ogPath = data.config.og.enabled ? withMountedBase("/og/changelog.png") : null;
2662
2706
  const ogImage = ogPath && base ? base + ogPath : ogPath;
2663
2707
 
2664
2708
  // The page chrome (h1, title, description) comes from the translatable
@@ -2780,8 +2824,8 @@ export const notFoundPageTemplate = (): string => `---
2780
2824
  // Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
2781
2825
  import Icon from "blume/components/Icon.astro";
2782
2826
  import PageLayout from "blume/components/layout/PageLayout.astro";
2783
- import { withBase } from "blume/components/islands/base-path.ts";
2784
- import { withBasePath } from "blume/core/base-path.ts";
2827
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
2828
+ import { mountBasePath } from "blume/core/base-path.ts";
2785
2829
  import data from "blume:data";
2786
2830
 
2787
2831
  export const prerender = true;
@@ -2805,14 +2849,14 @@ const htmlLang = i18n ? i18n.defaultLocale : "en";
2805
2849
  // their resolved target when the section has no index page of its own.
2806
2850
  const suggestions = [
2807
2851
  ...data.navigation.tabs.map((tab) => ({
2808
- href: withBase(tab.href ?? tab.path),
2852
+ href: withMountedBase(tab.href ?? tab.path),
2809
2853
  label: tab.label,
2810
2854
  })),
2811
2855
  ...(data.config.discovery.sitemap
2812
- ? [{ href: withBase("/sitemap.xml"), label: nf.sitemap }]
2856
+ ? [{ href: withMountedBase("/sitemap.xml"), label: nf.sitemap }]
2813
2857
  : []),
2814
2858
  ...(data.config.discovery.llmsTxt
2815
- ? [{ href: withBase("/llms.txt"), label: nf.llms }]
2859
+ ? [{ href: withMountedBase("/llms.txt"), label: nf.llms }]
2816
2860
  : []),
2817
2861
  ];
2818
2862
 
@@ -2824,7 +2868,7 @@ const suggestions = [
2824
2868
  // Localized routes sit under both bases (\`{deployment.base}/{basePath}/ar/…\`),
2825
2869
  // so the script strips the two together, trailing slash included.
2826
2870
  const { basePath } = data.config;
2827
- const siteRoot = withBase(withBasePath(basePath, "/"));
2871
+ const siteRoot = withMountedBase(mountBasePath(basePath, "/"));
2828
2872
  const localeBase = siteRoot.endsWith("/") ? siteRoot : siteRoot + "/";
2829
2873
  const localized = Object.fromEntries(
2830
2874
  (i18n?.locales ?? [])
@@ -2837,7 +2881,7 @@ const localized = Object.fromEntries(
2837
2881
  description: strings.description,
2838
2882
  dir: l.dir ?? "ltr",
2839
2883
  home: strings.home,
2840
- homeHref: withBase(withBasePath(basePath, "/" + l.code)),
2884
+ homeHref: withMountedBase(mountBasePath(basePath, "/" + l.code)),
2841
2885
  suggestions: strings.suggestions,
2842
2886
  title: strings.title,
2843
2887
  },
@@ -2887,7 +2931,7 @@ const localizedJson = JSON.stringify(localized).replaceAll("<", "\\\\u003c");
2887
2931
  <a
2888
2932
  class="mt-8 inline-flex items-center gap-1.5 rounded-full bg-accent py-2 pe-4 ps-3.5 text-sm font-medium text-accent-foreground transition-opacity hover:opacity-90"
2889
2933
  data-nf-home
2890
- href={withBase("/")}
2934
+ href={withMountedBase("/")}
2891
2935
  >
2892
2936
  <Icon class="rtl:-scale-x-100" name="arrow-left" size={14} />
2893
2937
  <span data-nf="home">{nf.home}</span>
@@ -2969,7 +3013,7 @@ const localizedJson = JSON.stringify(localized).replaceAll("<", "\\\\u003c");
2969
3013
  */
2970
3014
  export const notFoundMarkdownTemplate =
2971
3015
  (): string => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
2972
- import { withBase } from "blume/components/islands/base-path.ts";
3016
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
2973
3017
  import { absoluteUrl } from "blume/core/site-url.ts";
2974
3018
  import data from "blume:data";
2975
3019
 
@@ -2980,7 +3024,7 @@ const nf = data.ui.notFound;
2980
3024
  // Absolute for internal routes when the site is known; an external tab href
2981
3025
  // passes through untouched.
2982
3026
  const href = (path: string): string => {
2983
- const based = withBase(path);
3027
+ const based = withMountedBase(path);
2984
3028
  return data.config.site && based.startsWith("/") && !based.startsWith("//")
2985
3029
  ? absoluteUrl(data.config.site, based)
2986
3030
  : based;
@@ -3039,7 +3083,7 @@ export function GET() {
3039
3083
  export const notFoundJsonTemplate =
3040
3084
  (): string => `// Generated by Blume. Do not edit. Override by adding \`pages/404.astro\`.
3041
3085
  import { problem } from "blume/ai/api/problem.ts";
3042
- import { withBase } from "blume/components/islands/base-path.ts";
3086
+ import { withMountedBase } from "blume/components/islands/base-path.ts";
3043
3087
  import { absoluteUrl } from "blume/core/site-url.ts";
3044
3088
  import data from "blume:data";
3045
3089
 
@@ -3050,7 +3094,7 @@ const nf = data.ui.notFound;
3050
3094
  // Absolute for internal routes when the site is known; an external tab href
3051
3095
  // passes through untouched.
3052
3096
  const href = (path: string): string => {
3053
- const based = withBase(path);
3097
+ const based = withMountedBase(path);
3054
3098
  return data.config.site && based.startsWith("/") && !based.startsWith("//")
3055
3099
  ? absoluteUrl(data.config.site, based)
3056
3100
  : based;
@@ -3225,7 +3269,7 @@ ${wrapperPropsType("Example")}
3225
3269
  /**
3226
3270
  * The route prefix `<Component />` preview frames are served under:
3227
3271
  * `{basePath}/blume-examples/<example path>`. `deployment.base` is layered on
3228
- * top by Astro (components apply it with `withBase`).
3272
+ * top by Astro (components apply it with `withMountedBase`).
3229
3273
  */
3230
3274
  export const examplesRouteBase = (basePath: string): string =>
3231
3275
  `${basePath}/blume-examples`;
@@ -14,6 +14,9 @@ import { isServed, normalizePath, resolveHref, siteOrigin } from "../url.ts";
14
14
  /** Browser-magic fragments that scroll without needing a matching id. */
15
15
  const MAGIC_FRAGMENTS = new Set(["", "top"]);
16
16
 
17
+ /** Where a fragment's directives (text fragments) start. */
18
+ const FRAGMENT_DIRECTIVE = ":~:";
19
+
17
20
  /** Whether a fragment lands on an id of the target page. */
18
21
  const anchorResolves = (target: PageSnapshot, fragment: string): boolean => {
19
22
  if (MAGIC_FRAGMENTS.has(fragment)) {
@@ -65,8 +68,12 @@ export const linkChecks: CheckModule = {
65
68
  page: PageSnapshot,
66
69
  link: SnapshotLink,
67
70
  target: PageSnapshot,
68
- fragment: string
71
+ hash: string
69
72
  ): void => {
73
+ // A text fragment (`#:~:text=…`) is a directive the browser strips
74
+ // before it looks for an id; only the part in front of `:~:` names an
75
+ // element, and an empty one scrolls to the text instead.
76
+ const [fragment = ""] = hash.split(FRAGMENT_DIRECTIVE);
70
77
  if (anchorResolves(target, fragment)) {
71
78
  return;
72
79
  }
@@ -58,7 +58,8 @@ export const buildGraph = (
58
58
  * Non-indexable pages are excluded — a `noindex` page is *meant* to be
59
59
  * unreachable — as is the home page, which is nobody's job to link to. Under a
60
60
  * `basePath` the home page's built URL is the base itself, so the caller names
61
- * it via `homeUrl`.
61
+ * it via `homeUrl`. An i18n fallback copy is excluded too: it stands in for a
62
+ * translation nobody has written yet, so the page to link to is the original.
62
63
  */
63
64
  export const orphanPages = (
64
65
  pages: PageSnapshot[],
@@ -68,6 +69,7 @@ export const orphanPages = (
68
69
  pages.filter(
69
70
  (page) =>
70
71
  page.indexable &&
72
+ page.route?.fallback !== true &&
71
73
  page.url !== "/" &&
72
74
  page.url !== homeUrl &&
73
75
  (graph.contentIn.get(page.url)?.size ?? 0) === 0
package/src/audit/run.ts CHANGED
@@ -2,10 +2,12 @@ import { readFile } from "node:fs/promises";
2
2
 
3
3
  import pMap from "p-map";
4
4
 
5
- import { normalizeBasePath, withBasePath } from "../core/base-path.ts";
5
+ import { normalizeBasePath } from "../core/base-path.ts";
6
+ import { routeSetFor } from "../core/locale-links.ts";
6
7
  import type { BlumeProject } from "../core/project-graph.ts";
7
8
  import type { Diagnostic } from "../core/types.ts";
8
9
  import { deployStaticDir } from "../deploy/adapter-output.ts";
10
+ import { applyBaseToAstroRedirects } from "../deploy/redirects.ts";
9
11
  import { CHECKS } from "./catalog.ts";
10
12
  import type { CheckId } from "./catalog.ts";
11
13
  import { assetChecks } from "./checks/assets.ts";
@@ -162,14 +164,15 @@ export const runAudit = async (options: AuditOptions): Promise<AuditResult> => {
162
164
  redirects: resolveRedirects(
163
165
  // Redirects are authored as if mounted at root; the built page URLs they
164
166
  // are checked against carry `basePath` (it's a real directory in the
165
- // build), so both sides gain it here — mirroring what
166
- // `applyBaseToAstroRedirects` does at build time. `withBasePath` is
167
- // idempotent and leaves external `to` URLs untouched.
168
- project.config.redirects.map((redirect) => ({
169
- ...redirect,
170
- from: withBasePath(basePath, redirect.from),
171
- to: withBasePath(basePath, redirect.to),
172
- })),
167
+ // build), so both sides gain it here, based exactly as the build bases
168
+ // them — a `to` naming a public file stays at the root, and external
169
+ // `to` URLs pass through.
170
+ applyBaseToAstroRedirects(
171
+ project.config.redirects,
172
+ basePath,
173
+ "",
174
+ routeSetFor(project.manifest.routes)
175
+ ),
173
176
  // Pages, static files, and server routes alike: a redirect may
174
177
  // legitimately land on a served asset (`/old-whitepaper` ->
175
178
  // `/files/whitepaper.pdf`), and the same predicate the link and llms.txt
@@ -196,8 +196,13 @@ export const buildSnapshot = (options: {
196
196
  scripts: collectAssets(document, "script[src]", "src"),
197
197
  source: route?.sourcePath,
198
198
  styles: collectAssets(document, 'link[rel="stylesheet"][href]', "href"),
199
+ // Only the document's own titles: an accessible inline SVG carries a
200
+ // `<title>` of its own (an SVG-namespace element, not the page title), and
201
+ // counting it would flag BLUME_AUDIT_TITLE_MULTIPLE — or stand in for a
202
+ // head `<title>` that is genuinely missing.
199
203
  titles: document
200
204
  .querySelectorAll("title")
205
+ .filter((title) => !title.closest("svg"))
201
206
  .map((title) => title.text.trim())
202
207
  .filter((text) => text.length > 0),
203
208
  twitter: prefixedMeta(document, "name", "twitter:"),
package/src/cli/args.ts CHANGED
@@ -21,3 +21,35 @@ export const parsePort = (value?: string): number | undefined => {
21
21
  }
22
22
  return port;
23
23
  };
24
+
25
+ /**
26
+ * The longest `--timeout` a timer can hold, in seconds. Node clamps a
27
+ * `setTimeout` delay above 2^31−1 ms (about 24.8 days) to 1 ms, so a larger
28
+ * value would time every agent out the moment it starts.
29
+ */
30
+ export const MAX_TIMEOUT_S = Math.floor(2_147_483_647 / 1000);
31
+
32
+ /**
33
+ * Parse a `--timeout` value in whole seconds, or return `fallback` when unset.
34
+ * A non-integer, non-positive, or too-long value exits with an error.
35
+ */
36
+ export const parseTimeoutSeconds = (
37
+ value: string | undefined,
38
+ fallback: number
39
+ ): number => {
40
+ if (value === undefined) {
41
+ return fallback;
42
+ }
43
+ const timeout = Number(value);
44
+ if (!Number.isInteger(timeout) || timeout <= 0) {
45
+ logger.error(`Invalid --timeout "${value}" (whole seconds).`);
46
+ process.exit(1);
47
+ }
48
+ if (timeout > MAX_TIMEOUT_S) {
49
+ logger.error(
50
+ `Invalid --timeout "${value}" (at most ${MAX_TIMEOUT_S} seconds, about 24 days).`
51
+ );
52
+ process.exit(1);
53
+ }
54
+ return timeout;
55
+ };
@@ -238,7 +238,10 @@ export const auditCommand = defineCommand({
238
238
  process.stderr.write(formatReport(result, root, { verbose: args.verbose }));
239
239
 
240
240
  if (shouldFail(result, gate)) {
241
- process.exit(1);
241
+ // Set the code and return rather than `process.exit`, which doesn't wait
242
+ // for a piped stderr: a long report was cut off after the first 64 KiB,
243
+ // in exactly the CI logs that need to show every finding.
244
+ process.exitCode = 1;
242
245
  }
243
246
  },
244
247
  });
@@ -187,7 +187,9 @@ export const doctorCommand = defineCommand({
187
187
  logger.success("No problems found.");
188
188
  }
189
189
  if (hadErrors) {
190
- process.exit(1);
190
+ // Set the code and return rather than `process.exit`, which doesn't wait
191
+ // for a piped stderr: a long diagnostic list would be cut off.
192
+ process.exitCode = 1;
191
193
  }
192
194
  },
193
195
  });