blume 1.4.3 → 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 (194) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/index.js +1621 -576
  3. package/dist/cli/index.js.map +109 -104
  4. package/dist/types/ai/component-markdown.d.ts +14 -4
  5. package/dist/types/core/config-input.d.ts +79 -27
  6. package/dist/types/core/config.d.ts +2 -1
  7. package/dist/types/core/data.d.ts +16 -1
  8. package/dist/types/core/diagnostics.d.ts +5 -1
  9. package/dist/types/core/i18n-ui.d.ts +12 -0
  10. package/dist/types/core/schema.d.ts +112 -15
  11. package/dist/types/core/sources/types.d.ts +3 -1
  12. package/dist/types/core/standard-schema.d.ts +7 -3
  13. package/dist/types/core/types.d.ts +43 -2
  14. package/dist/types/core/ui-packs/index.d.ts +9 -1
  15. package/dist/types/openapi/references.d.ts +6 -5
  16. package/dist/types/seo/x-handle.d.ts +3 -2
  17. package/docs/advanced/api-reference.mdx +8 -6
  18. package/docs/configuration/search.mdx +2 -0
  19. package/docs/configuration/seo.mdx +1 -1
  20. package/docs/content/i18n.mdx +1 -1
  21. package/docs/content/meta.mdx +2 -1
  22. package/docs/content/meta.ts +1 -0
  23. package/docs/content/navigation.mdx +35 -1
  24. package/docs/content/versioning.mdx +106 -0
  25. package/docs/reference/cli.mdx +1 -0
  26. package/docs/reference/frontmatter.mdx +3 -0
  27. package/package.json +3 -1
  28. package/skills/blume-migrate/SKILL.md +2 -2
  29. package/skills/blume-migrate/references/docusaurus.md +1 -1
  30. package/skills/blume-migrate/references/fumadocs.md +1 -1
  31. package/skills/blume-migrate/references/mintlify.md +1 -1
  32. package/src/ai/agent-readability.ts +37 -10
  33. package/src/ai/ask-context.ts +5 -1
  34. package/src/ai/ask.ts +10 -1
  35. package/src/ai/component-markdown.ts +80 -43
  36. package/src/ai/llms.ts +40 -16
  37. package/src/ai/mcp/data.ts +48 -12
  38. package/src/ai/mcp/discovery.ts +28 -11
  39. package/src/ai/mcp/server.ts +183 -38
  40. package/src/ai/mcp/tools.ts +3 -3
  41. package/src/ai/skills.ts +32 -9
  42. package/src/ai/visibility.ts +2 -2
  43. package/src/astro/component-slots.ts +2 -0
  44. package/src/astro/examples.ts +6 -2
  45. package/src/astro/generate.ts +54 -29
  46. package/src/astro/integration.ts +13 -2
  47. package/src/astro/islands.ts +16 -9
  48. package/src/astro/templates.ts +152 -33
  49. package/src/audit/agent.ts +2 -2
  50. package/src/audit/checks/content.ts +26 -11
  51. package/src/audit/checks/dns-aid.ts +3 -0
  52. package/src/audit/checks/indexability.ts +24 -6
  53. package/src/audit/checks/llms.ts +9 -4
  54. package/src/audit/checks/network.ts +2 -0
  55. package/src/audit/checks/social.ts +18 -10
  56. package/src/audit/crawl.ts +37 -9
  57. package/src/audit/report.ts +20 -19
  58. package/src/audit/run.ts +5 -2
  59. package/src/audit/snapshot.ts +2 -4
  60. package/src/audit/types.ts +25 -3
  61. package/src/blume-modules.d.ts +5 -1
  62. package/src/cli/commands/audit.ts +9 -4
  63. package/src/cli/commands/build.ts +15 -9
  64. package/src/cli/commands/dev.ts +2 -0
  65. package/src/cli/commands/doctor.ts +2 -0
  66. package/src/cli/commands/eval.ts +7 -3
  67. package/src/cli/commands/init.ts +9 -9
  68. package/src/cli/commands/mcp-stdio.ts +3 -0
  69. package/src/cli/commands/translate.ts +14 -3
  70. package/src/cli/commands/version.ts +85 -0
  71. package/src/cli/dev-lock.ts +31 -10
  72. package/src/cli/eject-scripts.ts +17 -2
  73. package/src/cli/index.ts +2 -0
  74. package/src/cli/init/questions.ts +1 -1
  75. package/src/cli/init/scaffold.ts +22 -15
  76. package/src/cli/internal-error.ts +1 -0
  77. package/src/components/content/auto-type-table.ts +3 -0
  78. package/src/components/content/diff.ts +9 -5
  79. package/src/components/content/github-info.ts +2 -0
  80. package/src/components/islands/ask-ai.tsx +33 -25
  81. package/src/components/islands/hooks.ts +5 -1
  82. package/src/components/islands/webmcp.ts +49 -12
  83. package/src/components/layout/Header.astro +25 -1
  84. package/src/components/layout/NavSelector.astro +11 -2
  85. package/src/components/layout/NavTree.astro +4 -2
  86. package/src/components/layout/RootLayout.astro +18 -0
  87. package/src/components/layout/Search.astro +77 -13
  88. package/src/components/layout/VersionBanner.astro +39 -0
  89. package/src/components/layout/analytics-client.ts +8 -5
  90. package/src/components/layout/hydration-hint.ts +1 -1
  91. package/src/components/layout/nav-utils.ts +1 -4
  92. package/src/components/layout/overrides.ts +25 -12
  93. package/src/components/layout/search/algolia.ts +18 -5
  94. package/src/components/layout/search/endpoint.ts +3 -0
  95. package/src/components/layout/search/flexsearch.ts +23 -7
  96. package/src/components/layout/search/orama-cloud.ts +1 -1
  97. package/src/components/layout/search/orama.ts +4 -1
  98. package/src/components/layout/search/pagefind.ts +2 -0
  99. package/src/components/layout/search/types.ts +13 -1
  100. package/src/components/layout/search/typesense.ts +19 -3
  101. package/src/components/openapi/ApiOverview.astro +32 -6
  102. package/src/components/openapi/AsyncApiOperation.astro +237 -0
  103. package/src/components/openapi/Bindings.astro +89 -0
  104. package/src/components/openapi/MethodBadge.astro +3 -0
  105. package/src/components/openapi/Operation.astro +7 -2
  106. package/src/components/openapi/PanelTabs.astro +131 -0
  107. package/src/components/openapi/ParametersTable.astro +2 -0
  108. package/src/components/openapi/RequestPanel.astro +12 -119
  109. package/src/components/openapi/async-snippets.ts +174 -0
  110. package/src/components/openapi/async.ts +348 -0
  111. package/src/components/openapi/helpers.ts +52 -20
  112. package/src/components/openapi/security.ts +102 -29
  113. package/src/components/openapi/snippets.ts +11 -11
  114. package/src/core/component-overrides.ts +28 -23
  115. package/src/core/config-input.ts +88 -27
  116. package/src/core/config.ts +20 -7
  117. package/src/core/content.ts +3 -1
  118. package/src/core/data.ts +16 -1
  119. package/src/core/define-components.ts +5 -0
  120. package/src/core/diagnostics.ts +46 -38
  121. package/src/core/frontmatter.ts +33 -7
  122. package/src/core/graph.ts +137 -53
  123. package/src/core/i18n-ui.ts +15 -0
  124. package/src/core/i18n.ts +16 -8
  125. package/src/core/load-module.ts +1 -0
  126. package/src/core/manifest.ts +92 -3
  127. package/src/core/meta.ts +44 -14
  128. package/src/core/nav-diagnostics.ts +3 -3
  129. package/src/core/navigation.ts +247 -67
  130. package/src/core/project-graph.ts +15 -3
  131. package/src/core/schema.ts +213 -67
  132. package/src/core/sources/assets.ts +2 -0
  133. package/src/core/sources/cache.ts +6 -0
  134. package/src/core/sources/github-releases.ts +39 -31
  135. package/src/core/sources/mdx-remote.ts +4 -0
  136. package/src/core/sources/normalize.ts +67 -20
  137. package/src/core/sources/notion.ts +49 -17
  138. package/src/core/sources/portable-text.ts +32 -11
  139. package/src/core/sources/sanity.ts +68 -14
  140. package/src/core/sources/types.ts +4 -0
  141. package/src/core/sources/watch.ts +1 -1
  142. package/src/core/standard-schema.ts +9 -3
  143. package/src/core/text-width.ts +26 -0
  144. package/src/core/tsconfig-aliases.ts +9 -5
  145. package/src/core/types.ts +45 -2
  146. package/src/core/ui-packs/index.ts +9 -1
  147. package/src/core/version-cut.ts +301 -0
  148. package/src/core/version.ts +2 -0
  149. package/src/core/versions.ts +170 -0
  150. package/src/deploy/adapter-output.ts +5 -2
  151. package/src/deploy/cloudflare-negotiation.ts +25 -10
  152. package/src/deploy/sitemap.ts +33 -1
  153. package/src/deploy/vercel-negotiation.ts +11 -4
  154. package/src/eval/report.ts +4 -4
  155. package/src/eval/run.ts +2 -2
  156. package/src/eval/schema.ts +1 -1
  157. package/src/markdown/base-links.ts +6 -6
  158. package/src/markdown/directives.ts +7 -1
  159. package/src/markdown/heading-anchors.ts +17 -6
  160. package/src/markdown/index.ts +73 -24
  161. package/src/markdown/inline-code.ts +14 -2
  162. package/src/markdown/language-icon.ts +6 -2
  163. package/src/markdown/mdast.ts +18 -4
  164. package/src/markdown/package-commands.ts +6 -8
  165. package/src/markdown/table-wrap.ts +4 -1
  166. package/src/markdown/twoslash.ts +2 -0
  167. package/src/og/card.ts +30 -11
  168. package/src/og/derive.ts +43 -27
  169. package/src/openapi/asyncapi.ts +366 -0
  170. package/src/openapi/model.ts +126 -57
  171. package/src/openapi/parse.ts +97 -5
  172. package/src/openapi/references.ts +12 -10
  173. package/src/openapi/render-mdx.ts +73 -34
  174. package/src/openapi/scalar.ts +6 -8
  175. package/src/openapi/source.ts +98 -28
  176. package/src/registry/eject.ts +7 -2
  177. package/src/search/documents.ts +25 -5
  178. package/src/search/facets.ts +7 -5
  179. package/src/search/orama-index.ts +66 -20
  180. package/src/search/popular.ts +10 -5
  181. package/src/search/providers.ts +2 -2
  182. package/src/search/sync/index.ts +2 -0
  183. package/src/search/sync/typesense.ts +4 -2
  184. package/src/seo/jsonld.ts +24 -6
  185. package/src/seo/x-handle.ts +8 -3
  186. package/src/theme/chrome-icons.ts +7 -2
  187. package/src/theme/fonts.ts +8 -4
  188. package/src/theme/icons.ts +4 -2
  189. package/src/theme/palette.ts +22 -14
  190. package/src/translate/meta.ts +15 -6
  191. package/src/translate/report.ts +9 -5
  192. package/src/translate/run.ts +10 -4
  193. package/src/translate/validate.ts +52 -17
  194. package/src/translate/work-list.ts +0 -0
