blume 0.7.0 → 1.0.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 (185) hide show
  1. package/CHANGELOG.md +666 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/dist/cli/index.js +1852 -380
  5. package/dist/cli/index.js.map +98 -91
  6. package/dist/types/ai/component-markdown.d.ts +34 -0
  7. package/dist/types/components/content/youtube.d.ts +18 -0
  8. package/dist/types/core/base-path.d.ts +9 -0
  9. package/dist/types/core/config-input.d.ts +47 -2
  10. package/dist/types/core/config.d.ts +3 -2
  11. package/dist/types/core/data.d.ts +7 -0
  12. package/dist/types/core/i18n-ui.d.ts +526 -132
  13. package/dist/types/core/schema.d.ts +293 -146
  14. package/dist/types/index.d.ts +1 -0
  15. package/dist/types/openapi/references.d.ts +60 -0
  16. package/dist/types/seo/x-handle.d.ts +12 -0
  17. package/docs/01-quickstart.mdx +5 -2
  18. package/docs/02-deployment.mdx +8 -8
  19. package/docs/03-faq.mdx +46 -16
  20. package/docs/advanced/api-reference.mdx +1 -1
  21. package/docs/advanced/changelog.mdx +1 -1
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/skills.mdx +1 -1
  24. package/docs/configuration/ai.mdx +49 -10
  25. package/docs/configuration/customization.mdx +11 -0
  26. package/docs/configuration/export.mdx +1 -1
  27. package/docs/configuration/index.mdx +27 -3
  28. package/docs/configuration/seo.mdx +35 -5
  29. package/docs/content/components.mdx +2 -2
  30. package/docs/content/i18n.mdx +1 -1
  31. package/docs/content/navigation.mdx +3 -3
  32. package/docs/content/sources.mdx +1 -1
  33. package/docs/content/syntax.mdx +6 -4
  34. package/docs/index.mdx +2 -2
  35. package/docs/reference/cli.mdx +9 -7
  36. package/docs/reference/frontmatter.mdx +1 -1
  37. package/package.json +22 -4
  38. package/skills/blume/SKILL.md +5 -3
  39. package/skills/blume-update-docs/SKILL.md +3 -2
  40. package/src/ai/agent-readability.ts +9 -8
  41. package/src/ai/ask-context.ts +7 -2
  42. package/src/ai/ask-data.ts +3 -0
  43. package/src/ai/component-markdown.ts +461 -0
  44. package/src/ai/llms.ts +135 -26
  45. package/src/ai/markdown.ts +35 -6
  46. package/src/ai/mcp/data.ts +25 -4
  47. package/src/ai/mcp/discovery.ts +10 -3
  48. package/src/ai/mcp/server.ts +21 -7
  49. package/src/ai/mcp/tools.ts +1 -1
  50. package/src/ai/visibility.ts +74 -0
  51. package/src/astro/component-slots.ts +11 -1
  52. package/src/astro/generate.ts +77 -45
  53. package/src/astro/integration.ts +1 -1
  54. package/src/astro/markdown-negotiation.ts +1 -1
  55. package/src/astro/pages.ts +81 -19
  56. package/src/astro/templates.ts +150 -19
  57. package/src/blume-modules.d.ts +8 -0
  58. package/src/cli/commands/build.ts +120 -23
  59. package/src/cli/commands/check.ts +1 -1
  60. package/src/cli/commands/dev.ts +26 -5
  61. package/src/cli/commands/eject.ts +47 -19
  62. package/src/cli/commands/init.ts +120 -180
  63. package/src/cli/commands/preview.ts +4 -1
  64. package/src/cli/commands/validate.ts +43 -2
  65. package/src/cli/dev-lock.ts +8 -4
  66. package/src/cli/eject-scripts.ts +72 -0
  67. package/src/cli/env.ts +15 -5
  68. package/src/cli/init/questions.ts +158 -0
  69. package/src/cli/init/scaffold.ts +380 -0
  70. package/src/cli/internal-error.ts +9 -4
  71. package/src/cli/prepare.ts +3 -2
  72. package/src/components/Icon.astro +2 -1
  73. package/src/components/content/AccordionItem.astro +23 -4
  74. package/src/components/content/Badge.astro +3 -1
  75. package/src/components/content/Card.astro +4 -2
  76. package/src/components/content/Step.astro +10 -1
  77. package/src/components/content/Tabs.astro +15 -3
  78. package/src/components/content/Tile.astro +2 -1
  79. package/src/components/content/Tooltip.astro +3 -1
  80. package/src/components/content/Update.astro +9 -2
  81. package/src/components/content/auto-type-table.ts +7 -1
  82. package/src/components/content/base-href.ts +33 -0
  83. package/src/components/content/changelog-element.ts +9 -2
  84. package/src/components/content/mermaid-element.ts +7 -2
  85. package/src/components/islands/AskAI.astro +5 -2
  86. package/src/components/islands/ask-ai.tsx +86 -11
  87. package/src/components/islands/hooks.ts +28 -8
  88. package/src/components/layout/Banner.astro +10 -2
  89. package/src/components/layout/Breadcrumbs.astro +11 -2
  90. package/src/components/layout/Header.astro +13 -4
  91. package/src/components/layout/Logo.astro +11 -3
  92. package/src/components/layout/NavTree.astro +19 -5
  93. package/src/components/layout/PageActions.astro +25 -10
  94. package/src/components/layout/PageLayout.astro +85 -9
  95. package/src/components/layout/Pagination.astro +10 -4
  96. package/src/components/layout/ReferenceLayout.astro +20 -2
  97. package/src/components/layout/RootLayout.astro +142 -12
  98. package/src/components/layout/Search.astro +117 -27
  99. package/src/components/layout/search/algolia.ts +11 -2
  100. package/src/components/layout/search/endpoint.ts +11 -5
  101. package/src/components/layout/search/orama-cloud.ts +8 -2
  102. package/src/components/layout/search/types.ts +5 -1
  103. package/src/components/layout/search/typesense.ts +4 -1
  104. package/src/components/layout/toc-element.ts +1 -1
  105. package/src/components/openapi/ApiTagOperations.astro +2 -1
  106. package/src/components/openapi/Operation.astro +47 -40
  107. package/src/components/openapi/RequestPanel.astro +1 -1
  108. package/src/components/openapi/helpers.ts +71 -3
  109. package/src/components/openapi/panel.ts +1 -1
  110. package/src/core/base-path.ts +24 -0
  111. package/src/core/builtin-tags.ts +2 -0
  112. package/src/core/config-input.ts +48 -2
  113. package/src/core/config.ts +3 -2
  114. package/src/core/data.ts +4 -0
  115. package/src/core/frontmatter.ts +7 -0
  116. package/src/core/graph.ts +15 -5
  117. package/src/core/i18n-ui.ts +54 -0
  118. package/src/core/i18n.ts +16 -8
  119. package/src/core/last-modified.ts +13 -6
  120. package/src/core/links.ts +32 -8
  121. package/src/core/navigation.ts +29 -4
  122. package/src/core/package-json.ts +17 -2
  123. package/src/core/project-graph.ts +15 -6
  124. package/src/core/schema.ts +71 -2
  125. package/src/core/sources/assets.ts +6 -1
  126. package/src/core/sources/filesystem.ts +4 -0
  127. package/src/core/sources/mdx-remote.ts +23 -14
  128. package/src/core/sources/normalize.ts +152 -50
  129. package/src/core/sources/notion.ts +8 -8
  130. package/src/core/ui-packs/ar.ts +8 -0
  131. package/src/core/ui-packs/bg.ts +8 -0
  132. package/src/core/ui-packs/bn.ts +8 -0
  133. package/src/core/ui-packs/ca.ts +8 -0
  134. package/src/core/ui-packs/cs.ts +8 -0
  135. package/src/core/ui-packs/da.ts +8 -0
  136. package/src/core/ui-packs/de.ts +8 -0
  137. package/src/core/ui-packs/el.ts +8 -0
  138. package/src/core/ui-packs/es.ts +8 -0
  139. package/src/core/ui-packs/fa.ts +8 -0
  140. package/src/core/ui-packs/fi.ts +8 -0
  141. package/src/core/ui-packs/fr.ts +9 -1
  142. package/src/core/ui-packs/he.ts +8 -0
  143. package/src/core/ui-packs/hi.ts +8 -0
  144. package/src/core/ui-packs/hr.ts +8 -0
  145. package/src/core/ui-packs/hu.ts +8 -0
  146. package/src/core/ui-packs/id.ts +8 -0
  147. package/src/core/ui-packs/it.ts +8 -0
  148. package/src/core/ui-packs/ja.ts +8 -0
  149. package/src/core/ui-packs/ko.ts +8 -0
  150. package/src/core/ui-packs/nl.ts +8 -0
  151. package/src/core/ui-packs/no.ts +8 -0
  152. package/src/core/ui-packs/pl.ts +8 -0
  153. package/src/core/ui-packs/pt-br.ts +8 -0
  154. package/src/core/ui-packs/pt.ts +8 -0
  155. package/src/core/ui-packs/ro.ts +8 -0
  156. package/src/core/ui-packs/ru.ts +8 -0
  157. package/src/core/ui-packs/sk.ts +8 -0
  158. package/src/core/ui-packs/sr.ts +8 -0
  159. package/src/core/ui-packs/sv.ts +8 -0
  160. package/src/core/ui-packs/th.ts +8 -0
  161. package/src/core/ui-packs/tr.ts +8 -0
  162. package/src/core/ui-packs/uk.ts +8 -0
  163. package/src/core/ui-packs/vi.ts +8 -0
  164. package/src/core/ui-packs/zh-tw.ts +8 -0
  165. package/src/core/ui-packs/zh.ts +8 -0
  166. package/src/deploy/adapter-output.ts +18 -8
  167. package/src/deploy/redirects.ts +7 -2
  168. package/src/deploy/sitemap.ts +53 -11
  169. package/src/index.ts +5 -0
  170. package/src/markdown/base-links.ts +10 -8
  171. package/src/markdown/index.ts +15 -3
  172. package/src/markdown/inline-code.ts +7 -2
  173. package/src/markdown/package-commands.ts +10 -4
  174. package/src/og/card.ts +4 -2
  175. package/src/og/dimensions.ts +12 -0
  176. package/src/openapi/model.ts +12 -4
  177. package/src/openapi/parse.ts +21 -0
  178. package/src/openapi/references.ts +38 -8
  179. package/src/openapi/render-mdx.ts +62 -1
  180. package/src/openapi/source.ts +59 -10
  181. package/src/registry/eject.ts +184 -12
  182. package/src/registry/registry.ts +0 -3
  183. package/src/search/documents.ts +34 -2
  184. package/src/seo/jsonld.ts +20 -13
  185. package/src/seo/x-handle.ts +18 -0
