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
package/src/ai/tar.ts CHANGED
@@ -22,42 +22,169 @@ export interface TarEntry {
22
22
  path: string;
23
23
  }
24
24
 
25
- /** ustar `name` field capacity; skill layouts are shallow, so no `prefix`. */
25
+ /** ustar `name` field capacity. */
26
26
  const NAME_MAX = 100;
27
+ /** ustar `prefix` field capacity; `prefix/name` reaches 255 bytes. */
28
+ const PREFIX_MAX = 155;
29
+
30
+ /** ustar header byte offsets nanotar leaves for us to patch. */
31
+ const CHECKSUM_OFFSET = 148;
32
+ const CHECKSUM_SIZE = 8;
33
+ const TYPEFLAG_OFFSET = 156;
34
+ const PREFIX_OFFSET = 345;
35
+ const BLOCK_SIZE = 512;
36
+
37
+ /** The two zero blocks that mark the end of an archive. */
38
+ const END_OF_ARCHIVE = 2 * BLOCK_SIZE;
39
+ /** tar's default record size (20 blocks), which the archive is padded to. */
40
+ const RECORD_SIZE = 20 * BLOCK_SIZE;
41
+
42
+ /** The PAX extended-header typeflag (`x`). */
43
+ const PAX_TYPEFLAG = 0x78;
27
44
 
28
45
  const encoder = new TextEncoder();
29
46
 
47
+ const byteLength = (value: string): number => encoder.encode(value).byteLength;
48
+
49
+ /** `value` cut to at most `max` UTF-8 bytes, on a character boundary. */
50
+ const truncateBytes = (value: string, max: number): string =>
51
+ value.slice(0, encoder.encodeInto(value, new Uint8Array(max)).read);
52
+
53
+ /** One header nanotar writes, plus the fields it cannot. */
54
+ interface Block {
55
+ content: Uint8Array;
56
+ mode: string;
57
+ name: string;
58
+ /** The PAX extended header ahead of an over-long path. */
59
+ pax?: boolean;
60
+ /** ustar `prefix`, joined to `name` with a `/` by readers. */
61
+ prefix?: string;
62
+ }
63
+
64
+ /**
65
+ * Split a path over the `name` field at a `/` so the head fits `prefix` and
66
+ * the tail fits `name`. The longest head that fits leaves the shortest tail,
67
+ * so if that tail is still too long no split works.
68
+ */
69
+ const splitUstar = (
70
+ path: string
71
+ ): { name: string; prefix: string } | undefined => {
72
+ let slash = path.lastIndexOf("/");
73
+ while (slash > 0 && byteLength(path.slice(0, slash)) > PREFIX_MAX) {
74
+ slash = path.lastIndexOf("/", slash - 1);
75
+ }
76
+ const name = path.slice(slash + 1);
77
+ if (slash <= 0 || byteLength(name) > NAME_MAX) {
78
+ return undefined;
79
+ }
80
+ return { name, prefix: path.slice(0, slash) };
81
+ };
82
+
83
+ /**
84
+ * A PAX `path` record: `"<length> path=<value>\n"`, where the decimal length
85
+ * counts the whole record including its own digits.
86
+ */
87
+ const paxPathRecord = (path: string): Uint8Array => {
88
+ const body = ` path=${path}\n`;
89
+ const size = byteLength(body);
90
+ const length = size + String(size + String(size).length).length;
91
+ return encoder.encode(`${length}${body}`);
92
+ };
93
+
94
+ /**
95
+ * The header blocks for one entry. A path that fits `name` is written as is;
96
+ * up to 255 bytes it splits across `prefix` and `name`; beyond that (or with
97
+ * no usable split) a PAX extended header carries the full path and the ustar
98
+ * `name` holds a truncated fallback for readers that ignore PAX.
99
+ */
100
+ const entryBlocks = (entry: TarEntry): Block[] => {
101
+ const mode = entry.executable ? "755" : "644";
102
+ const file = { content: entry.content, mode };
103
+ if (byteLength(entry.path) <= NAME_MAX) {
104
+ return [{ ...file, name: entry.path }];
105
+ }
106
+ const split = splitUstar(entry.path);
107
+ if (split) {
108
+ return [{ ...file, ...split }];
109
+ }
110
+ const base = entry.path.slice(entry.path.lastIndexOf("/") + 1);
111
+ return [
112
+ {
113
+ content: paxPathRecord(entry.path),
114
+ mode: "644",
115
+ name: truncateBytes(`PaxHeader/${base}`, NAME_MAX),
116
+ pax: true,
117
+ },
118
+ { ...file, name: truncateBytes(base, NAME_MAX) },
119
+ ];
120
+ };
121
+
122
+ /** Recompute a patched header's checksum (nanotar's octal format). */
123
+ const writeChecksum = (header: Uint8Array): void => {
124
+ header.fill(0x20, CHECKSUM_OFFSET, CHECKSUM_OFFSET + CHECKSUM_SIZE);
125
+ const sum = header.reduce((total, byte) => total + byte, 0);
126
+ header.fill(0, CHECKSUM_OFFSET, CHECKSUM_OFFSET + CHECKSUM_SIZE);
127
+ encoder.encodeInto(
128
+ sum.toString(8),
129
+ header.subarray(CHECKSUM_OFFSET, CHECKSUM_OFFSET + CHECKSUM_SIZE)
130
+ );
131
+ };
132
+
30
133
  /**
31
134
  * Build a gzipped ustar archive of the given files, in the given order. Paths
32
- * longer than the ustar `name` field or escaping the archive root throw to
33
- * surface a programming error — nanotar would silently truncate an oversized
34
- * name into a corrupt archive, so the validation stays here.
135
+ * escaping the archive root throw to surface a programming error.
35
136
  */