@@ -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.
@@ -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.
@@ -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.3",
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",
@@ -134,6 +135,7 @@
134
135
  "sharp": "^0.35.3",
135
136
  "shiki": "^4.2.0",
136
137
  "simple-icons": "^13.0.0",
138
+ "string-width": "^8.1.0",
137
139
  "tailwindcss": "^4.3.3",
138
140
  "takumi-js": "^2.2.1",
139
141
  "tinyglobby": "^0.2.10",
@@ -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).
@@ -47,12 +47,41 @@ const askApiUrl = (
47
47
  : endpoint;
48
48
  };
49
49
 
50
+ /** The `.well-known` discovery URLs a site can publish. */
51
+ interface WellKnownArtifacts {
52
+ httpMessageSignaturesDirectory?: string;
53
+ apiCatalog?: string;
54
+ agentSkills?: string;
55
+ }
56
+
57
+ /** The agent-facing artifact index the manifest publishes. */
58
+ interface AgentArtifacts extends WellKnownArtifacts {
59
+ markdown: { contentNegotiation?: string; pattern: string };
60
+ llmsFullTxt?: string;
61
+ llmsTxt?: string;
62
+ mcp?: { discovery: string; url: string };
63
+ askApi?: string;
64
+ sitemap?: string;
65
+ feeds?: string[];
66
+ }
67
+
68
+ /** The published `agent-readability.json` document. */
69
+ export interface AgentReadabilityManifest {
70
+ artifacts: AgentArtifacts;
71
+ description?: string;
72
+ generator?: string;
73
+ name: string;
74
+ site: string | null;
75
+ contentUsage?: Record<string, boolean>;
76
+ repository?: string;
77
+ }
78
+
50
79
  /** The `.well-known` discovery artifacts the site publishes, if any. */
51
80
  const wellKnownArtifacts = (
52
81
  config: BlumeProject["config"],
53
82
  abs: (path: string) => string
54
- ): Record<string, string> => {
55
- const artifacts: Record<string, string> = {};
83
+ ): WellKnownArtifacts => {
84
+ const artifacts: WellKnownArtifacts = {};
56
85
  if (config.ai.webBotAuth.keys.length > 0) {
57
86
  artifacts.httpMessageSignaturesDirectory = abs(
58
87
  "/.well-known/http-message-signatures-directory"
@@ -77,7 +106,7 @@ const wellKnownArtifacts = (
77
106
  */
78
107
  export const buildAgentReadability = (
79
108
  project: BlumeProject
80
- ): Record<string, unknown> | null => {
109
+ ): AgentReadabilityManifest | null => {
81
110
  const { config } = project;
82
111
  if (!config.seo.agentReadability) {
83
112
  return null;
@@ -103,12 +132,10 @@ export const buildAgentReadability = (
103
132
  config.deployment.output === "server" &&
104
133
  (config.deployment.adapter === "vercel" ||
105
134
  config.deployment.adapter === "cloudflare");
106
- const artifacts: Record<string, unknown> = {
107
- markdown: {
108
- ...(negotiates ? { contentNegotiation: "text/markdown" } : {}),
109
- pattern: abs("/{route}.md"),
110
- },
111
- };
135
+ const markdown: AgentArtifacts["markdown"] = negotiates
136
+ ? { contentNegotiation: "text/markdown", pattern: abs("/{route}.md") }
137
+ : { pattern: abs("/{route}.md") };
138
+ const artifacts: AgentArtifacts = { markdown };
112
139
  if (config.ai.llmsTxt.enabled) {
113
140
  artifacts.llmsFullTxt = abs("/llms-full.txt");
114
141
  artifacts.llmsTxt = abs("/llms.txt");
@@ -135,7 +162,7 @@ export const buildAgentReadability = (
135
162
  }
136
163
 
137
164
  const version = project.manifest?.blumeVersion;
138
- const manifest: Record<string, unknown> = {
165
+ const manifest: AgentReadabilityManifest = {
139
166
  artifacts,
140
167
  description: config.description,
141
168
  generator: version ? `blume@${version}` : undefined,
@@ -102,9 +102,13 @@ const TERM = /[\p{L}\p{M}\p{N}]+/gu;
102
102
  * window silently degrades to the head of the page. The regex fallback covers
103
103
  * runtimes without the segmenter and still handles spaced scripts correctly.
104
104
  */
105
+ const hasSegmenter = (
106
+ segmenter: typeof Intl.Segmenter | undefined
107
+ ): segmenter is typeof Intl.Segmenter => typeof segmenter === "function";
108
+
105
109
  const segmentQuery = (query: string): string[] => {
106
110
  const lowered = query.normalize("NFC").toLowerCase();
107
- if (typeof Intl.Segmenter !== "function") {
111
+ if (!hasSegmenter(Intl.Segmenter)) {
108
112
  return lowered.match(TERM) ?? [];
109
113
  }
110
114
  const pieces: string[] = [];
package/src/ai/ask.ts CHANGED
@@ -35,7 +35,16 @@ const OPENAI_COMPATIBLE = "openai-compatible";
35
35
  /** The AI SDK provider package the OpenAI-compatible providers install. */
36
36
  const OPENAI_COMPATIBLE_DEP = "@ai-sdk/openai-compatible";
37
37
 
38
- const ASK_PRESETS: Record<string, AskPreset> = {
38
+ /**
39
+ * Connection presets keyed by provider name. Open-keyed on purpose: lookups
40
+ * use the configured `ai.ask.provider`, which includes values with no preset
41
+ * (the generic `openai-compatible`).
42
+ */
43
+ interface AskPresetRegistry {
44
+ [provider: string]: AskPreset;
45
+ }
46
+
47
+ const ASK_PRESETS: AskPresetRegistry = {
39
48
  inkeep: {
40
49
  apiKeyEnv: "INKEEP_API_KEY",
41
50
  baseUrl: "https://api.inkeep.com/v1",