@@ -1,3 +1,4 @@
1
+ export type { ComponentMarkdown, ComponentMarkdownChild, ComponentMarkdownContext, } from "./ai/component-markdown.ts";
1
2
  export { defineConfig } from "./core/config.ts";
2
3
  export type { BlumeBanner, BlumeData, BlumeDataConfig, BlumeDataI18n, BlumeDataLocale, BlumeFavicon, BlumeFeed, BlumeLogo, BlumeRoute, } from "./core/data.ts";
3
4
  export { defineComponents } from "./core/define-components.ts";
@@ -0,0 +1,60 @@
1
+ import type { ResolvedConfig } from "../core/schema.ts";
2
+ import type { NavTab } from "../core/types.ts";
3
+ /**
4
+ * Pure resolution of the configured API reference blocks into concrete routes,
5
+ * labels, and a renderer choice — no file IO, so the content source, the nav
6
+ * tabs, the Scalar page generator, and the `blume:openapi` data module all share
7
+ * one source of truth. Kept free of any Astro/template imports so `core` can
8
+ * depend on it without a cycle.
9
+ */
10
+ export type ReferenceKind = "openapi" | "asyncapi";
11
+ /** Who renders a reference: Blume's own UI, or the embedded Scalar SPA. */
12
+ export type ReferenceRenderer = "blume" | "scalar";
13
+ /** Per-block display options for the Blume renderer. */
14
+ export interface ReferenceDisplay {
15
+ /** Code-sample languages shown per operation. */
16
+ codeSamples: string[];
17
+ /** Whether nested schema rows start expanded. */
18
+ expandSchemas: boolean;
19
+ }
20
+ /** A spec source resolved to a concrete route, label, and renderer. */
21
+ export interface ReferenceSource {
22
+ kind: ReferenceKind;
23
+ renderer: ReferenceRenderer;
24
+ /** Unique token derived from the route; the `<Operation source>` / data key. */
25
+ slug: string;
26
+ /** Normalized route the reference mounts at, e.g. `/reference`. */
27
+ route: string;
28
+ /**
29
+ * Site-wide `basePath` the rendered pages are mounted under (`""` when
30
+ * none). Kept separate from `route` — the content pipeline applies it to
31
+ * staged entries itself — so consumers prefix only the URLs they emit.
32
+ */
33
+ basePath: string;
34
+ label: string;
35
+ /** Local path or `http(s)` URL, verbatim from config. */
36
+ spec: string;
37
+ /** Per-block Scalar theme name override, if any (Scalar renderer only). */
38
+ theme?: string;
39
+ /** Display options carried through to the Blume renderer. */
40
+ display: ReferenceDisplay;
41
+ /**
42
+ * Warnings recorded while deduping — another source's route collided with
43
+ * this one and was dropped. Surfaced as diagnostics when the source loads.
44
+ */
45
+ collisions?: string[];
46
+ }
47
+ export declare const slugify: (text: string) => string;
48
+ /** Normalize a configured route to a single leading slash, no trailing slash. */
49
+ export declare const normalizeRoute: (route: string) => string;
50
+ /**
51
+ * Resolve every enabled reference. OpenAPI honors its `renderer` (Blume's own UI
52
+ * by default); AsyncAPI is always rendered by Scalar for now.
53
+ */
54
+ export declare const resolveReferences: (config: ResolvedConfig) => ReferenceSource[];
55
+ /** Nav tabs (header links) for every reference, regardless of renderer. */
56
+ export declare const referenceTabs: (config: ResolvedConfig) => NavTab[];
57
+ /** Blume-rendered OpenAPI references, deduped by route (first wins). */
58
+ export declare const blumeReferences: (config: ResolvedConfig) => ReferenceSource[];
59
+ /** Whether any reference is Scalar-rendered (gates the `@scalar/astro` dep + pages). */
60
+ export declare const hasScalarReferences: (config: ResolvedConfig) => boolean;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Normalize an X account to the leading `@` that `twitter:site`/`twitter:creator`
3
+ * require, so `acme`, `@acme`, and ` @acme ` all land on `@acme`. Empty or
4
+ * blank input yields undefined, which renders no tag at all.
5
+ *
6
+ * The layouts call this on values that never passed through the config schema:
7
+ * Astro's collections carry no schema here, so a page's `seo.x.creator` reaches
8
+ * them as raw frontmatter, and the schema's own transform never runs on it.
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.)
11
+ */
12
+ export declare const normalizeXHandle: (value: unknown) => string | undefined;
@@ -6,7 +6,7 @@ sidebar:
6
6
  order: 1
