blume 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli/index.js +2260 -1100
  3. package/dist/cli/index.js.map +123 -117
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/base-path.d.ts +8 -0
  6. package/dist/types/core/config-input.d.ts +87 -27
  7. package/dist/types/core/config.d.ts +2 -1
  8. package/dist/types/core/data.d.ts +16 -1
  9. package/dist/types/core/diagnostics.d.ts +5 -1
  10. package/dist/types/core/i18n-ui.d.ts +12 -0
  11. package/dist/types/core/schema.d.ts +116 -15
  12. package/dist/types/core/sources/types.d.ts +11 -1
  13. package/dist/types/core/standard-schema.d.ts +7 -3
  14. package/dist/types/core/types.d.ts +43 -2
  15. package/dist/types/core/ui-packs/index.d.ts +9 -1
  16. package/dist/types/openapi/references.d.ts +14 -7
  17. package/dist/types/seo/x-handle.d.ts +3 -2
  18. package/docs/advanced/api-reference.mdx +8 -6
  19. package/docs/configuration/ai.mdx +26 -8
  20. package/docs/configuration/search.mdx +2 -0
  21. package/docs/configuration/seo.mdx +1 -1
  22. package/docs/content/i18n.mdx +1 -1
  23. package/docs/content/meta.mdx +2 -1
  24. package/docs/content/meta.ts +1 -0
  25. package/docs/content/navigation.mdx +35 -1
  26. package/docs/content/sources.mdx +1 -1
  27. package/docs/content/versioning.mdx +106 -0
  28. package/docs/reference/cli.mdx +1 -0
  29. package/docs/reference/frontmatter.mdx +3 -0
  30. package/package.json +13 -1
  31. package/skills/blume-migrate/SKILL.md +2 -2
  32. package/skills/blume-migrate/references/docusaurus.md +1 -1
  33. package/skills/blume-migrate/references/fumadocs.md +1 -1
  34. package/skills/blume-migrate/references/mintlify.md +1 -1
  35. package/src/ai/agent-readability.ts +40 -12
  36. package/src/ai/api-catalog.ts +2 -2
  37. package/src/ai/ask-context.ts +49 -12
  38. package/src/ai/ask.ts +10 -1
  39. package/src/ai/component-markdown.ts +80 -43
  40. package/src/ai/llms.ts +42 -17
  41. package/src/ai/mcp/data.ts +48 -12
  42. package/src/ai/mcp/discovery.ts +52 -16
  43. package/src/ai/mcp/server.ts +280 -125
  44. package/src/ai/mcp/tools.ts +3 -3
  45. package/src/ai/skills.ts +32 -9
  46. package/src/ai/tar.ts +29 -70
  47. package/src/ai/visibility.ts +2 -2
  48. package/src/astro/component-slots.ts +2 -0
  49. package/src/astro/examples.ts +13 -5
  50. package/src/astro/generate.ts +113 -63
  51. package/src/astro/integration.ts +13 -2
  52. package/src/astro/islands.ts +23 -12
  53. package/src/astro/templates.ts +185 -41
  54. package/src/audit/agent.ts +16 -31
  55. package/src/audit/checks/content.ts +26 -11
  56. package/src/audit/checks/dns-aid.ts +3 -0
  57. package/src/audit/checks/indexability.ts +24 -6
  58. package/src/audit/checks/llms.ts +9 -4
  59. package/src/audit/checks/network.ts +2 -0
  60. package/src/audit/checks/social.ts +18 -10
  61. package/src/audit/crawl.ts +78 -25
  62. package/src/audit/report.ts +20 -19
  63. package/src/audit/run.ts +15 -5
  64. package/src/audit/snapshot.ts +29 -6
  65. package/src/audit/types.ts +25 -3
  66. package/src/blume-modules.d.ts +5 -1
  67. package/src/cli/commands/audit.ts +21 -21
  68. package/src/cli/commands/build.ts +30 -16
  69. package/src/cli/commands/dev.ts +15 -15
  70. package/src/cli/commands/doctor.ts +2 -0
  71. package/src/cli/commands/eject.ts +4 -4
  72. package/src/cli/commands/eval.ts +24 -30
  73. package/src/cli/commands/init.ts +9 -9
  74. package/src/cli/commands/mcp-stdio.ts +3 -0
  75. package/src/cli/commands/translate.ts +14 -3
  76. package/src/cli/commands/version.ts +85 -0
  77. package/src/cli/dev-lock.ts +31 -10
  78. package/src/cli/eject-scripts.ts +17 -2
  79. package/src/cli/env.ts +13 -30
  80. package/src/cli/index.ts +2 -0
  81. package/src/cli/init/questions.ts +1 -1
  82. package/src/cli/init/scaffold.ts +41 -13
  83. package/src/cli/internal-error.ts +1 -0
  84. package/src/cli/report-format.ts +22 -0
  85. package/src/components/content/AccordionItem.astro +2 -9
  86. package/src/components/content/ColorItem.astro +5 -13
  87. package/src/components/content/Component.astro +12 -8
  88. package/src/components/content/Frame.astro +2 -12
  89. package/src/components/content/Prompt.astro +12 -31
  90. package/src/components/content/Tab.astro +2 -9
  91. package/src/components/content/Tooltip.astro +1 -9
  92. package/src/components/content/Update.astro +2 -9
  93. package/src/components/content/auto-type-table.ts +3 -0
  94. package/src/components/content/diff.ts +9 -5
  95. package/src/components/content/github-info.ts +2 -0
  96. package/src/components/content/inline-markdown.ts +28 -0
  97. package/src/components/copy-feedback.ts +96 -0
  98. package/src/components/islands/ask-ai.tsx +111 -34
  99. package/src/components/islands/hooks.ts +5 -1
  100. package/src/components/islands/webmcp.ts +49 -12
  101. package/src/components/layout/Header.astro +25 -1
  102. package/src/components/layout/NavSelector.astro +11 -2
  103. package/src/components/layout/NavTree.astro +4 -2
  104. package/src/components/layout/PageActions.astro +20 -32
  105. package/src/components/layout/PageLayout.astro +8 -28
  106. package/src/components/layout/RootLayout.astro +24 -48
  107. package/src/components/layout/Search.astro +133 -22
  108. package/src/components/layout/VersionBanner.astro +39 -0
  109. package/src/components/layout/analytics-client.ts +8 -5
  110. package/src/components/layout/drawer-inert.ts +31 -0
  111. package/src/components/layout/hydration-hint.ts +1 -1
  112. package/src/components/layout/nav-utils.ts +1 -4
  113. package/src/components/layout/overrides.ts +25 -12
  114. package/src/components/layout/search/algolia.ts +18 -5
  115. package/src/components/layout/search/endpoint.ts +3 -0
  116. package/src/components/layout/search/flexsearch.ts +23 -7
  117. package/src/components/layout/search/orama-cloud.ts +1 -1
  118. package/src/components/layout/search/orama.ts +4 -1
  119. package/src/components/layout/search/pagefind.ts +8 -5
  120. package/src/components/layout/search/types.ts +45 -1
  121. package/src/components/layout/search/typesense.ts +19 -3
  122. package/src/components/openapi/ApiOverview.astro +32 -6
  123. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  124. package/src/components/openapi/Bindings.astro +89 -0
  125. package/src/components/openapi/MethodBadge.astro +3 -0
  126. package/src/components/openapi/Operation.astro +7 -2
  127. package/src/components/openapi/PanelTabs.astro +131 -0
  128. package/src/components/openapi/ParametersTable.astro +2 -0
  129. package/src/components/openapi/RequestPanel.astro +12 -119
  130. package/src/components/openapi/async-snippets.ts +174 -0
  131. package/src/components/openapi/async.ts +348 -0
  132. package/src/components/openapi/helpers.ts +52 -20
  133. package/src/components/openapi/panel.ts +11 -8
  134. package/src/components/openapi/security.ts +102 -29
  135. package/src/components/openapi/snippets.ts +11 -11
  136. package/src/components/raf-throttle.ts +21 -0
  137. package/src/components/slug.ts +14 -0
  138. package/src/core/base-path.ts +18 -1
  139. package/src/core/component-overrides.ts +28 -23
  140. package/src/core/config-input.ts +96 -27
  141. package/src/core/config.ts +20 -7
  142. package/src/core/content.ts +3 -1
  143. package/src/core/data.ts +16 -1
  144. package/src/core/define-components.ts +5 -0
  145. package/src/core/diagnostics.ts +46 -38
  146. package/src/core/frontmatter.ts +74 -4
  147. package/src/core/graph.ts +137 -53
  148. package/src/core/i18n-ui.ts +15 -0
  149. package/src/core/i18n.ts +16 -8
  150. package/src/core/load-module.ts +1 -0
  151. package/src/core/manifest.ts +92 -3
  152. package/src/core/meta.ts +44 -14
  153. package/src/core/nav-diagnostics.ts +3 -3
  154. package/src/core/navigation.ts +247 -67
  155. package/src/core/probe.ts +7 -19
  156. package/src/core/project-graph.ts +27 -4
  157. package/src/core/schema.ts +219 -67
  158. package/src/core/site-url.ts +27 -0
  159. package/src/core/sources/assets.ts +2 -0
  160. package/src/core/sources/cache.ts +16 -8
  161. package/src/core/sources/github-releases.ts +39 -11
  162. package/src/core/sources/mdx-remote.ts +4 -0
  163. package/src/core/sources/normalize.ts +93 -22
  164. package/src/core/sources/notion.ts +76 -22
  165. package/src/core/sources/portable-text.ts +48 -12
  166. package/src/core/sources/resolve.ts +1 -0
  167. package/src/core/sources/sanity.ts +68 -14
  168. package/src/core/sources/types.ts +17 -1
  169. package/src/core/sources/watch.ts +1 -1
  170. package/src/core/standard-schema.ts +9 -3
  171. package/src/core/text-width.ts +26 -0
  172. package/src/core/tsconfig-aliases.ts +9 -5
  173. package/src/core/types.ts +45 -2
  174. package/src/core/ui-packs/index.ts +9 -1
  175. package/src/core/version-cut.ts +301 -0
  176. package/src/core/version.ts +2 -0
  177. package/src/core/versions.ts +170 -0
  178. package/src/deploy/adapter-output.ts +5 -2
  179. package/src/deploy/cloudflare-negotiation.ts +40 -11
  180. package/src/deploy/robots.ts +2 -1
  181. package/src/deploy/rss.ts +2 -1
  182. package/src/deploy/sitemap.ts +89 -8
  183. package/src/deploy/vercel-negotiation.ts +11 -4
  184. package/src/eval/agents.ts +13 -10
  185. package/src/eval/report.ts +5 -18
  186. package/src/eval/run.ts +2 -2
  187. package/src/eval/schema.ts +1 -1
  188. package/src/markdown/base-links.ts +6 -6
  189. package/src/markdown/directives.ts +7 -1
  190. package/src/markdown/heading-anchors.ts +17 -6
  191. package/src/markdown/index.ts +73 -24
  192. package/src/markdown/inline-code.ts +14 -2
  193. package/src/markdown/language-icon.ts +6 -2
  194. package/src/markdown/mdast.ts +18 -4
  195. package/src/markdown/package-commands.ts +63 -58
  196. package/src/markdown/table-wrap.ts +4 -1
  197. package/src/markdown/twoslash.ts +2 -0
  198. package/src/og/card.ts +50 -33
  199. package/src/og/derive.ts +43 -27
  200. package/src/openapi/asyncapi.ts +366 -0
  201. package/src/openapi/model.ts +135 -66
  202. package/src/openapi/parse.ts +166 -33
  203. package/src/openapi/references.ts +47 -22
  204. package/src/openapi/render-mdx.ts +137 -59
  205. package/src/openapi/scalar.ts +8 -10
  206. package/src/openapi/source.ts +126 -29
  207. package/src/registry/eject.ts +7 -2
  208. package/src/search/documents.ts +103 -39
  209. package/src/search/facets.ts +7 -5
  210. package/src/search/orama-index.ts +117 -32
  211. package/src/search/popular.ts +10 -5
  212. package/src/search/providers.ts +2 -2
  213. package/src/search/sync/index.ts +2 -0
  214. package/src/search/sync/typesense.ts +4 -2
  215. package/src/seo/jsonld.ts +24 -6
  216. package/src/seo/x-handle.ts +8 -3
  217. package/src/theme/chrome-icons.ts +7 -2
  218. package/src/theme/fonts.ts +8 -4
  219. package/src/theme/icons.ts +4 -2
  220. package/src/theme/palette.ts +27 -15
  221. package/src/translate/ledger.ts +4 -2
  222. package/src/translate/meta.ts +15 -6
  223. package/src/translate/report.ts +10 -19
  224. package/src/translate/run.ts +29 -38
  225. package/src/translate/validate.ts +52 -17
  226. package/src/translate/work-list.ts +0 -0
  227. package/src/cli/coalesce.ts +0 -43