36
137
  export const buildTarGz = (entries: readonly TarEntry[]): Uint8Array => {
37
138
  for (const entry of entries) {
38
- if (encoder.encode(entry.path).byteLength > NAME_MAX) {
39
- throw new Error(`tar path exceeds ${NAME_MAX} bytes: ${entry.path}`);
40
- }
41
139
  if (entry.path.startsWith("/") || entry.path.split("/").includes("..")) {
42
140
  throw new Error(`tar path must be archive-relative: ${entry.path}`);
43
141
  }
44
142
  }
143
+ const blocks = entries.flatMap(entryBlocks);
45
144
  const tar = createTar(
46
- entries.map((entry) => ({
145
+ blocks.map((block) => ({
47
146
  // Root-owned, epoch-mtime, empty owner names: every field a rebuild
48
147
  // could vary is pinned so the archive bytes are a function of content.
49
148
  attrs: {
50
149
  gid: 0,
51
150
  group: "",
52
- mode: entry.executable ? "755" : "644",
151
+ mode: block.mode,
53
152
  mtime: 0,
54
153
  uid: 0,
55
154
  user: "",
56
155
  },
57
- data: entry.content,
58
- name: entry.path,
156
+ data: block.content,
157
+ name: block.name,
59
158
  }))
60
159
  );
160
+ // nanotar writes neither `prefix` nor the PAX typeflag, so patch those
161
+ // headers in place and re-sum them.
162
+ let offset = 0;
163
+ for (const block of blocks) {
164
+ const header = tar.subarray(offset, offset + BLOCK_SIZE);
165
+ if (block.prefix) {
166
+ encoder.encodeInto(
167
+ block.prefix,
168
+ header.subarray(PREFIX_OFFSET, PREFIX_OFFSET + PREFIX_MAX)
169
+ );
170
+ writeChecksum(header);
171
+ }
172
+ if (block.pax) {
173
+ header[TYPEFLAG_OFFSET] = PAX_TYPEFLAG;
174
+ writeChecksum(header);
175
+ }
176
+ offset +=
177
+ BLOCK_SIZE * (1 + Math.ceil(block.content.byteLength / BLOCK_SIZE));
178
+ }
179
+ // nanotar pads the entries to a whole record but writes no end-of-archive
180
+ // marker, so entries ending within two blocks of a record boundary leave
181
+ // fewer than the two zero blocks readers expect (GNU tar warns "A lone zero
182
+ // block"). Pad to the first record that fits both; for every other archive
183
+ // that is the size nanotar already wrote, so its bytes are unchanged.
184
+ const archive = new Uint8Array(
185
+ Math.ceil((offset + END_OF_ARCHIVE) / RECORD_SIZE) * RECORD_SIZE
186
+ );
187
+ archive.set(tar.subarray(0, offset));
61
188
  // Sync gzip with a pinned level; Node writes no timestamp into the header.
62
- return new Uint8Array(gzipSync(tar, { level: 9 }));
189
+ return new Uint8Array(gzipSync(archive, { level: 9 }));
63
190
  };
@@ -12,6 +12,7 @@ import { hightouchHead } from "./hightouch.ts";
12
12
  import { hotjarHead } from "./hotjar.ts";
13
13
  import { logrocketHead } from "./logrocket.ts";
14
14
  import { mixpanelHead } from "./mixpanel.ts";
15
+ import { oneDollarStatsHead } from "./one-dollar-stats.ts";
15
16
  import { pirschHead } from "./pirsch.ts";
16
17
  import { plausibleHead } from "./plausible.ts";
17
18
  import { posthogHead } from "./posthog.ts";
@@ -92,6 +93,9 @@ const adapterScripts = (
92
93
  case "mixpanel": {
93
94
  return mixpanelHead(adapter.options);
94
95
  }
96
+ case "one-dollar-stats": {
97
+ return oneDollarStatsHead(adapter.options);
98
+ }
95
99
  case "pirsch": {
96
100
  return pirschHead(adapter.options);
97
101
  }
@@ -58,6 +58,11 @@ export type {
58
58
  MixpanelOptions,
59
59
  MixpanelRegion,
60
60
  } from "./mixpanel.ts";
61
+ export { oneDollarStats } from "./one-dollar-stats.ts";
62
+ export type {
63
+ OneDollarStatsAdapter,
64
+ OneDollarStatsOptions,
65
+ } from "./one-dollar-stats.ts";
61
66
  export { pirsch } from "./pirsch.ts";
62
67
  export type { PirschAdapter, PirschOptions } from "./pirsch.ts";
63
68
  export { plausible } from "./plausible.ts";
@@ -0,0 +1,86 @@
1
+ import { z } from "zod";
2
+
3
+ import type { AdapterDescriptor } from "../core/adapter.ts";
4
+ import { adapterDescriptorSchema } from "../core/adapter.ts";
5
+ import type { HeadScript } from "./head.ts";
6
+
7
+ /** The script OneDollarStats' install snippet loads. */
8
+ export const ONE_DOLLAR_STATS_SCRIPT_SRC =
9
+ "https://assets.onedollarstats.com/stonks.js";
10
+
11
+ /** The options {@link oneDollarStats} checks itself. */
12
+ export interface OneDollarStatsNamedOptions {
13
+ /**
14
+ * A bare host name, e.g. `docs.example.com`. The tracker reports every
15
+ * event under it on every host, not just `localhost`.
16
+ */
17
+ hostname?: string;
18
+ }
19
+
20
+ /**
21
+ * Options for {@link oneDollarStats}: any tracker setting, forwarded as a
22
+ * `data-` attribute on the tag (`hostname` → `data-hostname`, `devmode`,
23
+ * `url`, `autocollect`, `"hash-routing"`, …).
24
+ */
25
+ export type OneDollarStatsOptions = OneDollarStatsNamedOptions & {
26
+ [setting: string]: string;
27
+ };
28
+
29
+ export const oneDollarStatsOptionsSchema = z
30
+ .object({
31
+ hostname: z
32
+ .string()
33
+ .regex(/^[\p{L}\p{N}-]+(?:\.[\p{L}\p{N}-]+)*(?::\d+)?$/u, {
34
+ message:
35
+ "`hostname` must be a bare host name like `docs.example.com`, without `https://`, a path, a query, or a fragment.",
36
+ })
37
+ .optional(),
38
+ })
39
+ .catchall(z.string());
40
+
41
+ export type OneDollarStatsAdapter = AdapterDescriptor<
42
+ "one-dollar-stats",
43
+ OneDollarStatsOptions
44
+ >;
45
+
46
+ export const oneDollarStatsAdapterSchema = adapterDescriptorSchema(
47
+ "one-dollar-stats",
48
+ oneDollarStatsOptionsSchema
49
+ );
50
+
51
+ /**
52
+ * OneDollarStats. Needs no keys — the dashboard matches events to a site by
53
+ * the domain they come from.
54
+ */
55
+ export const oneDollarStats = (
56
+ options: OneDollarStatsOptions = {}
57
+ ): OneDollarStatsAdapter => ({
58
+ kind: "one-dollar-stats",
59
+ options,
60
+ requiredSecrets: [],
61
+ runtimeDeps: [],
62
+ });
63
+
64
+ /**
65
+ * The deferred tag from the install instructions, with every option as its
66
+ * own `data-` attribute. The tracker reads them from `document.currentScript`,
67
+ * so the tag stays a classic external script. It hooks `pushState` and
68
+ * `popstate` itself, so client-router navigations need no extra hook.
69
+ *
70
+ * The tracker turns hash routing on whenever `data-hash-routing` is present,
71
+ * whatever its value, so `"hash-routing": "false"` leaves the attribute off.
72
+ */
73
+ export const oneDollarStatsHead = (
74
+ options: OneDollarStatsOptions
75
+ ): HeadScript[] => {
76
+ const attributes: HeadScript["attributes"] = {};
77
+ for (const [setting, value] of Object.entries(options)) {
78
+ if (setting === "hash-routing" && value === "false") {
79
+ continue;
80
+ }
81
+ attributes[`data-${setting}`] = value;
82
+ }
83
+ attributes.defer = true;
84
+ attributes.src = ONE_DOLLAR_STATS_SCRIPT_SRC;
85
+ return [{ attributes, content: null }];
86
+ };
@@ -14,6 +14,7 @@ import { hightouchAdapterSchema } from "./hightouch.ts";
14
14
  import { hotjarAdapterSchema } from "./hotjar.ts";
15
15
  import { logrocketAdapterSchema } from "./logrocket.ts";
16
16
  import { mixpanelAdapterSchema } from "./mixpanel.ts";
17
+ import { oneDollarStatsAdapterSchema } from "./one-dollar-stats.ts";
17
18
  import { pirschAdapterSchema } from "./pirsch.ts";
18
19
  import { plausibleAdapterSchema } from "./plausible.ts";
19
20
  import { posthogAdapterSchema } from "./posthog.ts";
@@ -37,6 +38,7 @@ export const analyticsAdapterSchema = z.discriminatedUnion("kind", [
37
38
  hotjarAdapterSchema,
38
39
  logrocketAdapterSchema,
39
40
  mixpanelAdapterSchema,
41
+ oneDollarStatsAdapterSchema,
40
42
  pirschAdapterSchema,
41
43
  plausibleAdapterSchema,
42
44
  posthogAdapterSchema,
@@ -1887,7 +1887,7 @@ export const generateRuntime = async (
1887
1887
  // `astro.config.mjs` — which would restart the dev server in place.
1888
1888
  const contentRoutes = markdownRoutePaths(project);
1889
1889
  const homeLinkHeader =
1890
- buildHomeLinkHeader(config, contentRoutes) ?? undefined;
1890
+ buildHomeLinkHeader(config, contentRoutes, "dev") ?? undefined;
1891
1891
 
1892
1892
  const assistantEnabled = config.ai.assistant?.enabled ?? false;
1893
1893
  const exportPdf = config.export.pdf;
@@ -1,5 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
 
3
+ import { resolve } from "pathe";
4
+
3
5
  import { refreshBlumeContent } from "./integration.ts";
4
6
 
5
7
  /**
@@ -18,12 +20,21 @@ import { refreshBlumeContent } from "./integration.ts";
18
20
  */
19
21
 
20
22
  /** The Vite module-graph slice the plugin touches (structurally typed, like
21
- * every Blume-authored Vite plugin — see `serverAppResolvePlugin`). */
23
+ * every Blume-authored Vite plugin — see `serverAppResolvePlugin`). Every
24
+ * environment keeps its own graph, and the server's own `moduleGraph` only
25
+ * covers `client` and `ssr`: with `@astrojs/cloudflare` the content pages
26
+ * render in the `prerender` environment instead. */
22
27
  interface IncludeHmrServer {
23
- moduleGraph: {
24
- getModulesByFile: (file: string) => Set<unknown> | undefined;
25
- invalidateModule: (mod: never) => void;
26
- };
28
+ config: { root: string };
29
+ environments: Record<
30
+ string,
31
+ {
32
+ moduleGraph: {
33
+ getModulesByFile: (file: string) => Set<unknown> | undefined;
34
+ invalidateModule: (mod: never) => void;
35
+ };
36
+ }
37
+ >;
27
38
  ws: { send: (payload: { type: "full-reload" }) => void };
28
39
  }
29
40
 
@@ -51,16 +62,24 @@ export const includeHmrPlugin = (graphPath: string): IncludeHmrPlugin => ({
51
62
  // No graph yet (first run) — nothing to invalidate.
52
63
  return;
53
64
  }
54
- const includers = graph[ctx.file];
65
+ // An ejected app's graph is relative to the project, its Vite root; the
66
+ // hidden runtime's is absolute, which resolving leaves as it is.
67
+ const { config, environments, ws } = ctx.server;
68
+ const includers = Object.entries(graph).find(
69
+ ([partial]) => resolve(config.root, partial) === ctx.file
70
+ )?.[1];
55
71
  if (!includers || includers.length === 0) {
56
72
  return;
57
73
  }
58
- const { moduleGraph, ws } = ctx.server;
59
- for (const includer of includers) {
60
- for (const mod of moduleGraph.getModulesByFile(includer) ?? []) {
61
- // SAFETY: the module came out of this module graph; `never` only
62
- // reflects that the structural slice doesn't model the node type.
63
- moduleGraph.invalidateModule(mod as never);
74
+ for (const { moduleGraph } of Object.values(environments)) {
75
+ for (const includer of includers) {
76
+ for (const mod of moduleGraph.getModulesByFile(
77
+ resolve(config.root, includer)
78
+ ) ?? []) {
79
+ // SAFETY: the module came out of this module graph; `never` only
80
+ // reflects that the structural slice doesn't model the node type.
81
+ moduleGraph.invalidateModule(mod as never);
82
+ }
64
83
  }
65
84
  }
66
85
  // Plain `.md` pages have no Vite module: their HTML lives in the
@@ -29,7 +29,10 @@ type StoreEntry = ReturnType<LoaderContext["store"]["entries"]>[number][1];
29
29
  /** Meta-store key holding the recorded partials digest of an includer. */
30
30
  const stateKey = (id: string): string => `blume:include-state:${id}`;
31
31
 
32
- /** The generated include graph: absolute partial → absolute including pages. */
32
+ /**
33
+ * The generated include graph: partial → including pages, absolute in the
34
+ * hidden runtime and relative to the project root in an ejected app.
35
+ */
33
36
  type IncludeGraph = Record<string, string[]>;
34
37
 
35
38
  /**
@@ -47,14 +50,22 @@ const readGraph = (graphPath: string): IncludeGraph => {
47
50
  }
48
51
  };
49
52
 
50
- /** Invert the graph: absolute including page → its partials, sorted. */
51
- const partialsByPage = (graph: IncludeGraph): Map<string, string[]> => {
53
+ /**
54
+ * Invert the graph: absolute including page → its absolute partials, sorted.
55
+ * Each path resolves against the Astro root, which leaves the hidden runtime's
56
+ * absolute paths as they are.
57
+ */
58
+ const partialsByPage = (
59
+ graph: IncludeGraph,
60
+ root: string
61
+ ): Map<string, string[]> => {
52
62
  const byPage = new Map<string, string[]>();
53
63
  for (const [partial, pages] of Object.entries(graph)) {
54
64
  for (const page of pages) {
55
- const partials = byPage.get(page) ?? [];
56
- partials.push(partial);
57
- byPage.set(page, partials);
65
+ const abs = resolve(root, page);
66
+ const partials = byPage.get(abs) ?? [];
67
+ partials.push(resolve(root, partial));
68
+ byPage.set(abs, partials);
58
69
  }
59
70
  }
60
71
  for (const partials of byPage.values()) {
@@ -123,11 +134,11 @@ export const evictStaleIncluders = (
123
134
  context: LoaderContext,
124
135
  graphPath: string
125
136
  ): string[] => {
126
- const byPage = partialsByPage(readGraph(graphPath));
137
+ const root = fileURLToPath(context.config.root);
138
+ const byPage = partialsByPage(readGraph(graphPath), root);
127
139
  if (byPage.size === 0) {
128
140
  return [];
129
141
  }
130
- const root = fileURLToPath(context.config.root);
131
142
  const evicted: string[] = [];
132
143
  for (const [id, entry] of context.store.entries()) {
133
144
  if (evictIfStale(context, byPage, root, id, entry)) {
@@ -28,13 +28,40 @@ interface OverlayErrorPayload {
28
28
 
29
29
  /** The dev server's HMR channel — either `.ws` (Vite ≤5) or `.hot` (Vite 6+). */
30
30
  interface OverlayChannel {
31
- send: (payload: OverlayErrorPayload) => void;
31
+ send: (payload: OverlayErrorPayload | { type: "full-reload" }) => void;
32
32
  }
33
- interface OverlayServer {
33
+
34
+ /** A node in a Vite environment's module graph; only its identity matters. */
35
+ interface DevModuleNode {
36
+ id: string | null;
37
+ }
38
+
39
+ /**
40
+ * The dev-server slice the integration keeps: the overlay channel, and every
41
+ * Vite environment's module graph for the content re-sync (structurally
42
+ * typed, like every Blume-authored Vite plugin).
43
+ */
44
+ interface DevServer {
45
+ environments: Record<
46
+ string,
47
+ {
48
+ moduleGraph: {
49
+ getModuleById: (id: string) => DevModuleNode | undefined;
50
+ invalidateModule: (mod: never) => void;
51
+ };
52
+ }
53
+ >;
34
54
  hot?: OverlayChannel;
35
55
  ws?: OverlayChannel;
36
56
  }
37
57
 
58
+ /** The dev server's file watcher, as far as the data-store watch uses it. */
59
+ interface WatchedServer {
60
+ watcher: {
61
+ on: (event: "add" | "change", listener: (path: string) => void) => void;
62
+ };
63
+ }
64
+
38
65
  /** The parameters Astro hands `astro:server:setup`. */
39
66
  type ServerSetupParams = Parameters<
40
67
  NonNullable<AstroIntegration["hooks"]["astro:server:setup"]>
@@ -74,7 +101,7 @@ interface DevNegotiation {
74
101
  interface DevServerRegistry {
75
102
  buildProject: BlumeProject | null;
76
103
  negotiation: DevNegotiation | null;
77
- overlay: OverlayServer | null;
104
+ overlay: DevServer | null;
78
105
  refreshContent: RefreshContent | null;
79
106
  }
80
107
 
@@ -155,6 +182,64 @@ export const publishDevNegotiation = (
155
182
  : null;
156
183
  };
157
184
 
185
+ /** A dev server's HMR channel: `.ws`, or `.hot` where only that exists. */
186
+ const overlayChannelOf = (
187
+ server: DevServer | null
188
+ ): OverlayChannel | undefined => server?.ws ?? server?.hot;
189
+
190
+ /** Astro's content-layer data store, a virtual module in each environment. */
191
+ const DATA_STORE_MODULE_ID = "\0astro:data-layer-content";
192
+
193
+ /**
194
+ * Invalidate Astro's data store in every Vite environment, then reload the
195
+ * browser. Astro invalidates it after a sync in the `ssr` environment only,
196
+ * which is where pages render on most adapters. With `@astrojs/cloudflare`
197
+ * the `ssr` environment runs in workerd and the prerendered pages (every
198
+ * content page) render in the `prerender` environment, whose copy of the
199
+ * store then stayed at its startup snapshot: a page added in dev resolved
200
+ * its route but `getEntry` still missed it, so it 404ed until a restart.
201
+ */
202
+ const invalidateDataStore = (server: DevServer): void => {
203
+ for (const { moduleGraph } of Object.values(server.environments)) {
204
+ const mod = moduleGraph.getModuleById(DATA_STORE_MODULE_ID);
205
+ if (mod) {
206
+ // SAFETY: the node came out of this same module graph; `never` only
207
+ // reflects that the structural slice doesn't model the node type.
208
+ moduleGraph.invalidateModule(mod as never);
209
+ }
210
+ }
211
+ // Astro reloaded the browser when the store was written, which can land
212
+ // before the invalidation above; reload again so no page renders the
213
+ // stale copy.
214
+ overlayChannelOf(server)?.send({ type: "full-reload" });
215
+ };
216
+
217
+ /**
218
+ * Where Astro keeps the dev content store: `data-store.json` in the root's
219
+ * `.astro/` directory. Astro reloads its virtual module from this file.
220
+ */
221
+ const devDataStoreFile = (root: URL): string =>
222
+ fileURLToPath(new URL(".astro/data-store.json", root));
223
+
224
+ /**
225
+ * Invalidate the data store in every environment whenever Astro rewrites it.
226
+ * Astro's own file watcher updates the store on a content edit — a `.md`
227
+ * page's rendered body lives in it — but invalidates the module in `ssr`
228
+ * only, so with `@astrojs/cloudflare` a body edit stayed stale in
229
+ * `prerender` until something structural re-synced it. Keyed on the store
230
+ * write rather than Blume's regeneration, which can finish first.
231
+ */
232
+ const watchDataStore = (server: DevServer & WatchedServer, root: URL): void => {
233
+ const file = devDataStoreFile(root);
234
+ const onWrite = (path: string): void => {
235
+ if (path === file) {
236
+ invalidateDataStore(server);
237
+ }
238
+ };
239
+ server.watcher.on("add", onWrite);
240
+ server.watcher.on("change", onWrite);
241
+ };
242
+
158
243
  /**
159
244
  * Re-run Astro's content-layer loaders against the live dev server. Returns
160
245
  * `false` when no server has registered one — before the first
@@ -165,20 +250,21 @@ export const publishDevNegotiation = (
165
250
  * stale store and the moved page 404s.
166
251
  */
167
252
  export const refreshBlumeContent = async (): Promise<boolean> => {
168
- const { refreshContent } = registry();
253
+ const { overlay, refreshContent } = registry();
169
254
  if (!refreshContent) {
170
255
  return false;
171
256
  }
172
257
  // No loader filter: every collection re-syncs (the docs glob and any
173
258
  // staged collection alike).
174
259
  await refreshContent({});
260
+ if (overlay) {
261
+ invalidateDataStore(overlay);
262
+ }
175
263
  return true;
176
264
  };
177
265
 
178
- const overlayChannel = (): OverlayChannel | undefined => {
179
- const { overlay } = registry();
180
- return overlay?.ws ?? overlay?.hot;
181
- };
266
+ const overlayChannel = (): OverlayChannel | undefined =>
267
+ overlayChannelOf(registry().overlay);
182
268
 
183
269
  /**
184
270
  * Surface Blume's own diagnostics (config/frontmatter/content errors) in the
@@ -237,8 +323,9 @@ export interface BlumeIntegrationOptions {
237
323
  /**
238
324
  * Homepage `Link` header value for agent discovery (see
239
325
  * `ai/link-headers.ts`); the dev-server counterpart of the `_headers` /
240
- * Vercel-config emission, so `curl -I` against `blume dev` shows what the
241
- * deployed site will send. Published the same way as `contentRoutes`.
326
+ * Vercel-config emission, so `curl -I` against `blume dev` shows it too —
327
+ * built for the `"dev"` surface, which leaves out the targets only a build
328
+ * writes. Published the same way as `contentRoutes`.
242
329
  */
243
330
  homeLinkHeader?: string;
244
331
  /**
@@ -279,7 +366,8 @@ const isHomeUrl = (rawUrl: string | undefined): boolean => {
279
366
  * variant falls back to the synthesized llms.txt mirror when it's a landing
280
367
  * page (see `markdownRoutePaths`). The same
281
368
  * middleware also stamps the homepage agent-discovery `Link` header, mirroring
282
- * what the deployed site sends via `_headers` / the Vercel routing config.
369
+ * what the deployed site sends via `_headers` / the Vercel routing config
370
+ * minus the targets the dev server doesn't serve.
283
371
  *
284
372
  * Request URLs arrive base-less: Astro unshifts its own dev middlewares (base,
285
373
  * trailing slash, route guard) ahead of this one from its post-`configureServer`
@@ -351,9 +439,11 @@ export const blumeIntegration = (
351
439
  return;
352
440
  }
353
441
  // `dir` is what Astro reports as the client output — `dist/`, or
354
- // `dist/client` for a server build — which is what the platform
355
- // serves (the Vercel adapter copies it into its Build Output static
356
- // tree in a later hook).
442
+ // `dist/client` for a server build (`dist/client/<base>/` once
443
+ // `@astrojs/cloudflare` moves it under the base, whose `_headers` the
444
+ // writer places at the root served above it) — which is what the
445
+ // platform serves (the Vercel adapter copies it into its Build Output
446
+ // static tree in a later hook).
357
447
  await publishBuildArtifacts(project, fileURLToPath(dir), logger);
358
448
  },
359
449
  "astro:config:done": ({ config, injectTypes }) => {
@@ -399,6 +489,10 @@ export const blumeIntegration = (
399
489
  const shared = registry();
400
490
  shared.overlay = server;
401
491
  shared.refreshContent = refreshContent ?? null;
492
+ // `astro:config:done` always runs first; the root locates the store.
493
+ if (astroRoot) {
494
+ watchDataStore(server, astroRoot);
495
+ }
402
496
  // Prepend so the rewrite happens before Astro's own request handler,
403
497
  // letting the rewritten URL resolve to the `.md` endpoint.
404
498
  server.middlewares.stack.unshift({