7
7
  ---
8
8
 
9
- Go from an empty folder to a running docs site in a few commands. Blume needs **Node.js 22 or newer** and a `docs/` folder with at least one `.md` or `.mdx` file — there's nothing else to set up.
9
+ Go from an empty folder to a running docs site in a few commands. Blume needs **Node.js 22.12 or newer** and a `docs/` folder with at least one `.md` or `.mdx` file — there's nothing else to set up.
10
10
 
11
11
  ## Install and run
12
12
 
@@ -20,7 +20,10 @@ Go from an empty folder to a running docs site in a few commands. Blume needs **
20
20
 
21
21
  </Step>
22
22
  <Step title="Scaffold a project">
23
- `blume init` creates the minimum surface: a content folder and a config file.
23
+ `blume init` asks a few questions where to create the project, what your
24
+ docs site is called, which template, and where your content lives — then
25
+ scaffolds the minimum surface: a content folder and a config file. Pass
26
+ `--yes` to skip the prompts and take the defaults.
24
27
 
25
28
  ```bash
26
29
  blume init
@@ -10,11 +10,11 @@ sidebar:
10
10
 
11
11
  `blume build` compiles your docs to plain HTML, CSS, and a local search index in `dist/`. There's no server to run — point any static host at the folder.
12
12
 
13
- | Setting | Value |
14
- | ---------------- | ------------- |
15
- | Build command | `blume build` |
16
- | Output directory | `dist` |
17
- | Node version | 22 or newer |
13
+ | Setting | Value |
14
+ | ---------------- | -------------- |
15
+ | Build command | `blume build` |
16
+ | Output directory | `dist` |
17
+ | Node version | 22.12 or newer |
18
18
 
19
19
  These settings work on Vercel, Netlify, Cloudflare Pages, GitHub Pages, Amazon S3 + CloudFront, or any bucket or CDN. Make sure `blume` is a dependency so the host can run the build.
20
20
 
@@ -88,7 +88,7 @@ deployment: {
88
88
  }
89
89
  ```
90
90
 
91
- Choosing an adapter pulls in the matching Astro adapter automatically:
91
+ The `vercel` and `node` adapters ship with Blume — choosing one just works. The `netlify` and `cloudflare` adapters must be installed in your project (e.g. `bun add -d @astrojs/netlify`); the CLI warns you if the package is missing:
92
92
 
93
93
  | Adapter | Package | Use for |
94
94
  | ------------ | --------------------- | ------------------------------------ |
@@ -97,7 +97,7 @@ Choosing an adapter pulls in the matching Astro adapter automatically:
97
97
  | `node` | `@astrojs/node` | Self-hosted Node servers, containers |
98
98
  | `cloudflare` | `@astrojs/cloudflare` | Cloudflare Workers and Pages |
99
99
 
100
- On **Vercel**, **Netlify**, and **Cloudflare Pages**, Blume picks the matching adapter automatically for server output — set `output: "server"` and deploy. Set `adapter` explicitly to override the detected value, or when self-hosting with `node`.
100
+ On **Vercel**, **Netlify**, and **Cloudflare Pages**, Blume picks the matching adapter automatically for server output — set `output: "server"` and deploy (on Netlify and Cloudflare, install the adapter package too). Set `adapter` explicitly to override the detected value, or when self-hosting with `node`.
101
101
 
102
102
  A server build includes everything a static build does, plus any Astro endpoints or middleware you add. The `node` adapter produces a standalone server you can run directly.
103
103
 
@@ -126,7 +126,7 @@ When a feature needs a runtime secret, Blume warns at `blume dev`/`build` if it'
126
126
  | Feature | Variable |
127
127
  | --- | --- |
128
128
  | Ask AI (AI Gateway) | `AI_GATEWAY_API_KEY` (or Vercel OIDC) |
129
- | Ask AI (OpenRouter / OpenAI-compatible) | the provider's `apiKeyEnv` |
129
+ | Ask AI (other providers) | the provider's default key env var (`OPENROUTER_API_KEY`, `LLMGATEWAY_API_KEY`, `INKEEP_API_KEY`), or your configured `apiKeyEnv` |
130
130
  | Mixedbread search | `MIXEDBREAD_API_KEY` |
131
131
 
132
132
  Set them in `.env.local` for local dev and in your host's environment for production. Build-time secrets for search-index sync (Algolia, Orama Cloud, Typesense) are warned about separately during the sync step.