@@ -114,9 +114,24 @@ export interface PageRecord {
114
114
  locale: string;
115
115
  /**
116
116
  * Locale-agnostic logical route shared by every translation (e.g.
117
- * `/guides/x`). Pages with the same key are translations of each other.
117
+ * `/guides/x`, or `/v1.0/guides/x` under versioning the key is
118
+ * version-specific, so translations group within their version).
119
+ * Pages with the same key are translations of each other.
118
120
  */
119
121
  translationKey: string;
122
+ /**
123
+ * Resolved docs version: an archived id (`v1.0`) for snapshot pages, `""`
124
+ * for the current (unprefixed) docs — including every page of an
125
+ * unversioned project.
126
+ */
127
+ version: string;
128
+ /**
129
+ * Version- and locale-agnostic logical route (e.g. `/guides/x` for
130
+ * `/v1.0/guides/x`). Pages with the same key and locale are the same
131
+ * logical page across versions — this drives the switcher's same-page
132
+ * navigation and the canonical-to-latest lookup.
133
+ */
134
+ versionKey: string;
120
135
  /**
121
136
  * True for entries filled in from the fallback locale to pad a locale's
122
137
  * navigation for pages it hasn't translated yet. The record's content —
@@ -170,7 +185,12 @@ export type NavNode = {
170
185
  label: string;
171
186
  badge?: string;
172
187
  directory?: DirectoryMode;
173
- display?: SidebarDisplay;
188
+ /**
189
+ * The group's resolved render mode. Always stamped by both builders
190
+ * (generated and explicit-config sidebars), so the renderer can rely
191
+ * on it per node.
192
+ */
193
+ display: SidebarDisplay;
174
194
  icon?: string;
175
195
  route?: string;
176
196
  /**
@@ -249,6 +269,12 @@ export interface ContentGraph {
249
269
  navigation: Navigation;
250
270
  /** Navigation per locale; one entry per configured locale under i18n. */
251
271
  navigationByLocale: Record<string, Navigation>;
272
+ /**
273
+ * Navigation per archived version, keyed by version id and then locale code
274
+ * (`""` on a single-locale site). The current version's trees are
275
+ * `navigation`/`navigationByLocale`; empty when versioning is off.
276
+ */
277
+ navigationByVersion: Record<string, Record<string, Navigation>>;
252
278
  /** Map of route -> pageId for fast lookup and duplicate detection. */
253
279
  routes: Map<string, string>;
254
280
  diagnostics: Diagnostic[];
@@ -269,6 +295,12 @@ export interface LocaleSwitchOption {
269
295
  /** True when this locale has no real translation (renders fallback content). */
270
296
  untranslated: boolean;
271
297
  }
298
+ /** A docs version a logical page exists in, for the switcher and canonicals. */
299
+ export interface VersionAlternate {
300
+ /** Version id; `""` for the current (unprefixed) docs. */
301
+ version: string;
302
+ path: string;
303
+ }
272
304
  /** A route entry written to `blume.manifest.json`. */
273
305
  export interface RouteManifestEntry {
274
306
  id: string;
@@ -296,6 +328,15 @@ export interface RouteManifestEntry {
296
328
  locale: string;
297
329
  /** Locales this logical page is genuinely translated into (excludes fallbacks). */
298
330
  alternates: RouteAlternate[];
331
+ /** Resolved docs version (`""` for the current docs; see `PageRecord.version`). */
332
+ version: string;
333
+ /**
334
+ * Versions this logical page exists in within this route's locale — the
335
+ * current version first, then archived versions in configured order. Drives
336
+ * the switcher's same-page navigation and the canonical-to-latest lookup.
337
+ * Empty when versioning is off.
338
+ */
339
+ versionAlternates: VersionAlternate[];
299
340
  /** True when this route renders fallback content for a missing translation. */
300
341
  fallback?: boolean;
301
342
  /** Resolved "last updated" ISO date, when the feature is enabled. */
@@ -10,4 +10,12 @@ import type { UIStringsOverride } from "../i18n-ui.ts";
10
10
  * `zh-TW`). A locale's pack is merged onto English by {@link resolveUIStrings},
11
11
  * so a pack only needs the keys it translates.
12
12
  */
13
- export declare const UI_PACKS: Record<string, UIStringsOverride>;
13
+ /**
14
+ * Starter packs keyed by locale code. Open-keyed on purpose: resolvers look
15
+ * packs up by whatever locale a project configures, not only the codes here.
16
+ */
17
+ interface UiPackRegistry {
18
+ [locale: string]: UIStringsOverride;
19
+ }
20
+ export declare const UI_PACKS: UiPackRegistry;
21
+ export {};
@@ -1,4 +1,5 @@
1
1
  import type { ResolvedConfig } from "../core/schema.ts";
2
+ export { normalizeRoute } from "../core/base-path.ts";
2
3
  /**
3
4
  * Pure resolution of the configured API reference blocks into concrete routes,
4
5
  * labels, and a renderer choice — no file IO, so the content source, the
@@ -43,9 +44,10 @@ export interface ReferenceSource {
43
44
  theme?: string;
44
45
  /**
45
46
  * Arbitrary Scalar config forwarded to `<ScalarComponent>` (Scalar renderer
46
- * only). Takes precedence over Blume's derived spec/theme config.
47
+ * only). Takes precedence over Blume's derived spec/theme config. Typed off
48
+ * the config schema so the two can never drift.
47
49
  */
48
- scalar?: Record<string, unknown>;
50
+ scalar?: ResolvedConfig["openapi"]["scalar"];
49
51
  /** Display options carried through to the Blume renderer. */
50
52
  display: ReferenceDisplay;
51
53
  /**
@@ -54,12 +56,17 @@ export interface ReferenceSource {
54
56
  */
55
57
  collisions?: string[];
56
58
  }
59
+ /**
60
+ * Lowercase, hyphen-separated slug: `Add a Pet!` -> `add-a-pet`. Unicode
61
+ * letters are kept (`Größe` -> `größe`), NFC-normalized so canonically
62
+ * equivalent spellings (NFD input from macOS tooling) land on one slug.
63
+ * Non-ASCII slugs rely on the emitter percent-encoding the URL where a raw
64
+ * URI is required (sitemap, canonical).
65
+ */
57
66
  export declare const slugify: (text: string) => string;
58
- /** Normalize a configured route to a single leading slash, no trailing slash. */
59
- export declare const normalizeRoute: (route: string) => string;
60
67
  /**
61
- * Resolve every enabled reference. OpenAPI honors its `renderer` (Blume's own UI
62
- * by default); AsyncAPI is always rendered by Scalar for now.
68
+ * Resolve every enabled reference. Both blocks honor their `renderer`
69
+ * Blume's own UI by default, with the embedded Scalar SPA as the opt-out.
63
70
  */
64
71
  export declare const resolveReferences: (config: ResolvedConfig) => ReferenceSource[];
65
72
  /**
@@ -70,7 +77,7 @@ export declare const resolveReferences: (config: ResolvedConfig) => ReferenceSou
70
77
  * targets so such a tab doesn't read as a broken link.
71
78
  */
72
79
  export declare const referenceRoutes: (config: ResolvedConfig) => string[];
73
- /** Blume-rendered OpenAPI references, deduped by route (first wins). */
80
+ /** Blume-rendered references (both kinds), deduped by route (first wins). */
74
81
  export declare const blumeReferences: (config: ResolvedConfig) => ReferenceSource[];
75
82
  /** Whether any reference is Scalar-rendered (gates the `@scalar/astro` dep + pages). */
76
83
  export declare const hasScalarReferences: (config: ResolvedConfig) => boolean;
@@ -7,6 +7,7 @@
7
7
  * Astro's collections carry no schema here, so a page's `seo.x.creator` reaches
8
8
  * them as raw frontmatter, and the schema's own transform never runs on it.
9
9
  * (Blume's page pipeline does reject a non-string `creator` before the page is
10
- * built, so `unknown` is defense in depth rather than the expected path.)
10
+ * built, so the string guard is defense in depth rather than the expected
11
+ * path.)
11
12
  */
