blume 1.6.4 → 1.6.6

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 (178) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0ewz4trd.js +679 -0
  4. package/dist/cli/chunk-0ewz4trd.js.map +15 -0
  5. package/dist/cli/chunk-27gtm2ym.js +69 -0
  6. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  7. package/dist/cli/chunk-2aj8ddew.js +72 -0
  8. package/dist/cli/chunk-2aj8ddew.js.map +10 -0
  9. package/dist/cli/chunk-3k0kzs6d.js +69 -0
  10. package/dist/cli/chunk-3k0kzs6d.js.map +11 -0
  11. package/dist/cli/chunk-3r94j3tc.js +221 -0
  12. package/dist/cli/chunk-3r94j3tc.js.map +10 -0
  13. package/dist/cli/chunk-4trphnvy.js +102 -0
  14. package/dist/cli/chunk-4trphnvy.js.map +11 -0
  15. package/dist/cli/chunk-4xyggvgf.js +21 -0
  16. package/dist/cli/chunk-4xyggvgf.js.map +10 -0
  17. package/dist/cli/chunk-5hs6gb7n.js +32 -0
  18. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  19. package/dist/cli/chunk-5yvt556e.js +185 -0
  20. package/dist/cli/chunk-5yvt556e.js.map +11 -0
  21. package/dist/cli/chunk-62qsssnh.js +3808 -0
  22. package/dist/cli/chunk-62qsssnh.js.map +36 -0
  23. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  24. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  25. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  26. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  27. package/dist/cli/chunk-9sh49q0h.js +30 -0
  28. package/dist/cli/chunk-9sh49q0h.js.map +10 -0
  29. package/dist/cli/chunk-aerwpe14.js +2370 -0
  30. package/dist/cli/chunk-aerwpe14.js.map +15 -0
  31. package/dist/cli/chunk-ag1zyr5x.js +176 -0
  32. package/dist/cli/chunk-ag1zyr5x.js.map +10 -0
  33. package/dist/cli/chunk-bawgnt8x.js +277 -0
  34. package/dist/cli/chunk-bawgnt8x.js.map +11 -0
  35. package/dist/cli/chunk-bcy492zc.js +16 -0
  36. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  37. package/dist/cli/chunk-btfr9yvw.js +41 -0
  38. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  39. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  40. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  41. package/dist/cli/chunk-cnvm6k3e.js +96 -0
  42. package/dist/cli/chunk-cnvm6k3e.js.map +10 -0
  43. package/dist/cli/chunk-etsqspj6.js +5170 -0
  44. package/dist/cli/chunk-etsqspj6.js.map +47 -0
  45. package/dist/cli/chunk-ev67ycx0.js +15 -0
  46. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  47. package/dist/cli/chunk-ey89bjj1.js +209 -0
  48. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  49. package/dist/cli/chunk-f75cqye8.js +76 -0
  50. package/dist/cli/chunk-f75cqye8.js.map +10 -0
  51. package/dist/cli/chunk-j00ezcg5.js +259 -0
  52. package/dist/cli/chunk-j00ezcg5.js.map +11 -0
  53. package/dist/cli/chunk-jtb45atp.js +467 -0
  54. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  55. package/dist/cli/chunk-m3p3wahd.js +117 -0
  56. package/dist/cli/chunk-m3p3wahd.js.map +10 -0
  57. package/dist/cli/chunk-n0y172hf.js +387 -0
  58. package/dist/cli/chunk-n0y172hf.js.map +12 -0
  59. package/dist/cli/chunk-n4qjabmt.js +1062 -0
  60. package/dist/cli/chunk-n4qjabmt.js.map +25 -0
  61. package/dist/cli/chunk-nyqzjdhj.js +111 -0
  62. package/dist/cli/chunk-nyqzjdhj.js.map +11 -0
  63. package/dist/cli/chunk-pxj10x8y.js +35 -0
  64. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  65. package/dist/cli/chunk-s4jn7f1q.js +54 -0
  66. package/dist/cli/chunk-s4jn7f1q.js.map +10 -0
  67. package/dist/cli/chunk-s4k1pnvf.js +81 -0
  68. package/dist/cli/chunk-s4k1pnvf.js.map +10 -0
  69. package/dist/cli/chunk-s5e5jt53.js +227 -0
  70. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  71. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  72. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  73. package/dist/cli/chunk-tc89yh2r.js +136 -0
  74. package/dist/cli/chunk-tc89yh2r.js.map +10 -0
  75. package/dist/cli/chunk-vt8fgygt.js +23 -0
  76. package/dist/cli/chunk-vt8fgygt.js.map +10 -0
  77. package/dist/cli/chunk-vv237fp3.js +1002 -0
  78. package/dist/cli/chunk-vv237fp3.js.map +13 -0
  79. package/dist/cli/chunk-vv3f8mb6.js +5314 -0
  80. package/dist/cli/chunk-vv3f8mb6.js.map +58 -0
  81. package/dist/cli/chunk-vxv4x1n8.js +17 -0
  82. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  83. package/dist/cli/chunk-wb067mv3.js +758 -0
  84. package/dist/cli/chunk-wb067mv3.js.map +13 -0
  85. package/dist/cli/chunk-wd27zjcz.js +60 -0
  86. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  87. package/dist/cli/chunk-wkq5tbtq.js +1141 -0
  88. package/dist/cli/chunk-wkq5tbtq.js.map +19 -0
  89. package/dist/cli/chunk-x1vrdjyk.js +1967 -0
  90. package/dist/cli/chunk-x1vrdjyk.js.map +34 -0
  91. package/dist/cli/chunk-x66c5yjn.js +23 -0
  92. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  93. package/dist/cli/index.js +55 -27587
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/core/code-fences.d.ts +11 -0
  97. package/dist/types/core/config-input.d.ts +10 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +74 -1
  100. package/docs/02-deployment.mdx +1 -1
  101. package/docs/configuration/analytics.mdx +21 -2
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/syntax.mdx +1 -1
  105. package/docs/reference/cli.mdx +1 -1
  106. package/package.json +16 -14
  107. package/src/ai/api/handlers.ts +4 -7
  108. package/src/ai/api/paths.ts +8 -0
  109. package/src/ai/api/spec.ts +2 -1
  110. package/src/ai/ask-context.ts +378 -22
  111. package/src/astro/generate.ts +25 -29
  112. package/src/astro/include-hmr.ts +10 -13
  113. package/src/astro/include-refresh.ts +0 -0
  114. package/src/astro/index.ts +6 -1
  115. package/src/astro/integration.ts +269 -53
  116. package/src/astro/module-types.ts +74 -0
  117. package/src/astro/templates.ts +85 -97
  118. package/src/audit/image-size.ts +10 -8
  119. package/src/cli/command-meta.ts +77 -0
  120. package/src/cli/commands/add.ts +2 -4
  121. package/src/cli/commands/audit.ts +2 -4
  122. package/src/cli/commands/build.ts +42 -346
  123. package/src/cli/commands/check.ts +2 -4
  124. package/src/cli/commands/dev.ts +31 -42
  125. package/src/cli/commands/doctor.ts +2 -4
  126. package/src/cli/commands/eject.ts +3 -41
  127. package/src/cli/commands/eval.ts +2 -5
  128. package/src/cli/commands/init.ts +2 -4
  129. package/src/cli/commands/mcp-stdio.ts +2 -5
  130. package/src/cli/commands/preview.ts +3 -5
  131. package/src/cli/commands/sync.ts +2 -4
  132. package/src/cli/commands/translate.ts +2 -5
  133. package/src/cli/commands/validate.ts +2 -4
  134. package/src/cli/commands/version.ts +2 -4
  135. package/src/cli/eject-scripts.ts +0 -45
  136. package/src/cli/host-args.ts +16 -0
  137. package/src/cli/index.ts +84 -35
  138. package/src/cli/lazy-command.ts +47 -0
  139. package/src/components/content/GithubInfo.astro +4 -1
  140. package/src/components/content/mermaid-element.ts +8 -0
  141. package/src/components/layout/Analytics.astro +20 -1
  142. package/src/components/layout/PageLayout.astro +14 -3
  143. package/src/components/layout/ReferenceLayout.astro +15 -4
  144. package/src/components/layout/RootLayout.astro +15 -4
  145. package/src/components/layout/analytics-client.ts +2 -1
  146. package/src/components/layout/page-locale.ts +29 -0
  147. package/src/components/openapi/AsyncApiOperation.astro +5 -3
  148. package/src/components/openapi/Authorization.astro +4 -6
  149. package/src/components/openapi/Bindings.astro +2 -2
  150. package/src/components/openapi/Description.astro +109 -0
  151. package/src/components/openapi/GraphqlFieldsTable.astro +5 -7
  152. package/src/components/openapi/GraphqlOperation.astro +4 -3
  153. package/src/components/openapi/GraphqlType.astro +4 -6
  154. package/src/components/openapi/ParametersTable.astro +5 -7
  155. package/src/components/openapi/RequestBody.astro +2 -4
  156. package/src/components/openapi/Responses.astro +4 -3
  157. package/src/components/openapi/SchemaProperty.astro +11 -6
  158. package/src/components/openapi/description.ts +91 -0
  159. package/src/core/api-name.ts +18 -0
  160. package/src/core/code-fences.ts +48 -0
  161. package/src/core/config-input.ts +10 -0
  162. package/src/core/content-assets.ts +3 -7
  163. package/src/core/includes.ts +3 -7
  164. package/src/core/package-root.ts +1 -1
  165. package/src/core/schema.ts +27 -0
  166. package/src/core/sources/normalize.ts +2 -37
  167. package/src/core/sources/obsidian.ts +3 -2
  168. package/src/core/svg-dimensions.ts +97 -0
  169. package/src/core/version-cut.ts +2 -2
  170. package/src/deploy/artifacts.ts +370 -0
  171. package/src/deploy/cloudflare-negotiation.ts +97 -32
  172. package/src/deploy/function-bundle.ts +66 -20
  173. package/src/deploy/sitemap.ts +6 -0
  174. package/src/deploy/vercel-negotiation.ts +8 -30
  175. package/src/og/card.ts +6 -12
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +0 -2
  178. package/src/theme/entry.ts +9 -2
@@ -55,6 +55,31 @@ export interface AskRetrievalOptions {
55
55
  * Exported for testing; {@link createAskContext} is the runtime entry point.
56
56
  */
57
57
  export declare const relevantExcerpt: (content: string, query: string, max: number) => string;
58
+ interface PageSection {
59
+ /** Word tokens of the section's heading line, or none when it has no heading. */
60
+ headingWords: string[];
61
+ /** Position in the page, for source-order output and omission markers. */
62
+ index: number;
63
+ text: string;
64
+ /** The section's word tokens, cut once so scoring is a prefix test. */
65
+ words: string[];
66
+ }
67
+ /** A page split into sections once, so per-request scoring never re-tokenizes. */
68
+ export interface ParsedPage {
69
+ sections: PageSection[];
70
+ /** NFC-normalized, LF-only, trimmed page text; excerpts slice from it. */
71
+ text: string;
72
+ }
73
+ /**
74
+ * Split a page at its `##`+ headings (outside code fences). The text above the
75
+ * first heading is the page's own lead-in and is a section like any other.
76
+ * Line endings are folded to LF first so a CRLF checkout splits and matches the
77
+ * same way as an LF one. Exported for testing; {@link createAskContext}
78
+ * parses each page once and caches it across requests.
79
+ */
80
+ export declare const parsePage: (content: string) => ParsedPage;
81
+ /** {@link excerptPage} over a page parsed on the spot. Exported for testing. */
82
+ export declare const sectionExcerpt: (content: string, query: string, max: number) => string;
58
83
  /**
59
84
  * Build the request-time grounding function for the Ask AI endpoint.
60
85
  *
@@ -76,3 +101,4 @@ export declare const createAskContext: (data: AskData, options?: {
76
101
  instructions?: string;
77
102
  retrieval?: AskRetrievalOptions;
78
103
  }) => ((messages: AskMessage[], page?: AskPage) => Promise<string | undefined>);
104
+ export {};
@@ -0,0 +1,11 @@
1
+ /** The open fence's delimiter char and run length, or null outside one. */
2
+ export type FenceState = {
3
+ delimiter: "`" | "~";
4
+ length: number;
5
+ } | null;
6
+ /**
7
+ * Advance the fenced-code state for one line: an opening fence records its
8
+ * delimiter and run length, only a bare run of the same character at least as
9
+ * long closes it (CommonMark), and any other line leaves the state untouched.
10
+ */
11
+ export declare const nextFenceState: (line: string, fence: FenceState) => FenceState;
@@ -789,6 +789,16 @@ export interface AnalyticsScript {
789
789
  }
790
790
  /** Analytics providers. Configure one, several, or none. */
791
791
  export interface AnalyticsConfig {
792
+ /**
793
+ * Cloudflare Web Analytics, for a site Cloudflare doesn't proxy (manual
794
+ * setup). Not needed on a proxied zone with automatic RUM enabled — that
795
+ * injects the beacon at the edge, and configuring it here too would count
796
+ * every pageview twice.
797
+ */
798
+ cloudflare?: {
799
+ /** Site token from the Web Analytics JS snippet (`data-cf-beacon`). */
800
+ token: string;
801
+ };
792
802
  /** PostHog product analytics. */
793
803
  posthog?: {
794
804
  /** API host (for self-hosted / EU). Defaults to PostHog cloud. */
@@ -11,7 +11,7 @@ export declare const findPackageRoot: (start: string) => string;
11
11
  * Anchoring here — rather than at a fixed offset from `import.meta` — keeps the
12
12
  * package's own `src/`, assets, and `node_modules` locatable whether the code
13
13
  * runs from source under Bun (`src/...`) or from the published, bundled CLI
14
- * (`dist/cli/index.js`). The two layouts sit at different depths, so a relative
14
+ * (`dist/cli/*.js`). The two layouts sit at different depths, so a relative
15
15
  * `../..` resolves to different places; locating `package.json` does not.
16
16
  */
17
17
  export declare const packageRoot: () => string;
@@ -146,6 +146,76 @@ export declare const pageMetaSchema: z.ZodObject<{
146
146
  }, z.core.$strict>;
147
147
  export type PageMeta = z.infer<typeof pageMetaBaseSchema>;
148
148
  export type PageMetaInput = z.input<typeof pageMetaBaseSchema>;
149
+ /**
150
+ * The page schema as the generated content collections declare it, so
151
+ * `entry.data` is typed and normalized (dates as ISO strings, X handles with
152
+ * their `@`) the same way the scan's `PageMeta` is. Two deliberate loosenings
153
+ * over {@link pageMetaSchema}: custom keys (`frontmatter.extend`, per-type
154
+ * maps) pass through instead of failing the strict parse, and a page the
155
+ * strict parse rejects resolves to the empty defaults instead of throwing.
156
+ * The scan has already dropped such a page with a `BLUME_FRONTMATTER_INVALID`
157
+ * diagnostic — Blume continues without it unless `--strict` — so the
158
+ * collection must not turn that dropped page into a failed content sync.
159
+ */
160
+ export declare const pageCollectionSchema: z.ZodCatch<z.ZodObject<{
161
+ ai: z.ZodPrefault<z.ZodObject<{
162
+ exclude: z.ZodDefault<z.ZodBoolean>;
163
+ }, z.core.$strict>>;
164
+ authors: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
165
+ avatar: z.ZodOptional<z.ZodString>;
166
+ image: z.ZodOptional<z.ZodString>;
167
+ name: z.ZodString;
168
+ url: z.ZodOptional<z.ZodString>;
169
+ }, z.core.$catchall<z.ZodUnknown>>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
170
+ avatar: z.ZodOptional<z.ZodString>;
171
+ image: z.ZodOptional<z.ZodString>;
172
+ name: z.ZodString;
173
+ url: z.ZodOptional<z.ZodString>;
174
+ }, z.core.$catchall<z.ZodUnknown>>]>>]>>;
175
+ changelog: z.ZodOptional<z.ZodObject<{
176
+ category: z.ZodOptional<z.ZodString>;
177
+ date: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodDate]>, z.ZodTransform<string, string | Date>>>;
178
+ version: z.ZodOptional<z.ZodString>;
179
+ }, z.core.$strict>>;
180
+ date: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodDate]>, z.ZodTransform<string, string | Date>>>;
181
+ deprecated: z.ZodDefault<z.ZodBoolean>;
182
+ description: z.ZodOptional<z.ZodString>;
183
+ draft: z.ZodDefault<z.ZodBoolean>;
184
+ hidden: z.ZodDefault<z.ZodBoolean>;
185
+ icon: z.ZodOptional<z.ZodString>;
186
+ lastModified: z.ZodOptional<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodDate]>, z.ZodTransform<string, string | Date>>>;
187
+ noindex: z.ZodDefault<z.ZodBoolean>;
188
+ search: z.ZodPrefault<z.ZodObject<{
189
+ boost: z.ZodOptional<z.ZodNumber>;
190
+ exclude: z.ZodDefault<z.ZodBoolean>;
191
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
192
+ }, z.core.$strict>>;
193
+ seo: z.ZodPrefault<z.ZodObject<{
194
+ canonical: z.ZodOptional<z.ZodURL>;
195
+ description: z.ZodOptional<z.ZodString>;
196
+ image: z.ZodOptional<z.ZodString>;
197
+ noindex: z.ZodDefault<z.ZodBoolean>;
198
+ title: z.ZodOptional<z.ZodString>;
199
+ x: z.ZodOptional<z.ZodObject<{
200
+ creator: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
201
+ }, z.core.$strict>>;
202
+ }, z.core.$strict>>;
203
+ sidebar: z.ZodPrefault<z.ZodObject<{
204
+ badge: z.ZodOptional<z.ZodString>;
205
+ display: z.ZodOptional<z.ZodEnum<{
206
+ flat: "flat";
207
+ group: "group";
208
+ page: "page";
209
+ }>>;
210
+ hidden: z.ZodDefault<z.ZodBoolean>;
211
+ icon: z.ZodOptional<z.ZodString>;
212
+ label: z.ZodOptional<z.ZodString>;
213
+ order: z.ZodOptional<z.ZodNumber>;
214
+ }, z.core.$strict>>;
215
+ slug: z.ZodOptional<z.ZodString>;
216
+ title: z.ZodOptional<z.ZodString>;
217
+ type: z.ZodOptional<z.ZodString>;
218
+ }, z.core.$loose>>;
149
219
  export declare const folderMetaSchema: z.ZodObject<{
150
220
  collapsed: z.ZodOptional<z.ZodBoolean>;
151
221
  display: z.ZodOptional<z.ZodEnum<{
@@ -561,6 +631,9 @@ export declare const blumeConfigSchema: z.ZodObject<{
561
631
  webmcp: z.ZodDefault<z.ZodBoolean>;
562
632
  }, z.core.$strict>>;
563
633
  analytics: z.ZodOptional<z.ZodObject<{
634
+ cloudflare: z.ZodOptional<z.ZodObject<{
635
+ token: z.ZodString;
636
+ }, z.core.$strict>>;
564
637
  posthog: z.ZodOptional<z.ZodObject<{
565
638
  host: z.ZodOptional<z.ZodString>;
566
639
  key: z.ZodString;
@@ -733,10 +806,10 @@ export declare const blumeConfigSchema: z.ZodObject<{
733
806
  }, z.core.$strict>>;
734
807
  deployment: z.ZodPrefault<z.ZodObject<{
735
808
  adapter: z.ZodDefault<z.ZodNullable<z.ZodEnum<{
809
+ cloudflare: "cloudflare";
736
810
  vercel: "vercel";
737
811
  node: "node";
738
812
  netlify: "netlify";
739
- cloudflare: "cloudflare";
740
813
  }>>>;
741
814
  base: z.ZodOptional<z.ZodString>;
742
815
  output: z.ZodDefault<z.ZodEnum<{
@@ -101,7 +101,7 @@ On **Vercel**, **Netlify**, and **Cloudflare Pages**, Blume picks the matching a
101
101
 
102
102
  A server build includes everything a static build does, plus any Astro endpoints or middleware you add. The `node` adapter produces a standalone server you can run directly.
103
103
 
104
- On Vercel and Cloudflare, a server build also turns on [`Accept: text/markdown` content negotiation](/docs/discoverability/markdown#content-negotiation), so an agent requesting any content page with that header receives its raw-Markdown mirror at the same URL. On Vercel, Blume splices header-conditional rewrites into the deploy's routing config; on Cloudflare, it generates a small Worker in front of the Astro one and scopes `assets.run_worker_first` to the content routes, since the platform would otherwise serve the prerendered pages before any server code runs — other assets keep their zero-Worker fast path.
104
+ On Vercel and Cloudflare, a server build also turns on [`Accept: text/markdown` content negotiation](/docs/discoverability/markdown#content-negotiation), so an agent requesting any content page with that header receives its raw-Markdown mirror at the same URL. On Vercel, Blume splices header-conditional rewrites into the deploy's routing config; on Cloudflare, it generates a small Worker in front of the Astro one and scopes `assets.run_worker_first` to the content routes, since the platform would otherwise serve the prerendered pages before any server code runs — other assets keep their zero-Worker fast path. That Worker also answers the prerendered per-page JSON documents (`/api/docs/pages/{route}.json`) from the asset binding when a request for one reaches it, because Astro would otherwise route it to the `/api/` catch-all.
105
105
 
106
106
  :::note
107
107
  Server features have their own configuration — for example, Ask AI needs a model API key. See the [Ask AI guide](/docs/configuration/ask-ai) for setup.
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  title: Analytics
3
- description: First-party web analytics — Vercel Web Analytics, PostHog, or any custom script — wired up from blume.config.ts.
3
+ description: First-party web analytics — Vercel Web Analytics, Cloudflare Web Analytics, PostHog, or any custom script — wired up from blume.config.ts.
4
4
  ---
5
5
 
6
- Blume injects analytics for you from a single `analytics` block in `blume.config.ts`. Vercel Web Analytics and PostHog are first-class, and a `scripts` escape hatch covers every other provider — Plausible, Fathom, Google Analytics, Umami, and the rest.
6
+ Blume injects analytics for you from a single `analytics` block in `blume.config.ts`. Vercel Web Analytics, Cloudflare Web Analytics, and PostHog are first-class, and a `scripts` escape hatch covers every other provider — Plausible, Fathom, Google Analytics, Umami, and the rest.
7
7
 
8
8
  Analytics loads in **production builds only**. The scripts are emitted by `blume build`, never by `blume dev`, so local traffic never reaches your dashboards and you don't need a separate "development" project.
9
9
 
@@ -23,6 +23,24 @@ analytics: {
23
23
 
24
24
  No keys are needed — the script reports to the project it's deployed under. This only collects data on Vercel deployments, where the `/_vercel/insights` endpoint exists.
25
25
 
26
+ ## Cloudflare Web Analytics
27
+
28
+ [Cloudflare Web Analytics](https://developers.cloudflare.com/web-analytics/) has two setups, and only one of them needs config.
29
+
30
+ **Proxied zone (automatic setup).** If Cloudflare serves your site — a Worker with a custom domain, Pages, or any zone with the orange cloud on — enable Web Analytics for the zone in the Cloudflare dashboard and stop there. Cloudflare injects the beacon at the edge, so leave `analytics.cloudflare` unset; configuring it as well would count every pageview twice.
31
+
32
+ **Any other host (manual setup).** For a site Cloudflare doesn't proxy, add the site under Web Analytics in the dashboard, copy the token out of the JS snippet it gives you (the `token` inside `data-cf-beacon`), and pass it here. Blume renders the same beacon tag the snippet does.
33
+
34
+ ```ts blume.config.ts lineNumbers
35
+ analytics: {
36
+ cloudflare: {
37
+ token: "0123456789abcdef0123456789abcdef",
38
+ },
39
+ }
40
+ ```
41
+
42
+ The token is safe to ship to the browser — it only identifies the site. The beacon tracks history changes on its own, so client-router navigations count without any extra wiring.
43
+
26
44
  ## PostHog
27
45
 
28
46
  Provide your **project API key** to add [PostHog](https://posthog.com). The host defaults to PostHog Cloud US; set `host` for EU Cloud (`https://eu.i.posthog.com`) or a self-hosted instance.
@@ -72,6 +90,7 @@ analytics: {
72
90
  | Option | Default | Description |
73
91
  | --- | --- | --- |
74
92
  | `vercel` | `false` | Add Vercel Web Analytics (Vercel deployments only). |
93
+ | `cloudflare.token` | — | Cloudflare Web Analytics site token (manual setup). Enables the beacon when set. |
75
94
  | `posthog.key` | — | PostHog project API key. Enables PostHog when set. |
76
95
  | `posthog.host` | `https://us.i.posthog.com` | PostHog ingestion host (EU Cloud or self-hosted). |
77
96
  | `scripts[].src` | — | External script URL. Mutually exclusive with `content`. |
@@ -170,7 +170,7 @@ Set `apiKeyEnv` (and, for the named providers, `baseUrl`) on any backend to poin
170
170
  **Inkeep** answers from the content you've indexed in the Inkeep dashboard — it runs its own retrieval — so Blume leaves it ungrounded. Every other backend is [grounded](#grounding) in this site's pages.
171
171
  :::
172
172
 
173
- Keys are read with `process.env`, which covers the Node, Vercel, and Netlify adapters. On Cloudflare, expose the key through the platform's [runtime binding](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets). Enabling Ask AI also turns on React for the in-page island — see [Customization](/docs/configuration/customization#interactive-islands).
173
+ Keys are read through Astro's [`getSecret()`](https://docs.astro.build/en/guides/environment-variables/#retrieving-secrets-programmatically), so each adapter supplies them its own way: environment variables on Node, Vercel, and Netlify, and the Worker's [bindings](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets) on Cloudflare. Enabling Ask AI also turns on React for the in-page island — see [Customization](/docs/configuration/customization#interactive-islands).
174
174
 
175
175
  ## Rate limiting
176
176
 
@@ -182,13 +182,6 @@ blume eject --yes
182
182
 
183
183
  Eject is a one-way step: the hidden `.blume/` runtime becomes a normal Astro app you own and can modify directly. The `blume` package stays importable, so you keep its components, theme, and Markdown processors.
184
184
 
185
- ### What eject leaves behind
185
+ ### What eject keeps
186
186
 
187
- After ejecting, your `build` script runs plain `astro build` — the site itself builds the same, but the artifacts `blume build` layered on top are no longer produced. The eject command warns about the ones your config actually uses. To keep them:
188
-
189
- - **Pagefind search index** — with `search.provider: "pagefind"`, the search UI loads the index from the built site, so search breaks in production until you index it yourself. Install `pagefind` as a devDependency and index after each build: `"build": "astro build && pagefind --site dist"`.
190
- - **Hosted search sync** — a hosted provider's index is no longer pushed on build; re-upload your search records after each build with the provider's API or CLI.
191
- - **sitemap.xml** — recreate it with the standard [@astrojs/sitemap](https://docs.astro.build/en/guides/integrations-guide/sitemap/) integration.
192
- - **robots.txt** — ship your own as `public/robots.txt`.
193
- - **llms.txt / llms-full.txt and agent-readability.json** — write them by hand (or generate them in a build step of your own) and serve them from `public/`.
194
- - **Platform redirect files** — `_redirects` and `vercel.json` are no longer emitted for static builds. Your redirects still work as Astro-generated meta-refresh pages, or you can move them into your host's own config.
187
+ The ejected app's `build` script runs plain `astro build`, and the artifacts `blume build` layers on top — the search index (and a hosted provider's index sync), `llms.txt` and `llms-full.txt`, `sitemap.xml`, `robots.txt`, `agent-readability.json`, the `.well-known` discovery files, Agent Skills, and the platform `_redirects`/`_headers` files — are still produced: the Blume integration in the ejected `astro.config.mjs` writes them from Astro's `astro:build:done` hook, scanning the project (your `blume.config.ts` and content) the way the CLI did. What the ejected build does not do is the CLI's adapter post-processing: the Vercel and Cloudflare `Accept: text/markdown` routing splices, the Vercel function-bundle audit, and the `--analyze`/`--budget-*` gate.
@@ -384,7 +384,7 @@ flowchart LR
384
384
  ```
385
385
  ````
386
386
 
387
- Diagrams render on the client, so this is an MDX-only feature, and the Mermaid library loads only on pages that include one. The rest of this section is a gallery of common types — see the [Mermaid docs](https://mermaid.js.org/intro/) for the full list.
387
+ Diagrams render on the client, so this is an MDX-only feature, and the Mermaid library loads only on pages that include one. Diagrams use Mermaid's dagre layout and classic look by default; opt a single diagram into another layout or look through Mermaid front matter (a `config:` block with `layout: elk` or `look: neo`), and the ELK engine loads only for diagrams that ask for it. The rest of this section is a gallery of common types — see the [Mermaid docs](https://mermaid.js.org/intro/) for the full list.
388
388
 
389
389
  ### Flowchart
390
390
 
@@ -111,7 +111,7 @@ Add a `tsconfig.json` extending Astro's config to your project root so authored
111
111
  ```json title="tsconfig.json"
112
112
  {
113
113
  "extends": "astro/tsconfigs/strict",
114
- "include": [".blume/.astro/types.d.ts", ".blume/src/env.d.ts", "**/*"]
114
+ "include": [".blume/.astro/types.d.ts", "**/*"]
115
115
  }
116
116
  ```
117
117
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -59,6 +59,7 @@
59
59
  },
60
60
  "scripts": {
61
61
  "build": "bun run scripts/build.ts",
62
+ "bench": "bun bench/run.ts",
62
63
  "bundle-docs": "node scripts/bundle-docs.mjs",
63
64
  "prepack": "bun run scripts/build.ts && node scripts/bundle-docs.mjs",
64
65
  "test": "bun test",
@@ -74,12 +75,12 @@
74
75
  "@astrojs/vercel": "^11.0.10",
75
76
  "@asyncapi/converter": "^2.0.2",
76
77
  "@clack/prompts": "^1.8.0",
77
- "@iconify-json/lucide": "^1.2.130",
78
+ "@iconify-json/lucide": "^1.2.131",
78
79
  "@iconify/types": "^2.0.0",
79
80
  "@iconify/utils": "^3.1.7",
80
81
  "@modelcontextprotocol/sdk": "^1.30.0",
81
82
  "@orama/orama": "^3.1.18",
82
- "@pierre/diffs": "^1.4.1",
83
+ "@pierre/diffs": "^1.4.2",
83
84
  "@scalar/astro": "^0.4.18",
84
85
  "@scalar/openapi-parser": "^0.29.1",
85
86
  "@scalar/openapi-types": "^0.9.5",
@@ -89,7 +90,7 @@
89
90
  "@tailwindcss/vite": "^4.3.3",
90
91
  "@types/mdast": "^4.0.4",
91
92
  "@vercel/analytics": "^2.0.1",
92
- "ai": "^7.0.94",
93
+ "ai": "^7.0.99",
93
94
  "astro": "^7.3.2",
94
95
  "babel-plugin-react-compiler": "^1.0.0",
95
96
  "chokidar": "^5.0.0",
@@ -99,12 +100,12 @@
99
100
  "dompurify": "^3.4.15",
100
101
  "dotenv": "^17.4.2",
101
102
  "epub-gen-memory": "^1.1.2",
103
+ "es-module-lexer": "^3.0.2",
102
104
  "fast-xml-parser": "^5.11.1",
103
105
  "github-slugger": "^2.0.0",
104
106
  "graphql": "^17.0.2",
105
107
  "gray-matter": "^4.0.3",
106
108
  "html-escaper": "^3.0.3",
107
- "image-size": "^2.0.2",
108
109
  "jiti": "^2.7.0",
109
110
  "js-yaml": "^5.4.1",
110
111
  "katex": "^0.18.7",
@@ -114,7 +115,7 @@
114
115
  "mdast-util-gfm": "^3.1.0",
115
116
  "mdast-util-to-string": "^4.0.0",
116
117
  "medium-zoom": "^1.1.0",
117
- "mermaid": "^11.17.2",
118
+ "mermaid": "^12.0.0",
118
119
  "micromark-extension-gfm": "^3.0.0",
119
120
  "nanotar": "^0.3.0",
120
121
  "node-html-parser": "^9.0.4",
@@ -127,8 +128,8 @@
127
128
  "pathe": "^2.0.3",
128
129
  "perfect-debounce": "^2.1.0",
129
130
  "picomatch": "^4.0.7",
130
- "react": "^19.2.8",
131
- "react-dom": "^19.2.8",
131
+ "react": "^19.3.0",
132
+ "react-dom": "^19.3.0",
132
133
  "robots-parser": "^3.0.1",
133
134
  "satteri": "^0.10.5",
134
135
  "semver": "^7.8.5",
@@ -145,10 +146,10 @@
145
146
  "ufo": "^1.6.4",
146
147
  "undici": "^8.10.2",
147
148
  "write-file-atomic": "^8.0.0",
148
- "zod": "^4.5.4"
149
+ "zod": "^4.6.2"
149
150
  },
150
151
  "devDependencies": {
151
- "@ai-sdk/openai-compatible": "^3.0.45",
152
+ "@ai-sdk/openai-compatible": "^3.0.48",
152
153
  "@mixedbread/sdk": "^0.77.0",
153
154
  "@notionhq/client": "^5.26.0",
154
155
  "@openrouter/ai-sdk-provider": "^3.0.0",
@@ -156,16 +157,17 @@
156
157
  "@sanity/client": "^8.6.1",
157
158
  "@types/cross-spawn": "^6.0.6",
158
159
  "@types/html-escaper": "^3.0.4",
159
- "@types/node": "^22.20.1",
160
+ "@types/node": "^22.20.2",
160
161
  "@types/picomatch": "^4.0.3",
161
- "@types/react": "^19.2.18",
162
- "@types/react-dom": "^19.2.7",
162
+ "@types/react": "^19.3.0",
163
+ "@types/react-dom": "^19.3.0",
163
164
  "@types/semver": "^7.8.0",
164
165
  "@types/write-file-atomic": "^4.0.3",
165
166
  "@typescript/native-preview": "^7.0.0-dev.20260707.2",
166
- "algoliasearch": "^5.57.0",
167
+ "algoliasearch": "^5.59.0",
167
168
  "bun-types": "^1.4.2",
168
169
  "flexsearch": "^0.8.212",
170
+ "mitata": "1.0.34",
169
171
  "typesense": "^3.0.6"
170
172
  },
171
173
  "peerDependencies": {
@@ -11,10 +11,11 @@ import {
11
11
  } from "../mcp/query.ts";
12
12
  import type { SearchHitPayload } from "../mcp/query.ts";
13
13
  import {
14
- API_BASE,
15
14
  API_PAGES_PATH,
16
15
  API_SEARCH_PATH,
17
16
  OPENAPI_PATH,
17
+ pageJsonPath,
18
+ pageParam,
18
19
  } from "./paths.ts";
19
20
  import { problemResponse } from "./problem.ts";
20
21
 
@@ -85,10 +86,6 @@ export const jsonResponse = (payload: ApiPayload, status = 200): Response =>
85
86
  status,
86
87
  });
87
88
 
88
- /** The `pages/{route}.json` path segment for a route (`index` for home). */
89
- export const pageParam = (route: string): string =>
90
- route === "/" ? "index" : route.slice(1);
91
-
92
89
  /** The absolute (or root-relative) URL for a base-less path. */
93
90
  const siteUrl = (path: string, context: ApiSiteContext): string => {
94
91
  const based = withBasePath(context.base, path);
@@ -98,7 +95,7 @@ const siteUrl = (path: string, context: ApiSiteContext): string => {
98
95
  const summarize = (route: McpRoute, data: McpData): ApiPageSummary => {
99
96
  const summary: ApiPageSummary = {
100
97
  contentType: route.contentType,
101
- json: siteUrl(`${API_BASE}/pages/${pageParam(route.route)}.json`, data),
98
+ json: siteUrl(pageJsonPath(route.route), data),
102
99
  lastModified: route.lastModified,
103
100
  locale: route.locale,
104
101
  markdownUrl: siteUrl(`/${pageParam(route.route)}.md`, data),
@@ -163,7 +160,7 @@ export const pageResponse = (data: McpData, route: string): Response => {
163
160
  return problemResponse({
164
161
  code: "PAGE_NOT_FOUND",
165
162
  detail: `No documentation page has the route "${route}".`,
166
- instance: siteUrl(`${API_BASE}/pages/${pageParam(route)}.json`, data),
163
+ instance: siteUrl(pageJsonPath(route), data),
167
164
  resolution: `List every page at ${siteUrl(API_PAGES_PATH, data)}, or discover the API through ${siteUrl(OPENAPI_PATH, data)}.`,
168
165
  status: 404,
169
166
  title: "Page not found",
@@ -12,3 +12,11 @@ export const API_PAGES_PATH = `${API_BASE}/pages.json`;
12
12
  export const API_PAGE_PATH = `${API_BASE}/pages/{route}.json`;
13
13
  export const API_NAVIGATION_PATH = `${API_BASE}/navigation.json`;
14
14
  export const API_SEARCH_PATH = `${API_BASE}/search`;
15
+
16
+ /** The `pages/{route}.json` path segment for a route (`index` for home). */
17
+ export const pageParam = (route: string): string =>
18
+ route === "/" ? "index" : route.slice(1);
19
+
20
+ /** The base-less served path of a route's per-page JSON document. */
21
+ export const pageJsonPath = (route: string): string =>
22
+ `${API_BASE}/pages/${pageParam(route)}.json`;
@@ -1,3 +1,4 @@
1
+ import { apiNamePhrase } from "../../core/api-name.ts";
1
2
  import { withBasePath } from "../../core/base-path.ts";
2
3
  import { absoluteUrl, siteRoot } from "../../core/site-url.ts";
3
4
  import {
@@ -658,7 +659,7 @@ export const buildApiSpec = (input: ApiSpecInput): ApiSpecDocument => {
658
659
  `Read-only JSON API over the ${input.name} documentation${input.description ? `: ${input.description}` : "."}`,
659
660
  "Every operation is public and needs no authentication. Errors are RFC 9457 problem details (`application/problem+json`) with a stable `code`, a `detail`, and a `resolution` hint.",
660
661
  ].join("\n\n"),
661
- title: `${input.name} API`,
662
+ title: apiNamePhrase(input.name),
662
663
  version: input.version,
663
664
  "x-generator": `blume@${input.version}`,
664
665
  };