package/docs/03-faq.mdx CHANGED
@@ -79,26 +79,26 @@ Once the opening `:::note` fence is joined to the prose, it's no longer a direct
79
79
 
80
80
  ### Why it happens
81
81
 
82
- This is a bug in oxfmt's Markdown formatter (inherited from Prettier's Markdown printer — see [prettier/prettier#19040](https://github.com/prettier/prettier/pull/19040)). When it wraps prose, it treats the `:::` fence lines as ordinary text and joins them with the adjacent line, breaking the directive. It affects every container directive — `:::note`, `:::tip`, `:::warning`, `:::danger`, `:::success`.
82
+ This is a bug in oxfmt's Markdown formatter (inherited from Prettier's Markdown printer — see [prettier/prettier#19040](https://github.com/prettier/prettier/pull/19040)). When it wraps prose, it treats the `:::` fence lines as ordinary text and joins them with the adjacent line, breaking the directive. It affects every container directive type — `:::note`, `:::tip`, `:::info`, `:::warning`, `:::danger`, `:::success`.
83
83
 
84
84
  We reported it upstream in [oxc-project/oxc#24096](https://github.com/oxc-project/oxc/issues/24096); until it's fixed there, the patch below is the workaround.
85
85
 
86
86
  ### The fix
87
87
 
88
- Patch oxfmt so it preserves the line break that sits directly against a `:::` fence. Blume ships exactly this patch in its own repo, and you can apply the same one in any project.
88
+ Patch oxfmt so it preserves the line break that sits directly against a `:::` fence. Blume ships the same fix in its own repo, and you can apply it in any project.
89
89
 
90
- 1. Save the patch as `patches/oxfmt@0.57.0.patch`:
90
+ 1. Save the patch as `patches/oxfmt@0.58.0.patch`:
91
91
 
92
- ```diff patches/oxfmt@0.57.0.patch
93
- diff --git a/dist/markdown-B5hFVJKQ.js b/dist/markdown-B5hFVJKQ.js
94
- index 58322b247b263f87975c2a97eecfe0b97d7143c4..0f39f1901603026a391c6d343cb838dfaccbfd03 100644
95
- --- a/dist/markdown-B5hFVJKQ.js
96
- +++ b/dist/markdown-B5hFVJKQ.js
97
- @@ -1141,7 +1141,14 @@ function Yf(e, r, t) {
98
- case "sentence": return Yi(e, t);
99
- case "word": return Li(e);
92
+ ```diff patches/oxfmt@0.58.0.patch
93
+ diff --git a/dist/markdown-BjyDFyaO.js b/dist/markdown-BjyDFyaO.js
94
+ index 1905aa7563e612808426e26f1ed28bebbef3456f..42bb9f66829068676c5697fc9fc39279bf937f3e 100644
95
+ --- a/dist/markdown-BjyDFyaO.js
96
+ +++ b/dist/markdown-BjyDFyaO.js
97
+ @@ -4830,7 +4830,43 @@ function lu(e, t, r) {
98
+ case "sentence": return Mh(e, r);
99
+ case "word": return t.parser !== "mdx" ? Vh(e, t) : Gh(e);
100
100
  case "whitespace": {
101
- - let { next: i } = e, u = i && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/u.test(i.value) ? "never" : r.proseWrap;
101
+ - let { next: i } = e, u = i && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(i.value) && !ME(e) && !(t.proseWrap === "preserve" && zE(e)) ? "never" : t.proseWrap;
102
102
  + let { next: i, previous: oxfmtFencePrev } = e;
103
103
  + // Preserve line breaks that sit directly against a `:::` container
104
104
  + // directive fence, so `proseWrap: "never"` keeps the opening/closing
@@ -106,8 +106,37 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
106
106
  + // breaks the directive). Ordinary prose still wraps per proseWrap.
107
107
  + // See prettier/prettier#19040.
108
108
  + let oxfmtIsFence = (w) => w != null && typeof w.value === "string" && w.value.startsWith(":::");
109
- + let u = oxfmtIsFence(oxfmtFencePrev) || oxfmtIsFence(i) ? "preserve" : i && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/u.test(i.value) ? "never" : r.proseWrap;
110
- return qt(e, n.value, u);
109
+ + // A titled directive (`:::warning[Heads up]`) parses its `[title]` as a
110
+ + // linkReference between two sentence nodes at the paragraph level: the
111
+ + // fence word ends the sentence before the reference, and the body's
112
+ + // leading newline opens the sentence after it. So when this whitespace
113
+ + // starts its sentence, climb to the paragraph and check whether the two
114
+ + // preceding siblings are a (link) reference and a sentence ending in a
115
+ + // `:::` fence word.
116
+ + let oxfmtPrevIsTitledFence = !1;
117
+ + if (oxfmtFencePrev == null && e.index === 0 && e.grandparent != null && Array.isArray(e.grandparent.children)) {
118
+ + let oxfmtSibs = e.grandparent.children, oxfmtSentIdx = oxfmtSibs.indexOf(e.parent);
119
+ + if (oxfmtSentIdx >= 2) {
120
+ + let oxfmtLink = oxfmtSibs[oxfmtSentIdx - 1], oxfmtBefore = oxfmtSibs[oxfmtSentIdx - 2];
121
+ + let oxfmtLastWord = oxfmtBefore && oxfmtBefore.type === "sentence" && Array.isArray(oxfmtBefore.children) ? oxfmtBefore.children[oxfmtBefore.children.length - 1] : null;
122
+ + oxfmtPrevIsTitledFence = oxfmtLink != null && (oxfmtLink.type === "linkReference" || oxfmtLink.type === "link") && oxfmtIsFence(oxfmtLastWord);
123
+ + }
124
+ + }
125
+ + // The plain-markdown parser keeps a titled fence's `[title]` as literal
126
+ + // words, so the whole directive is one sentence. For a newline
127
+ + // whitespace, walk back to the start of its visual line within the
128
+ + // sentence; a line led by a `:::` word is a fence whose break must stay.
129
+ + if (!oxfmtPrevIsTitledFence && e.node.value.includes("\n") && e.parent != null && Array.isArray(e.parent.children)) {
130
+ + let oxfmtLineFirst = null;
131
+ + for (let oxfmtJ = e.index - 1; oxfmtJ >= 0; oxfmtJ--) {
132
+ + let oxfmtSib = e.parent.children[oxfmtJ];
133
+ + if (oxfmtSib.type === "whitespace" && typeof oxfmtSib.value === "string" && oxfmtSib.value.includes("\n")) break;
134
+ + oxfmtLineFirst = oxfmtSib;
135
+ + }
136
+ + oxfmtPrevIsTitledFence = oxfmtIsFence(oxfmtLineFirst);
137
+ + }
138
+ + let u = oxfmtIsFence(oxfmtFencePrev) || oxfmtPrevIsTitledFence || oxfmtIsFence(i) ? "preserve" : i && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(i.value) && !ME(e) && !(t.proseWrap === "preserve" && zE(e)) ? "never" : t.proseWrap;
139
+ return ou(e, n.value, u, !1, t);
111
140
  }
112
141
  case "emphasis": {
113
142
  ```
@@ -117,7 +146,7 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
117
146
  ```json package.json
118
147
  {
119
148
  "patchedDependencies": {
120
- "oxfmt@0.57.0": "patches/oxfmt@0.57.0.patch"
149
+ "oxfmt@0.58.0": "patches/oxfmt@0.58.0.patch"
121
150
  }
122
151
  }
123
152
  ```
@@ -128,5 +157,6 @@ Patch oxfmt so it preserves the line break that sits directly against a `:::` fe
128
157
  bun install
129
158
  ```
130
159
 
131
- :::warning[Version-pinned] The patch targets oxfmt `0.57.0` — its diff references a file hashed to that build (`dist/markdown-B5hFVJKQ.js`). When you bump oxfmt, regenerate the patch (e.g. `bun patch oxfmt`) or check whether the upstream fix has landed and the patch is no longer needed.
160
+ :::warning[Version-pinned]
161
+ The patch targets a specific oxfmt build — its diff references a file whose name is hashed per release (`dist/markdown-*.js`). When you bump oxfmt, regenerate the patch (e.g. `bun patch oxfmt`) or check whether the upstream fix has landed and the patch is no longer needed.
132
162
  :::
@@ -3,7 +3,7 @@ title: OpenAPI / AsyncAPI
3
3
  description: Drop in an OpenAPI 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.
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. These docs ship one: the [Example API](/api) reference is generated from the Petstore spec by the config below.
7
7
 
8
8
  ```ts blume.config.ts lineNumbers
9
9
  openapi: {
@@ -71,7 +71,7 @@ When your versions follow [semver](https://semver.org) and span more than one ma
71
71
 
72
72
  ## From GitHub Releases
73
73
 
74
- Rather than authoring entries by hand, point the built-in [`github-releases` source](/docs/content/sources#github-releases) at a repo and every release becomes a `type: changelog` entry — the same timeline and feed, fed straight from the releases you already publish:
74
+ Rather than authoring entries by hand, point the built-in [`github-releases` source](/docs/content/sources#github-releases) at a repo and every release becomes a `type: changelog` entry — the same timeline and feed, fed straight from the releases you already publish. Blume's own [changelog](/changelog) is built this way:
75
75
 
76
76
  ```ts blume.config.ts
77
77
  content: {
@@ -74,7 +74,7 @@ The module exposes:
74
74
  type: "BlumeDataConfig",
75
75
  required: true,
76
76
  description:
77
- "Resolved site settings: title, description, logo, favicon, appleIcon, banner, theme, site, repoUrl, search, i18n, mcp, og, analytics, feedback, structuredData, codeWrap, and imageZoom.",
77
+ "Resolved site settings: title, description, logo, favicon, appleIcon, banner, theme, site, repoUrl, search, i18n, mcp, ask, og, analytics, feedback, structuredData, toc, codeThemes, codeWrap, and imageZoom.",
78
78
  },
79
79
  navigation: {
80
80
  type: "Navigation",
@@ -18,7 +18,7 @@ npx skills add haydenbleasel/blume
18
18
  `blume-update-docs` keeps your docs in sync with the product they document. On each run — typically from a schedule you configure in your agent runner — it audits recently merged PRs, changelogs, config schemas, and CLI help against the docs content, updates only pages that are factually stale (feature-flagged work is ignored), verifies with `blume build`, and opens or updates a `blume/*` pull request. If nothing drifted, it reports a clean no-op instead of opening a noisy PR.
19
19
 
20
20
  ```bash
21
- npx skills use haydenbleasel/blume@blume-update-docs
21
+ npx skills add haydenbleasel/blume --skill blume-update-docs
22
22
  ```
23
23
 
24
24
  Blume doesn't host the automation — wire the skill into a scheduled task in Claude Code, a Codex or Cursor automation, or plain cron, with permission to read repo history and open PRs. A typical weekly prompt:
@@ -17,23 +17,62 @@ ai: {
17
17
 
18
18
  While enabled, `blume build` writes two files to the root of your site:
19
19
 
20
- - **`/llms.txt`** — a compact index: your site title and description, then a linked list of every page with its summary.
20
+ - **`/llms.txt`** — a compact index: your site title and description, then a linked list of every page with its summary, organized into sections that mirror your sidebar — folders and groups become headings, so an agent sees the docs' structure, not one flat blob.
21
21
  - **`/llms-full.txt`** — the entire corpus: each page's full Markdown body, with its source URL, in one file.
22
22
 
23
23
  Draft pages are excluded. Set [`deployment.site`](/docs/deployment) so the links and source URLs resolve to absolute addresses.
24
24
 
25
+ `llmsTxt` also takes an object form with knobs for what the files include. If your [API reference](/docs/advanced/api-reference) documents a placeholder or example spec, set `openapi: false` to keep its generated pages out of both files:
26
+
27
+ ```ts blume.config.ts lineNumbers
28
+ ai: {
29
+ llmsTxt: {
30
+ enabled: true, // default
31
+ openapi: false, // exclude generated API reference pages
32
+ },
33
+ }
34
+ ```
35
+
36
+ To take full control of either file, add your own `llms.txt` or `llms-full.txt` to your `public/` folder. Like a custom favicon, it's picked up automatically and ships in place of the generated file — override one and Blume still generates the other.
37
+
25
38
  ## Raw Markdown
26
39
 
27
40
  Append `.md` or `.mdx` to any page's URL to fetch its raw Markdown source — perfect for LLMs, coding agents, and "copy as Markdown" workflows. It's available for every page, in dev and production, with no configuration.
28
41
 
29
- | URL | Returns |
30
- | ----------------- | ----------------------- |
31
- | `/quickstart` | The rendered page |
32
- | `/quickstart.md` | The raw Markdown source |
33
- | `/quickstart.mdx` | The raw Markdown source |
42
+ | URL | Returns |
43
+ | ----------------- | ----------------------------------------- |
44
+ | `/quickstart` | The rendered page |
45
+ | `/quickstart.md` | Plain Markdown, with components converted |
46
+ | `/quickstart.mdx` | The raw MDX source, exactly as written |
34
47
 
35
48
  Nested routes work the same way (`/content/syntax.md`), and the home page is served at `/index.md`.
36
49
 
50
+ The `.md` variant _downlevels_ components to plain Markdown for consumers that can't interpret JSX: `<TypeTable>` becomes a Markdown table, `<Callout>` a labeled blockquote, `<Steps>` an ordered list, `<Tabs>` bold-labeled sections, and `<YouTube>` a link. Anything that can't be converted faithfully — a custom component, or a prop computed from an import — is left as-is, and component markup inside fenced code blocks is never touched. The same conversion applies to `llms-full.txt` and the MCP server's `get_page` tool, so every agent-facing surface reads clean Markdown. When you want the untransformed source, use the `.mdx` variant.
51
+
52
+ ### Custom component serializers
53
+
54
+ Give your own components a Markdown form with `ai.markdownComponents` — a map of JSX name to serializer. Each serializer receives the component's `props` (statically evaluated from the MDX attributes) and its `children` (already downleveled to Markdown), and returns the replacement — or `null` to leave the JSX as-is:
55
+
56
+ ```ts blume.config.ts lineNumbers
57
+ import { defineConfig } from "blume";
58
+ import type { ComponentMarkdown } from "blume";
59
+
60
+ const chart: ComponentMarkdown = ({ props }) =>
61
+ `![${props.title}](/charts/${props.slug}.png)`;
62
+
63
+ export default defineConfig({
64
+ ai: {
65
+ markdownComponents: {
66
+ Chart: chart,
67
+ },
68
+ },
69
+ });
70
+ ```
71
+
72
+ For container components, `childComponents("Name")` extracts direct children by tag — the same way the built-in `<Steps>` serializer collects its `<Step>` items. A same-name entry replaces a built-in serializer, so you can restyle how `<Callout>` downlevels — or return `null` to opt one out entirely.
73
+
74
+ Serializers live in `blume.config.ts`, not `components.tsx`: the config file is executed at build time, while the components file is only statically analyzed (it may import `.astro` files, which can't run outside the site build). Your components themselves stay registered in `components.tsx` exactly as before — `markdownComponents` only adds their agent-facing Markdown form.
75
+
37
76
  ## Copy as Markdown
38
77
 
39
78
  Every page carries a **Copy as Markdown** action — in the [page actions](/docs/content/navigation#page-actions) beneath the table of contents — that copies the page's raw Markdown to the clipboard. It's the same source served at the [`.md` URL](#raw-markdown) above, ready to paste into an LLM, an issue, or your notes. It's available on every page, in dev and production, with no configuration.
@@ -46,7 +85,7 @@ The **Open in chat** action opens the current page in an AI assistant — v0, Ch
46
85
 
47
86
  Like Copy as Markdown, it needs no setup. The assistant fetches the page over its public URL, so it works as soon as the page is deployed.
48
87
 
49
- To embed a ready-to-copy prompt inline in your content — rather than a whole-page action — use the [Prompt component](/docs/content/components#prompt), which renders a labelled row with a **Copy prompt** button and an optional open-in-Cursor link.
88
+ To embed a ready-to-copy prompt inline in your content — rather than a whole-page action — use the [Prompt component](/docs/content/components#prompt), which renders a labeled row with a **Copy prompt** button and an optional open-in-Cursor link.
50
89
 
51
90
  ## Ask AI
52
91
 
@@ -172,7 +211,7 @@ mcp: {
172
211
  | `name` | title | Server name shown to clients (defaults to title). |
173
212
  | `instructions` | — | Optional system hint passed to connecting agents. |
174
213
 
175
- The server exposes four read-only tools — `search_docs`, `get_page`, `list_pages`, and `get_navigation` — and publishes discovery documents at `/.well-known/mcp.json` and `/.well-known/mcp/server-card.json`. Each page's **Connect to MCP** menu offers copy-and-go install for Claude Code, Cursor, and VS Code (shown once [`deployment.site`](/docs/deployment) is set).
214
+ The server exposes read-only tools — `search_docs`, `get_page`, `list_pages`, and `get_navigation` — and publishes discovery documents at `/.well-known/mcp.json` and `/.well-known/mcp/server-card.json`. Each page's **Connect to MCP** menu offers copy-and-go install for Claude Code, Cursor, VS Code, and Codex (shown once [`deployment.site`](/docs/deployment) is set).
176
215
 
177
216
  `search_docs` runs its own full-text index, so it works regardless of your [search](/docs/configuration/search) provider — and even when search is set to `none`. The MCP server is a separate feature from on-page search.
178
217
 
@@ -196,7 +235,7 @@ claude mcp add --transport http my-docs https://docs.example.com/mcp
196
235
 
197
236
  ## Agent readability
198
237
 
199
- Blume writes an **`/agent-readability.json`** manifest at your site root that indexes the agent-facing surface described on this page — so an agent can discover it in a single fetch instead of guessing at conventions or scraping HTML. Unlike the features above, it's on by default:
238
+ Blume writes an **`/agent-readability.json`** manifest at your site root that indexes the agent-facing surface described on this page — so an agent can discover it in a single fetch instead of guessing at conventions or scraping HTML. Like `llms.txt`, it's on by default:
200
239
 
201
240
  ```ts blume.config.ts lineNumbers
202
241
  seo: {
@@ -241,4 +280,4 @@ npx skills add haydenbleasel/blume
241
280
 
242
281
  The skill teaches the agent what Blume is and how to scaffold, write, and configure a site, and points it at the full docs bundled in the installed package (`node_modules/blume/docs`).
243
282
 
244
- It's one of the [agent skills Blume ships](/docs/advanced/skills), alongside skills for migrating an existing docs site to Blume and keeping docs in sync with your product from a scheduled agent run.
283
+ It's one of the [agent skills Blume ships](/docs/advanced/skills), alongside a skill for keeping docs in sync with your product from a scheduled agent run.
@@ -154,3 +154,14 @@ blume eject --yes
154
154
  ```
155
155
 
156
156
  Eject is a one-way step: the hidden `.blume/` runtime becomes a normal Astro app you own and can modify directly. The `blume` package stays importable, so you keep its components, theme, and Markdown processors.
157
+
158
+ ### What eject leaves behind
159
+
160
+ After ejecting, your `build` script runs plain `astro build` — the site itself builds the same, but the artifacts `blume build` layered on top are no longer produced. The eject command warns about the ones your config actually uses. To keep them:
161
+
162
+ - **Pagefind search index** — with `search.provider: "pagefind"`, the search UI loads the index from the built site, so search breaks in production until you index it yourself. Install `pagefind` as a devDependency and index after each build: `"build": "astro build && pagefind --site dist"`.
163
+ - **Hosted search sync** — a hosted provider's index is no longer pushed on build; re-upload your search records after each build with the provider's API or CLI.
164
+ - **sitemap.xml** — recreate it with the standard [@astrojs/sitemap](https://docs.astro.build/en/guides/integrations-guide/sitemap/) integration.
165
+ - **robots.txt** — ship your own as `public/robots.txt`.
166
+ - **llms.txt / llms-full.txt and agent-readability.json** — write them by hand (or generate them in a build step of your own) and serve them from `public/`.
167
+ - **Platform redirect files** — `_redirects` and `vercel.json` are no longer emitted for static builds. Your redirects still work as Astro-generated meta-refresh pages, or you can move them into your host's own config.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Export
3
- description: Let readers download any page as a PDF or EPUB.
3
+ description: Let readers download any page as a PDF or EPUB — client-side, so static builds stay static.
4
4
  ---
5
5
 
6
6
  Blume can add an **Export** action to the [page actions](/docs/content/navigation#page-actions) beneath the table of contents, letting readers save the page they're on as a **PDF** or an **EPUB**. It's off by default and entirely client-side — no server, and [static](/docs/deployment) builds stay static.
@@ -189,6 +189,24 @@ content: {
189
189
 
190
190
  Static assets live in `public/` — a file at `public/logo.png` is served at `/logo.png`, so a reference like `![](/images/create.png)` resolves against `public/images/create.png`.
191
191
 
192
+ ## GitHub
193
+
194
+ Point Blume at your repository with `github`. It powers the header [repository link](/docs/content/navigation#repository-link) and the **Edit on GitHub** and **Give feedback** [page actions](/docs/content/navigation#page-actions):
195
+
196
+ ```ts blume.config.ts lineNumbers
197
+ github: {
198
+ owner: "acme",
199
+ repo: "docs",
200
+ }
201
+ ```
202
+
203
+ | Option | Default | Description |
204
+ | --- | --- | --- |
205
+ | `owner` | — | GitHub account or organization that owns the repository. |
206
+ | `repo` | — | Repository name. |
207
+ | `branch` | `"main"` | Branch that edit links point at. |
208
+ | `dir` | — | Path from the repo root to the project root (for monorepos). |
209
+
192
210
  ## Last modified
193
211
 
194
212
  Show a "Last updated on …" line at the bottom of each page. Off by default; set `lastModified` to `true` to derive each page's date from its git history:
@@ -248,7 +266,13 @@ The on-this-page outline is on by default and lists `H2`–`H3` headings. Turn i
248
266
  ```ts blume.config.ts
249
267
  export default defineConfig({
250
268
  toc: false, // hide it everywhere
251
- // …or narrow the range:
269
+ });
270
+ ```
271
+
272
+ Or narrow the heading range instead:
273
+
274
+ ```ts blume.config.ts
275
+ export default defineConfig({
252
276
  toc: { minHeadingLevel: 2, maxHeadingLevel: 4 },
253
277
  });
254
278
  ```
@@ -261,8 +285,8 @@ Each of these has its own guide. The config field is the entry point:
261
285
  | --- | --- | --- |
262
286
  | `theme` | Accent color, corner radius, fonts, light/dark mode | [Theming](/docs/configuration/theming) |
263
287
  | `navigation` | Explicit sidebar and header tabs | [Navigation](/docs/content/navigation) |
264
- | `search` | Provider (Orama or Pagefind) and indexing | [Search](/docs/configuration/search) |
265
- | `markdown` | Opt-in Markdown features like math | [Syntax](/docs/content/syntax) |
288
+ | `search` | Provider (Orama, Pagefind, Algolia, and more) and indexing | [Search](/docs/configuration/search) |
289
+ | `markdown` | Markdown rendering options code blocks, heading anchors, image zoom | [Syntax](/docs/content/syntax) |
266
290
  | `ai` | `llms.txt`, Ask AI, and the MCP server | [AI](/docs/configuration/ai) |
267
291
  | `mcp` | Hosted MCP server for coding agents | [AI](/docs/configuration/ai#mcp-server) |
268
292
  | `analytics` | Vercel, PostHog, and custom scripts | [Analytics](/docs/configuration/analytics) |
@@ -5,7 +5,7 @@ sidebar:
5
5
  label: SEO
6
6
  ---
7
7
 
8
- Blume handles the discoverability layer for you: page metadata, social share images, feeds, and structured data. The three configurable features live under the `seo` key in `blume.config.ts`; metadata is driven by your content.
8
+ Blume handles the discoverability layer for you: page metadata, social share images, feeds, and structured data. The configurable features live under the `seo` key in `blume.config.ts`; metadata is driven by your content.
9
9
 
10
10
  ```ts blume.config.ts lineNumbers
11
11
  seo: {
@@ -14,6 +14,7 @@ seo: {
14
14
  sitemap: true,
15
15
  robots: true,
16
16
  structuredData: true,
17
+ x: { handle: "@acme" },
17
18
  }
18
19
  ```
19
20
 
@@ -25,10 +26,34 @@ Every page renders the standard `<head>` tags from your config and frontmatter:
25
26
 
26
27
  - `<title>` — the page title plus your site `title`.
27
28
  - `<meta name="description">` and `og:description` — the page `description`, falling back to the site `description`.
28
- - `og:title` — the page title.
29
- - `<link rel="canonical">` — the page's absolute URL (when `deployment.site` is set).
29
+ - `og:title` and `og:site_name` — the page title and your site `title`.
30
+ - `<link rel="canonical">` and `og:url` — the page's absolute URL (when `deployment.site` is set).
31
+ - `og:type` — `article` on blog posts and changelog entries, `website` elsewhere. Article pages also emit `article:published_time` and `article:modified_time` from the page's `date` and last-modified timestamp.
32
+ - `og:image` — the [OG image](#open-graph-images) for the page. A generated card also declares its `og:image:width`, `og:image:height`, `og:image:type`, and `og:image:alt`, so a crawler can lay the card out without fetching it first; an `seo.image` you supply yourself declares none of these, since its size and format are unknown.
33
+ - `twitter:card`, `twitter:title`, `twitter:description`, `twitter:image` — the X card. Pages with an image get the wide `summary_large_image` variant; pages without one still get the compact `summary` card rather than rendering as a bare link.
30
34
 
31
- Override any of these per page with `seo` frontmatter:
35
+ ### X attribution
36
+
37
+ X reads everything else on the card from the `og:*` tags, so the only values it can't infer are the accounts to credit. Set them under `seo.x` and Blume emits `twitter:site` (your site's account) and `twitter:creator` (the author's). The `@` is optional — `acme` and `@acme` both work.
38
+
39
+ ```ts blume.config.ts lineNumbers
40
+ seo: {
41
+ x: { handle: "@acme", creator: "@jane" },
42
+ }
43
+ ```
44
+
45
+ A page can claim its own author, which is what you want for a guest post:
46
+
47
+ ```yaml lineNumbers
48
+ ---
49
+ title: How we shipped it
50
+ seo:
51
+ x:
52
+ creator: "@guestauthor"
53
+ ---
54
+ ```
55
+
56
+ Override any of the other tags per page with `seo` frontmatter:
32
57
 
33
58
  ```yaml lineNumbers
34
59
  ---
@@ -63,6 +88,11 @@ seo:
63
88
  type: "boolean",
64
89
  description: "Emit robots noindex and skip structured data.",
65
90
  },
91
+ "seo.x.creator": {
92
+ type: "string",
93
+ description:
94
+ "Credit this page to an X account (twitter:creator), overriding seo.x.creator from your config.",
95
+ },
66
96
  }}
67
97
  />
68
98
 
@@ -225,6 +255,6 @@ seo: {
225
255
  }}
226
256
  />
227
257
 
228
- Content signals express a preference, not access control: they tell well-behaved crawlers how you'd like your content used, and it's on the crawler to honour them.
258
+ Content signals express a preference, not access control: they tell well-behaved crawlers how you'd like your content used, and it's on the crawler to honor them.
229
259
 
230
260
  Ship your own `public/robots.txt` to take over.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Components
3
- description: The built-in JSX components — cards, columns, steps, tabs, accordions, badges, code groups, frames, trees, tooltips, type tables, live component previews, diffs, and a GitHub card usable in any MDX page.
3
+ description: Cards, steps, tabs, accordions, badges, code groups, frames, trees, type tables, live previews, and diffs the built-in components, usable in any MDX page.
4
4
  ---
5
5
 
6
6
  Blume ships an accessible, themeable component set available in any `.mdx` page with **no imports**. Each one is shown below with a live preview and its source. Components are vanilla and React-free; React only switches on if you add your own island.
@@ -428,7 +428,7 @@ A single row with a label and a copy button. The `description` (Markdown) is the
428
428
 
429
429
  ## Visibility
430
430
 
431
- Show or hide content by audience. `for="web"` renders only on the site; `for="agents"` targets the generated Markdown (`llms.txt`) that AI agents read.
431
+ Show or hide content by audience. `for="web"` renders only on the site; `for="agents"` targets the agent-facing Markdown that AI agents read (`llms-full.txt` and each page's `.md` mirror).
432
432
 
433
433
  <Visibility for="web">
434
434
  This note appears on the website but is omitted from the agent-facing
@@ -108,7 +108,7 @@ When i18n is on, a language switcher appears in the header automatically, genera
108
108
 
109
109
  Blume ships built-in translations for its own interface chrome — “On this page”, “Search”, “Edit on GitHub”, and the rest — so a locale with a built-in pack gets translated UI out of the box. **You only translate your content.**
110
110
 
111
- Packs ship for 36 languages — Arabic, Bengali, Bulgarian, Catalan, Chinese (Simplified and Traditional), Croatian, Czech, Danish, Dutch, Finnish, French, German, Greek, Hebrew, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, Norwegian, Persian, Polish, Portuguese (and Brazilian Portuguese), Romanian, Russian, Serbian, Slovak, Spanish, Swedish, Thai, Turkish, Ukrainian, and Vietnamese. They're community-maintained — open a PR to add a locale or sharpen a translation.
111
+ Packs ship for over 30 languages — Arabic, Bengali, Bulgarian, Catalan, Chinese (Simplified and Traditional), Croatian, Czech, Danish, Dutch, Finnish, French, German, Greek, Hebrew, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, Norwegian, Persian, Polish, Portuguese (and Brazilian Portuguese), Romanian, Russian, Serbian, Slovak, Spanish, Swedish, Thai, Turkish, Ukrainian, and Vietnamese. They're community-maintained — open a PR to add a locale or sharpen a translation.
112
112
 
113
113
  Missing or unshipped strings fall back to the default locale, then to English. To override a string or supply your own language, set `i18n.ui`, keyed by locale:
114
114
 
@@ -147,7 +147,7 @@ navigation: {
147
147
  }
148
148
  ```
149
149
 
150
- Each link takes a `label`, an `href`, and an optional `icon` (a [built-in icon](/docs/configuration/customization) name, image path/URL, or inline SVG — the same as anywhere else). An `href` may point anywhere: an external URL opens in a new tab, while an internal route (`/contact`) is validated against your pages at build time, warning you if nothing matches.
150
+ Each link takes a `label`, an `href`, and an optional `icon` (a [built-in icon](/docs/content/components#icon) name, image path/URL, or inline SVG — the same as anywhere else). An `href` may point anywhere: an external URL opens in a new tab, while an internal route (`/contact`) is validated against your pages at build time, warning you if nothing matches.
151
151
 
152
152
  ## Explicit sidebar
153
153
 
@@ -196,10 +196,10 @@ A right-rail outline is generated automatically from each page's `##` and `###`
196
196
 
197
197
  Below the table of contents, every page shows a set of quick actions:
198
198
 
199
- - **Edit this page on GitHub** — links straight to the source file. Appears once you set [`github`](/docs/configuration) in your config.
199
+ - **Edit on GitHub** — links straight to the source file. Appears once you set [`github`](/docs/configuration) in your config.
200
200
  - **Scroll to top** — smoothly returns to the top of long pages.
201
201
  - **Give feedback** — opens a prefilled GitHub issue with an optional reaction and note (also requires `github`).
202
202
 
203
- Three more hand the page to AI tools — **Copy as Markdown**, **Open in chat**, and **Ask AI about this page** — covered in [AI](/docs/configuration/ai#copy-as-markdown).
203
+ Others hand the page to AI tools — **Copy as Markdown** and **Open in chat** — covered in [AI](/docs/configuration/ai#copy-as-markdown).
204
204
 
205
205
  With [`export`](/docs/configuration/export) on, an **Export** action also lets readers download the page as a PDF or EPUB.
@@ -159,7 +159,7 @@ Two flags control how remote content is fetched and what's included:
159
159
  blume dev --preview # author workflow: see drafts live
160
160
  blume build --preview # render a full preview build
161
161
  blume sync # refresh remote content now
162
- blume sync --force # ...ignoring any cached snapshot
162
+ blume sync --force # ...ignoring any cached snapshot
163
163
  ```
164
164
 
165
165
  ## Custom sources