12
- export declare const normalizeXHandle: (value: unknown) => string | undefined;
13
+ export declare const normalizeXHandle: <Value>(value: Value) => string | undefined;
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  title: OpenAPI / AsyncAPI
3
- description: Drop in an OpenAPI spec and get a native API reference — one real page per operation, in your sidebar and search.
3
+ description: Drop in an OpenAPI or AsyncAPI spec and get a native API reference — one real page per operation, in your sidebar and search.
4
4
  ---
5
5
 
6
- Point Blume at an OpenAPI spec and it generates a native API reference: one **real page per operation**, grouped by tag in a tab-scoped sidebar, with schema tables, request/response examples, and generated code samples. Because each operation is a genuine Blume page, it gets its own URL, shows up in **site search** and `llms.txt`, and gets an Open Graph image — the same as any hand-written doc. The config below points Blume at the public Petstore spec as an example.
6
+ Point Blume at an OpenAPI spec and it generates a native API reference: one **real page per operation**, grouped by tag in a tab-scoped sidebar, with schema tables, request/response examples, and generated code samples. Because each operation is a genuine Blume page, it gets its own URL, shows up in **site search** and `llms.txt`, and gets an Open Graph image — the same as any hand-written doc. Event-driven APIs get the [same treatment from an AsyncAPI spec](#asyncapi). The config below points Blume at the public Petstore spec as an example.
7
7
 
8
8
  ```ts blume.config.ts lineNumbers
9
9
  openapi: {
@@ -153,7 +153,7 @@ Blume's own [`i18n`](/docs/content/i18n) translates the docs chrome, but Scalar
153
153
 
154
154
  ## AsyncAPI
155
155
 
156
- Event-driven APIs use a sibling `asyncapi` block with the same shape. AsyncAPI is rendered by Scalar (the native renderer is OpenAPI-only for now); only the default route differs (`/events`):
156
+ Event-driven APIs use a sibling `asyncapi` block with the same shape — and the same native renderer. Each `send`/`receive` operation becomes a real page with message payload and header schema tables, channel parameters, protocol bindings, and an Authorization section derived from the spec's `securitySchemes` (server-level and operation-level, alternatives as "or" groups). Only the default route differs (`/events`):
157
157
 
158
158
  ```ts blume.config.ts lineNumbers
159
159
  asyncapi: {
@@ -162,6 +162,8 @@ asyncapi: {
162
162
  }
163
163
  ```
164
164
 
165
- :::warning
166
- Scalar's AsyncAPI support is still a work in progress — it renders channels, operations, messages, and a Models section, but there's **no interactive playground** for events yet. It improves upstream over time.
167
- :::
165
+ AsyncAPI **2.x specs are normalized to 3.x automatically** with the official AsyncAPI converter, so `publish`/`subscribe` channels map onto `send`/`receive` operation pages with stable URLs — later upgrading the spec file itself through the converter moves nothing. Operations group by tag; untagged operations group under their channel address.
166
+
167
+ Code samples are **protocol-aware**, keyed off the operation's binding (or its servers' protocol): `wscat` and a browser `WebSocket` snippet for WebSockets, `kcat` for Kafka, `mosquitto_pub`/`mosquitto_sub` for MQTT. `codeSamples` filters that set, the same way it picks languages on the `openapi` block; a protocol without a supported tool renders the message payload example alone rather than a fabricated client.
168
+
169
+ Everything documented above carries over unchanged: `route`, `sources` with `label`/`route`, `expandSchemas`, the [per-source indexing](#per-source-indexing) flags, and search indexing by operation summary and tag. Setting `renderer: "scalar"` opts back into the embedded Scalar SPA — Scalar auto-detects the document type and renders channels, operations, messages, and a Models section (no interactive playground for events) — where, as with OpenAPI, only `noindex` applies.
@@ -136,6 +136,22 @@ ai: {
136
136
 
137
137
  `label` is the question that gets asked; `icon` is optional. Leave `suggestions` unset (or empty) and the panel opens to a plain input.
138
138
 
139
+ ### Custom instructions
140
+
141
+ Add your own system-prompt text with `instructions` — identity, language, tone, or anything else the assistant should keep in mind:
142
+
143
+ ```ts blume.config.ts lineNumbers
144
+ ai: {
145
+ ask: {
146
+ enabled: true,
147
+ instructions:
148
+ "You are Bloomy, the Acme docs assistant. Answer in the language the question was asked in, and keep answers under three paragraphs.",
149
+ },
150
+ }
151
+ ```
152
+
153
+ Your text is **appended to** the built-in instructions rather than replacing them: the built-in part carries the [grounding](#grounding) contract — answer only from the retrieved pages, cite them as Markdown links — that the chat panel's citations depend on, so it stays intact whatever you add.
154
+
139
155
  ### Grounding
140
156
 
141
157
  Ask AI is **grounded in your docs**. For each question it retrieves the most relevant pages — using the same lexical [Orama](/docs/configuration/search) index that powers on-page search — and injects them into the model's system prompt, so answers come from your content instead of the model's own knowledge. The assistant is told to answer only from the retrieved pages, to say when something isn't covered, and to cite the pages it drew from.
@@ -185,15 +201,17 @@ A static build with Ask AI enabled and no external `endpoint` fails fast with a
185
201
 
186
202
  By default Ask AI routes through the **Vercel AI Gateway**: `model` is a `provider/model` string, so you switch models by changing it (`openai/gpt-5.5`, `anthropic/claude-sonnet-4-5`, and so on) with no provider SDK to install. The gateway reads `AI_GATEWAY_API_KEY` from your environment and is wired up automatically when you deploy on Vercel.
187
203
 
188
- Set `provider` to point Ask AI somewhere else. Each backend reads its API key from an environment variable, and the matching provider SDK is added to your project's runtime automatically when you build — only the one you use:
204
+ Set `provider` to point Ask AI somewhere else. Each backend reads its API key from an environment variable and streams through a provider SDK you install in your project — only the one you use:
205
+
206
+ | `provider` | `model` | API key env var | SDK to install |
207
+ | --- | --- | --- | --- |
208
+ | `gateway` (default) | a `provider/model` string via the AI Gateway | `AI_GATEWAY_API_KEY` | none — ships with Blume |
209
+ | `openrouter` | any [OpenRouter](https://openrouter.ai) model | `OPENROUTER_API_KEY` | `@openrouter/ai-sdk-provider` |
210
+ | `llmgateway` | any [LLMGateway](https://llmgateway.io) model | `LLMGATEWAY_API_KEY` | `@ai-sdk/openai-compatible` |
211
+ | `inkeep` | an [Inkeep](https://inkeep.com) QA model | `INKEEP_API_KEY` | `@ai-sdk/openai-compatible` |
212
+ | `openai-compatible` | whatever your endpoint serves | set with `apiKeyEnv` | `@ai-sdk/openai-compatible` |
189
213
 
190
- | `provider` | `model` | API key env var |
191
- | --- | --- | --- |
192
- | `gateway` (default) | a `provider/model` string via the AI Gateway | `AI_GATEWAY_API_KEY` |
193
- | `openrouter` | any [OpenRouter](https://openrouter.ai) model | `OPENROUTER_API_KEY` |
194
- | `llmgateway` | any [LLMGateway](https://llmgateway.io) model | `LLMGATEWAY_API_KEY` |
195
- | `inkeep` | an [Inkeep](https://inkeep.com) QA model | `INKEEP_API_KEY` |
196
- | `openai-compatible` | whatever your endpoint serves | set with `apiKeyEnv` |
214
+ The SDKs are optional peer dependencies, so add the one your backend needs to your project (e.g. `npm install @openrouter/ai-sdk-provider`). If it's missing, the build warns with the exact package name before Vite would fail to resolve the import.
197
215
 
198
216
  For example, to use OpenRouter:
199
217
 
@@ -42,6 +42,8 @@ Write `href` as if the site were mounted at the root — a `basePath` is applied
42
42
 
43
43
  For every indexable page, Blume indexes its title, description, and body reduced to plain text — code blocks, images, and markup are stripped, so results stay relevant. The index is built from your source files, so it's identical in dev and production.
44
44
 
45
+ On a [versioned](/docs/content/versioning) site, results default to the version being viewed, with an "All versions" toggle in the dialog footer (remembered per reader). Cross-version hits name their version on the row. Orama, FlexSearch, Algolia, and Typesense honor the scoping — hosted records carry a `version` facet, with the current docs uploaded as `"current"` — while Pagefind stays unscoped, matching its locale behavior.
46
+
45
47
  ## Tags
46
48
 
47
49
  Add `search.tags` to a page's frontmatter to group it under a filter in the search dialog — readers can narrow results to a tag with a click. Tags also become a facet on the hosted providers.
@@ -249,7 +249,7 @@ URLs are absolute when `deployment.site` is set. Pages marked `seo.noindex` are
249
249
 
250
250
  ## Sitemap
251
251
 
252
- Blume writes a `sitemap.xml` of every indexable page at build time. It needs an absolute [`deployment.site`](/docs/deployment) and lists every page except drafts, hidden, and `noindex` pages. On by default:
252
+ Blume writes a `sitemap.xml` of every indexable page at build time. It needs an absolute [`deployment.site`](/docs/deployment) and lists every page except drafts, hidden, and `noindex` pages. On a [versioned](/docs/content/versioning) site, archived pages whose canonical points at their live equivalent are left out too — the live page is the one to index. On by default:
253
253
 
254
254
  ```ts blume.config.ts lineNumbers
255
255
  seo: {
@@ -3,7 +3,7 @@ title: Internationalization
3
3
  description: Serve your docs in multiple languages with locale-aware routing, per-language navigation, translated UI, and SEO — all convention-first.
4
4
  ---
5
5
 
6
- Blume serves one project in many languages. Drop translated files into the right place and Blume wires up routing, the language switcher, per-locale navigation, and SEO for you — there's no separate routing layer to maintain. It's opt-in: without an `i18n` block, your site stays single-language exactly as before.
6
+ Blume serves one project in many languages. Drop translated files into the right place and Blume wires up routing, the language switcher, per-locale navigation, and SEO for you — there's no separate routing layer to maintain. It's opt-in: without an `i18n` block, your site stays single-language exactly as before. It also composes with [versioning](/docs/content/versioning) — a frozen snapshot keeps its translations, and locale fallback works within each version.
7
7
 
8
8
  ## Enable it
9
9
 
@@ -31,11 +31,12 @@ Every field is optional — set only what you want to override.
31
31
  | `icon` | `string` | Icon shown next to the label. |
32
32
  | `order` | `number` | Position among sibling groups and pages. Lower numbers sort first. |
33
33
  | `collapsed` | `boolean` | Under the [`group` display mode](/docs/content/navigation#display-modes), whether the group starts collapsed. |
34
+ | `display` | `"flat" \| "group" \| "page"` | Render mode for this group; overrides the global [`navigation.sidebar.display`](/docs/content/navigation#display-modes). |
34
35
  | `pages` | `string[]` | Explicit order for the group's children, by slug. |
35
36
 
36
37
  The `pages` array lists children by slug — the folder or file name with its numeric prefix and any parentheses stripped (so `01-quickstart.mdx` is `"quickstart"`). Children you leave out still appear, after the listed ones.
37
38
 
38
- How groups render — flat headers, collapsible disclosures, or drill-in panels — is a sidebar-wide setting, not per folder: see [display modes](/docs/content/navigation#display-modes).
39
+ How groups render — flat headers, collapsible disclosures, or drill-in panels — defaults to the sidebar-wide `navigation.sidebar.display`; set `display` here to override it for this group alone. A folder's `index` page can also set it from frontmatter, which wins over `meta.ts` — see [per-group overrides](/docs/content/navigation#per-group-overrides).
39
40
 
40
41
  ## Computed meta
41
42
 
@@ -10,6 +10,7 @@ export default defineMeta({
10
10
  "islands",
11
11
  "sources",
12
12
  "i18n",
13
+ "versioning",
13
14
  ],
14
15
  title: "Content",
15
16
  });
@@ -69,7 +69,39 @@ navigation: {
69
69
  `page` mode keeps deep sections tidy — reach for it when groups have many children and you'd rather drill into them than scroll past them.
70
70
  :::
71
71
 
72
- A group in an [explicit sidebar](#explicit-sidebar) can override the global mode with its own `display`.
72
+ ### Per-group overrides
73
+
74
+ Any generated group can opt out of the global mode — no explicit sidebar required. Set `display` in the folder's [`meta.ts`](/docs/content/meta), or — when the folder has an `index` page — under `sidebar` in that page's frontmatter, and only that group changes:
75
+
76
+ ```ts meta.ts
77
+ import { defineMeta } from "blume";
78
+
79
+ export default defineMeta({
80
+ title: "Client SDKs",
81
+ display: "page",
82
+ });
83
+ ```
84
+
85
+ ```yaml index.mdx
86
+ ---
87
+ title: Client SDKs
88
+ sidebar:
89
+ display: page
90
+ ---
91
+ ```
92
+
93
+ A generated group's effective mode resolves highest priority first:
94
+
95
+ 1. `sidebar.display` in the group's own `index` page frontmatter
96
+ 2. `display` in the folder's `meta.ts`
97
+ 3. The global `navigation.sidebar.display`
98
+ 4. The Blume default (`flat`)
99
+
100
+ A group's `display` applies to that group only — nested subgroups resolve their own value through the same chain. A `page`-mode group with an index page still drills into its sub-panel: the index page lists as the panel's first item, and landing on its URL opens the panel directly.
101
+
102
+ `sidebar.display` means nothing anywhere else — on a non-index page, on the content root's own `index` page (the root isn't a group; use `navigation.sidebar.display`), or on any page when an [explicit sidebar](#explicit-sidebar) is configured (its items own each group's mode) — so Blume reports a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning instead of silently dropping it. `collapsed` remains specific to `group` mode; it's inert when a group resolves to `flat` or `page`.
103
+
104
+ A group in an [explicit sidebar](#explicit-sidebar) overrides the global mode with its own `display`, exactly as before.
73
105
 
74
106
  ## Ordering
75
107
 
@@ -171,6 +203,8 @@ navigation: {
171
203
 
172
204
  Each item takes a `label`, a `path`, and optional `icon`, `description`, and `tag`. `kind` (`dropdown`, `product`, `version`, or `language`) is a hint for how the selector is used; all render the same dropdown.
173
205
 
206
+ With [versioning](/docs/content/versioning) configured, Blume renders a version selector automatically — declaring your own `kind: "version"` selector here replaces the automatic one, so hand-rolled setups keep working.
207
+
174
208
  ## Featured links
175
209
 
176
210
  Pin links to the top of the sidebar, above every section — a blog, a changelog, a contact or support page that should always be one click away. Unlike the generated tree, featured links are **not scoped by tab**: they show on every route, on every breakpoint.
@@ -146,7 +146,7 @@ export default defineConfig({
146
146
  });
147
147
  ```
148
148
 
149
- The integration token comes from the `NOTION_TOKEN` environment variable (share the database with your integration). By default every page is imported; set `publishedValue` to make the `Status` property a publish gate — any other value then maps to `draft: true`, which production builds drop. **Notion image URLs are signed and expire**, so the adapter downloads them at build time into the site's assets and rewrites the references — a CMS image never rots a static build.
149
+ The integration token comes from the `NOTION_TOKEN` environment variable (share the database with your integration). By default every page is imported; set `publishedValue` to make the `Status` property a publish gate — any other value then maps to `draft: true`, which production builds drop. **Notion image URLs are signed and expire**, so the adapter downloads them at build time into the site's assets and rewrites the references — a CMS image never rots a static build. API calls are paced through a small request pool (3 at a time, matching Notion's per-integration rate limit) so databases with hundreds of pages import without tripping `429` responses; set `concurrency` on the source to tune it.
150
150
 
151
151
  ## Preview and sync
152
152
 
@@ -0,0 +1,106 @@
1
+ ---
2
+ title: Versioning
3
+ description: Freeze snapshots of your docs per release with a version switcher, canonical-to-latest SEO, version-scoped search, and a version-aware agent surface.
4
+ ---
5
+
6
+ Blume versions your docs the way releases actually work: the latest documentation lives at your content root with clean, unprefixed URLs, and each past version is a frozen snapshot in its own folder. Cut a snapshot when you ship, and Blume wires up the switcher, the "old version" notice, search scoping, SEO, and the agent surface for you. It's opt-in: without a `versions` block, nothing changes.
7
+
8
+ ## Enable it
9
+
10
+ Add a `versions` block naming the current docs and any archived snapshots:
11
+
12
+ ```ts blume.config.ts lineNumbers
13
+ versions: {
14
+ current: { label: "v2.0", badge: "Latest" },
15
+ archived: [
16
+ { id: "v1.0" },
17
+ { id: "v0.9", label: "0.9 (legacy)" },
18
+ ],
19
+ }
20
+ ```
21
+
22
+ `current` labels the unprefixed tree in the switcher (with an optional `badge`). Each archived entry's `id` is both the snapshot's directory name and its URL segment — ids must start with a letter (`v1.0`, not `1.0`) so they can never collide with [numeric ordering prefixes](/docs/content/navigation#ordering). List archived versions newest first; that order is the switcher order.
23
+
24
+ ## Cut a version
25
+
26
+ When you release, freeze the current docs with one command:
27
+
28
+ ```bash
29
+ blume version v1.0
30
+ ```
31
+
32
+ This copies your content tree into `docs/v1.0/` (existing snapshots excluded), rewrites root-absolute links inside the copy so they stay within the snapshot (`/guides/x` becomes `/v1.0/guides/x`, fenced and inline code untouched), and registers the id in `blume.config.ts` — or prints the entry to paste when your config is shaped in a way it won't touch. Links to pages that aren't part of the copied tree — generated API references, remote sources like a changelog — keep pointing at the live pages, since the snapshot has no copy of them. Run `blume version` with no id to list the configured versions.
33
+
34
+ Review and commit the new directory like any other content. Restart `blume dev` to pick it up.
35
+
36
+ ```txt
37
+ docs/
38
+ index.mdx -> / (latest)
39
+ guides/quickstart.mdx -> /guides/quickstart
40
+ v1.0/
41
+ index.mdx -> /v1.0 (frozen)
42
+ guides/quickstart.mdx -> /v1.0/guides/quickstart
43
+ ```
44
+
45
+ **Archived means frozen.** Future edits belong in the live tree; a snapshot is the docs as they were. Blume leans on that: snapshots keep their own folder meta and translations, [`blume translate`](/docs/reference/translate) never retranslates them, and a configured explicit sidebar applies only to the current docs — a snapshot's sidebar always comes from its own files.
46
+
47
+ ## The switcher and the notice
48
+
49
+ With versions configured, the header grows a version dropdown automatically. Switching lands on the same page in the target version when it exists, and on that version's root when it doesn't (set `switcher.redirect: "root"` to always land on the root). If you declare your own `kind: "version"` selector in [`navigation.selectors`](/docs/content/navigation#selectors), it replaces the automatic one.
50
+
51
+ Every archived page also shows a non-dismissible notice with a "Go to latest" link pointing at the page's live equivalent. Customize or disable it per version:
52
+
53
+ ```ts blume.config.ts
54
+ archived: [
55
+ { id: "v1.0", banner: "These docs cover the 1.x SDK." },
56
+ { id: "v0.9", banner: false },
57
+ ];
58
+ ```
59
+
60
+ ## SEO
61
+
62
+ Old docs are search engines' favorite trap: the stale page outranks the live one, or both compete. Blume defaults to the answer SEO guides recommend and no other docs framework automates — archived pages stay indexable but declare the **latest equivalent as their canonical**, so the live page is authoritative while version-only content (a page that no longer exists in the latest docs) remains findable with a self-canonical.
63
+
64
+ Per version you can pick a different treatment:
65
+
66
+ ```ts blume.config.ts
67
+ archived: [
68
+ { id: "v1.0" }, // canonical → latest (default)
69
+ { id: "v0.9", canonical: "self" }, // every page authoritative
70
+ { id: "v0.8", noindex: true }, // deindexed entirely
71
+ ];
72
+ ```
73
+
74
+ The sitemap follows suit: archived pages whose canonical points at a live equivalent are left out, `noindex` versions are left out wholesale, and version-only pages stay listed. A page's own `seo.canonical` frontmatter always wins.
75
+
76
+ ## Search
77
+
78
+ The search dialog scopes results to the version being viewed, with an "All versions" toggle (remembered per reader) beside the language one. Cross-version hits name their version on the result row. Orama (the default), FlexSearch, Algolia, and Typesense all honor the scoping — hosted records carry a `version` facet, with the current docs uploaded as `"current"` — while Pagefind stays unscoped, matching its locale behavior.
79
+
80
+ ## Agents
81
+
82
+ The agent surface is version-aware — something no other docs framework does:
83
+
84
+ - The MCP `search_docs` and `list_pages` tools default to the current docs and accept `version`: an archived id (`"v1.0"`) or `"all"`. `get_navigation` returns an archived snapshot's tree on request.
85
+ - `llms.txt` sections archived versions after the current docs, labeled `1.0 (archived)`, so an agent reading the index knows which docs are frozen.
86
+ - `llms-full.txt` stays current-only — the flat dump never interleaves frozen copies of the same page.
87
+ - Raw Markdown mirrors (`.md` URLs) exist for every version's pages, as for any route.
88
+
89
+ ## With i18n
90
+
91
+ Versioning composes with [internationalization](/docs/content/i18n). On disk the version folder is outermost — a snapshot naturally contains its locale folders — while in URLs the locale stays outermost, matching the rest of the site:
92
+
93
+ ```txt
94
+ docs/
95
+ guides/x.mdx -> /guides/x
96
+ fr/guides/x.mdx -> /fr/guides/x
97
+ v1.0/
98
+ guides/x.mdx -> /v1.0/guides/x
99
+ fr/guides/x.mdx -> /fr/v1.0/guides/x
100
+ ```
101
+
102
+ Locale fallback works within each version: an untranslated snapshot page renders the fallback locale's content at the localized URL, and `hreflang` alternates group per version. A version id may not collide with a configured locale code — Blume rejects that config outright.
103
+
104
+ ## What stays unversioned
105
+
106
+ Versioning covers the docs content tree. The blog, changelog, API references generated from OpenAPI specs, and custom pages are always current. Two more behaviors worth knowing: header tabs are defined against the current docs, so inside an archived tree the sidebar renders unscoped by tabs; and large sites should note each snapshot is a full copy — content, search index entries, and navigation data all grow per version.
@@ -24,6 +24,7 @@ blume <command> [options]
24
24
  | `blume audit` | Audit the built site for SEO and health issues. |
25
25
  | `blume eval` | Test the docs: an agent answers your questions using only the documentation. |
26
26
  | `blume translate` | Translate docs into the configured locales with a local agent CLI. |
27
+ | `blume version [id]` | Freeze the current docs as an [archived version](/docs/content/versioning) (no id lists configured versions). |
27
28
 
28
29
  ## Common flags
29
30
 
@@ -46,8 +46,11 @@ sidebar:
46
46
  icon: download
47
47
  badge: New
48
48
  hidden: false
49
+ display: page
49
50
  ```
50
51
 
52
+ `display` sets the render mode of the page's folder group ([per-group overrides](/docs/content/navigation#per-group-overrides)) and is only meaningful on a folder's `index` page under the generated sidebar — anywhere else (a non-index page, the content root's own `index` page, or any page under an explicit `navigation.sidebar`) it has no group to configure, and Blume warns with `BLUME_SIDEBAR_DISPLAY_IGNORED`.
53
+
51
54
  ## SEO
52
55
 
53
56
  ```yaml lineNumbers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -72,6 +72,7 @@
72
72
  "@astrojs/node": "^11.0.0",
73
73
  "@astrojs/react": "^6.0.0",
74
74
  "@astrojs/vercel": "^11.0.3",
75
+ "@asyncapi/converter": "^2.0.2",
75
76
  "@clack/prompts": "^1.7.0",
76
77
  "@iconify-json/lucide": "^1.2.115",
77
78
  "@iconify/types": "^2.0.0",
@@ -86,6 +87,7 @@
86
87
  "@shikijs/twoslash": "^4.2.0",
87
88
  "@tailwindcss/typography": "^0.5.20",
88
89
  "@tailwindcss/vite": "^4",
90
+ "@types/mdast": "^4.0.4",
89
91
  "@vercel/analytics": "^2.0.1",
90
92
  "ai": "^7.0.42",
91
93
  "astro": "^7.1.0",
@@ -93,6 +95,7 @@
93
95
  "chokidar": "^5.0.0",
94
96
  "citty": "^0.1.6",
95
97
  "consola": "^3.4.0",
98
+ "cross-spawn": "^7.0.6",
96
99
  "dompurify": "^3.4.13",
97
100
  "dotenv": "^17.4.2",
98
101
  "epub-gen-memory": "^1.1.2",
@@ -113,10 +116,16 @@
113
116
  "medium-zoom": "^1.1.0",
114
117
  "mermaid": "^11.16.1",
115
118
  "micromark-extension-gfm": "^3.0.0",
119
+ "nanotar": "^0.3.0",
116
120
  "node-html-parser": "^9.0.0",
117
121
  "openapi-sampler": "^1.7.4",
122
+ "p-limit": "^7.3.1",
123
+ "p-map": "^7.0.6",
124
+ "p-retry": "^8.0.0",
125
+ "package-manager-detector": "^1.8.0",
118
126
  "pagefind": "^1.3.0",
119
127
  "pathe": "^2.0.0",
128
+ "perfect-debounce": "^2.1.0",
120
129
  "picomatch": "^4.0.5",
121
130
  "react": "^19.0.0",
122
131
  "react-dom": "^19.0.0",
@@ -126,16 +135,19 @@
126
135
  "sharp": "^0.35.3",
127
136
  "shiki": "^4.2.0",
128
137
  "simple-icons": "^13.0.0",
138
+ "string-width": "^8.1.0",
129
139
  "tailwindcss": "^4.3.3",
130
140
  "takumi-js": "^2.2.1",
131
141
  "tinyglobby": "^0.2.10",
132
142
  "twoslash": "^0.3.9",
133
143
  "typescript": "^6.0.3",
144
+ "ufo": "^1.6.4",
134
145
  "undici": "^8.9.0",
135
146
  "write-file-atomic": "^8.0.0",
136
147
  "zod": "^4.3.6"
137
148
  },
138
149
  "devDependencies": {
150
+ "@types/cross-spawn": "^6.0.6",
139
151
  "@types/html-escaper": "^3.0.4",
140
152
  "@types/js-yaml": "^4.0.9",
141
153
  "@types/node": "^22.10.0",
@@ -44,8 +44,8 @@ The single biggest shift for most sources — especially Mintlify — is that **
44
44
 
45
45
  - **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name.
46
46
  - **Ordering resolves highest-priority-first:** an explicit `navigation.sidebar` (replaces the whole tree) → a folder's `meta.ts` `pages` array → a page's frontmatter `sidebar.order` → the filesystem (`index` first, then numeric filename prefix like `01-`, then alphabetical).
47
- - **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages })`). The `pages` array lists children by slug (numeric prefix and parentheses stripped); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order.
48
- - **Sidebar render mode is global, not per-folder.** `navigation.sidebar.display` in `blume.config.ts` is `"flat"` (default), `"group"` (collapsible), or `"page"` (drill-in sub-panel) and applies to **every** group at once. (It used to live on each folder's `meta.ts` as `display`; that field was **removed** writing it in a `meta.ts` is now a build error. Set it once in config instead.) An explicit `navigation.sidebar` item may still override its own group's `display`.
47
+ - **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages, display })`). The `pages` array lists children by slug (numeric prefix and parentheses stripped); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order.
48
+ - **Sidebar render mode: a global default with per-folder overrides.** `navigation.sidebar.display` in `blume.config.ts` is `"flat"` (default), `"group"` (collapsible), or `"page"` (drill-in sub-panel) and sets the mode for every group at once. A folder can override its **own** group: `display` in its `meta.ts`, or — sugar when the folder has an `index` page `sidebar.display` in that index page's frontmatter. Precedence: index frontmatter `meta.ts` global config `flat`; an override applies to that one group only (nested subgroups resolve their own chain). So a source's per-category collapse/drill-in modes migrate per folder — only reach for the global mode when the whole sidebar changes. Under an explicit `navigation.sidebar`, the config item's own `display` field is the only per-group control (frontmatter/meta `display` is ignored there, with a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning).
49
49
  - **An explicit `navigation.sidebar` replaces filesystem generation entirely.** Use it only for a nav shape files can't express. Its items are a page route string, a group (`{ label, items }`), or a link (`{ label, href }`).
50
50
  - **Config-declared nesting has no on-disk counterpart — materialize it or it flattens silently.** When a source (Mintlify `groups`, Nextra `_meta`, a Docusaurus sidebar…) declares a nested group, its pages usually sit **flat in one folder** and the grouping lives only in config. Filesystem-derived nav sees the flat folder and drops the inner group. To keep the nesting you must **either** move those pages into a real subfolder (`meta.ts` for label/`collapsed`) — which changes their URLs, so add `redirects` — **or** declare the group in an explicit `navigation.sidebar`, which nests the existing routes without moving a file. Walk config `pages`/nav arrays **recursively** during inventory and record where config nesting depth exceeds on-disk depth; that gap is exactly what gets lost.
51
51
 
@@ -41,7 +41,7 @@ Every Docusaurus repo serves **`static/`** at the site root (`static/img/foo.png
41
41
  ## Navigation: `sidebars.js` + `_category_.json`
42
42
 
43
43
  - **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed.
44
- - **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed` (**no `display` field in `meta.ts`** if collapse matters, set the global `navigation.sidebar.display: "group"` once in config).
44
+ - **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed`, and collapsible rendering that folder's `meta.ts` `display: "group"` (or `navigation.sidebar.display: "group"` once in config when every category collapses).
45
45
  - **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible`/`className`/`customProps` → drop (report).
46
46
  - **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way.
47
47
 
@@ -39,7 +39,7 @@ Everything else is `defineConfig({ title })`.
39
39
  | `description` | **drop** (folders have no description) |
40
40
  | `collapsible: false` | **drop** (report — no per-folder equivalent) |
41
41
 
42
- `meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages` there is **no `display` field** (writing one is a build error). Collapse rendering is global: when any folder needs collapsing, set `navigation.sidebar.display: "group"` once in `blume.config.ts`.
42
+ `meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages`, `display`. Render mode is per-folder or global: a folder that needs collapsible rendering sets its own `meta.ts` `display: "group"` (drill-in is `"page"`); when the whole sidebar should collapse, set `navigation.sidebar.display: "group"` once in `blume.config.ts` instead of repeating it per folder.
43
43
 
44
44
  **`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`.
45
45
 
@@ -83,7 +83,7 @@ The codemod touches **only frontmatter**. Icons in MDX **body** (`<Icon icon="
83
83
 
84
84
  Mintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim:
85
85
 
86
- - **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — is **not** a `meta.ts` field; it's the global `navigation.sidebar.display` in `blume.config.ts`.)
86
+ - **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages, display })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — defaults to the global `navigation.sidebar.display` in `blume.config.ts`; a folder overrides its own group with `meta.ts` `display`, or `sidebar.display` in its `index` page's frontmatter.)
87
87
  - **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here.
88
88
  - **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten.
89
89
  - **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `"GET /path"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI).