blume 1.5.2 → 1.6.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 +85 -0
  2. package/dist/cli/index.js +3639 -1377
  3. package/dist/cli/index.js.map +103 -91
  4. package/dist/types/ai/component-markdown.d.ts +79 -0
  5. package/dist/types/components/layout/nav-utils.d.ts +60 -0
  6. package/dist/types/core/base-path.d.ts +9 -0
  7. package/dist/types/core/config-input.d.ts +206 -4
  8. package/dist/types/core/config.d.ts +6 -4
  9. package/dist/types/core/data.d.ts +23 -1
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +8 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +117 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +23 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +21 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/_snippets/include-demo.mdx +7 -0
  21. package/docs/advanced/api-reference.mdx +3 -3
  22. package/docs/advanced/custom-pages.mdx +1 -1
  23. package/docs/advanced/graphql.mdx +84 -0
  24. package/docs/advanced/meta.ts +8 -1
  25. package/docs/configuration/ai.mdx +21 -3
  26. package/docs/configuration/index.mdx +24 -0
  27. package/docs/configuration/search.mdx +13 -1
  28. package/docs/configuration/seo.mdx +27 -0
  29. package/docs/configuration/theming.mdx +17 -0
  30. package/docs/content/components.mdx +7 -0
  31. package/docs/content/includes.mdx +68 -0
  32. package/docs/content/meta.ts +1 -0
  33. package/docs/content/navigation.mdx +25 -0
  34. package/docs/content/sources.mdx +42 -1
  35. package/docs/content/syntax.mdx +69 -1
  36. package/docs/content/versioning.mdx +15 -9
  37. package/docs/reference/cli.mdx +2 -1
  38. package/package.json +23 -14
  39. package/skills/blume-migrate/SKILL.md +16 -7
  40. package/skills/blume-migrate/references/docusaurus.md +5 -3
  41. package/skills/blume-migrate/references/fumadocs.md +10 -2
  42. package/skills/blume-migrate/references/mintlify.md +3 -2
  43. package/skills/blume-migrate/references/nextra.md +2 -2
  44. package/skills/blume-migrate/references/starlight.md +1 -1
  45. package/src/ai/agent-readability.ts +2 -1
  46. package/src/ai/ask-data.ts +2 -1
  47. package/src/ai/component-markdown.ts +199 -36
  48. package/src/ai/llms.ts +93 -6
  49. package/src/ai/markdown.ts +2 -2
  50. package/src/ai/mcp/discovery.ts +10 -2
  51. package/src/ai/mcp/server.ts +74 -2
  52. package/src/astro/generate.ts +183 -116
  53. package/src/astro/include-hmr.ts +81 -0
  54. package/src/astro/include-refresh.ts +0 -0
  55. package/src/astro/index.ts +3 -5
  56. package/src/astro/templates.ts +125 -76
  57. package/src/cli/commands/build.ts +84 -15
  58. package/src/cli/init/questions.ts +1 -0
  59. package/src/cli/init/scaffold.ts +27 -4
  60. package/src/components/colors.ts +142 -0
  61. package/src/components/content/Badge.astro +5 -12
  62. package/src/components/content/Callout.astro +19 -36
  63. package/src/components/content/Card.astro +15 -21
  64. package/src/components/content/Component.astro +10 -1
  65. package/src/components/content/GithubInfo.astro +28 -9
  66. package/src/components/content/Tabs.astro +27 -5
  67. package/src/components/content/github-info.ts +20 -5
  68. package/src/components/dropdown-dismiss.ts +122 -0
  69. package/src/components/layout/Fonts.astro +15 -8
  70. package/src/components/layout/Header.astro +44 -0
  71. package/src/components/layout/LanguageSwitcher.astro +9 -1
  72. package/src/components/layout/NavSelector.astro +12 -3
  73. package/src/components/layout/NavTree.astro +6 -18
  74. package/src/components/layout/PageActions.astro +29 -8
  75. package/src/components/layout/PageLayout.astro +10 -1
  76. package/src/components/layout/ReferenceLayout.astro +6 -1
  77. package/src/components/layout/RootLayout.astro +46 -15
  78. package/src/components/layout/Search.astro +36 -4
  79. package/src/components/layout/TableOfContents.astro +8 -2
  80. package/src/components/layout/head-scripts.ts +53 -1
  81. package/src/components/openapi/ApiOverview.astro +13 -3
  82. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  83. package/src/components/openapi/GraphqlChip.astro +33 -0
  84. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  85. package/src/components/openapi/GraphqlOperation.astro +186 -0
  86. package/src/components/openapi/GraphqlType.astro +154 -0
  87. package/src/components/openapi/MethodBadge.astro +3 -14
  88. package/src/components/openapi/Operation.astro +12 -5
  89. package/src/components/openapi/OperationPanel.astro +43 -0
  90. package/src/components/openapi/RequestPanel.astro +5 -10
  91. package/src/components/openapi/Responses.astro +1 -16
  92. package/src/components/openapi/graphql-helpers.ts +466 -0
  93. package/src/components/openapi/playground-client.ts +15 -0
  94. package/src/components/openapi/sample-panels.ts +45 -0
  95. package/src/components/openapi/snippets.ts +13 -35
  96. package/src/core/base-path.ts +11 -0
  97. package/src/core/config-input.ts +209 -2
  98. package/src/core/config.ts +6 -4
  99. package/src/core/content-assets.ts +15 -4
  100. package/src/core/data.ts +18 -2
  101. package/src/core/diagnostics.ts +8 -0
  102. package/src/core/frontmatter.ts +20 -8
  103. package/src/core/github.ts +71 -0
  104. package/src/core/graph.ts +22 -8
  105. package/src/core/heading-markers.ts +96 -0
  106. package/src/core/i18n-ui.ts +11 -0
  107. package/src/core/includes.ts +632 -0
  108. package/src/core/last-modified.ts +36 -11
  109. package/src/core/links.ts +79 -13
  110. package/src/core/meta.ts +2 -1
  111. package/src/core/nav-diagnostics.ts +11 -2
  112. package/src/core/navigation.ts +27 -6
  113. package/src/core/project-graph.ts +61 -9
  114. package/src/core/schema.ts +226 -35
  115. package/src/core/server-features.ts +5 -9
  116. package/src/core/sources/github-releases.ts +2 -2
  117. package/src/core/sources/normalize.ts +502 -115
  118. package/src/core/sources/notion.ts +43 -8
  119. package/src/core/sources/obsidian.ts +1038 -0
  120. package/src/core/sources/read.ts +36 -1
  121. package/src/core/sources/resolve.ts +34 -1
  122. package/src/core/sources/types.ts +28 -6
  123. package/src/core/sources/watch.ts +12 -8
  124. package/src/core/tsconfig-aliases.ts +48 -35
  125. package/src/core/types.ts +25 -2
  126. package/src/core/ui-packs/ar.ts +1 -0
  127. package/src/core/ui-packs/bg.ts +2 -0
  128. package/src/core/ui-packs/bn.ts +1 -0
  129. package/src/core/ui-packs/ca.ts +2 -0
  130. package/src/core/ui-packs/cs.ts +1 -0
  131. package/src/core/ui-packs/da.ts +1 -0
  132. package/src/core/ui-packs/de.ts +2 -0
  133. package/src/core/ui-packs/el.ts +2 -0
  134. package/src/core/ui-packs/es.ts +2 -0
  135. package/src/core/ui-packs/fa.ts +1 -0
  136. package/src/core/ui-packs/fi.ts +1 -0
  137. package/src/core/ui-packs/fr.ts +2 -0
  138. package/src/core/ui-packs/he.ts +1 -0
  139. package/src/core/ui-packs/hi.ts +1 -0
  140. package/src/core/ui-packs/hr.ts +2 -0
  141. package/src/core/ui-packs/hu.ts +2 -0
  142. package/src/core/ui-packs/id.ts +2 -0
  143. package/src/core/ui-packs/it.ts +1 -0
  144. package/src/core/ui-packs/ja.ts +2 -0
  145. package/src/core/ui-packs/ko.ts +2 -0
  146. package/src/core/ui-packs/nl.ts +2 -0
  147. package/src/core/ui-packs/no.ts +2 -0
  148. package/src/core/ui-packs/pl.ts +2 -0
  149. package/src/core/ui-packs/pt-br.ts +2 -0
  150. package/src/core/ui-packs/pt.ts +2 -0
  151. package/src/core/ui-packs/ro.ts +2 -0
  152. package/src/core/ui-packs/ru.ts +2 -0
  153. package/src/core/ui-packs/sk.ts +1 -0
  154. package/src/core/ui-packs/sr.ts +1 -0
  155. package/src/core/ui-packs/sv.ts +2 -0
  156. package/src/core/ui-packs/th.ts +1 -0
  157. package/src/core/ui-packs/tr.ts +2 -0
  158. package/src/core/ui-packs/uk.ts +2 -0
  159. package/src/core/ui-packs/vi.ts +1 -0
  160. package/src/core/ui-packs/zh-tw.ts +1 -0
  161. package/src/core/ui-packs/zh.ts +1 -0
  162. package/src/core/version-cut.ts +21 -3
  163. package/src/core/yaml.ts +26 -0
  164. package/src/deploy/function-bundle.ts +251 -0
  165. package/src/eval/schema.ts +3 -1
  166. package/src/markdown/code-title.ts +22 -16
  167. package/src/markdown/features.ts +21 -0
  168. package/src/markdown/fence-meta.ts +50 -0
  169. package/src/markdown/heading-anchors.ts +198 -37
  170. package/src/markdown/include.ts +247 -0
  171. package/src/markdown/index.ts +43 -34
  172. package/src/markdown/language-icon.ts +2 -2
  173. package/src/markdown/mdast.ts +7 -3
  174. package/src/markdown/ts2js.ts +264 -0
  175. package/src/openapi/asyncapi.ts +4 -1
  176. package/src/openapi/graphql-build.ts +293 -0
  177. package/src/openapi/graphql.ts +212 -0
  178. package/src/openapi/model.ts +38 -5
  179. package/src/openapi/parse.ts +34 -0
  180. package/src/openapi/proxy.ts +30 -5
  181. package/src/openapi/references.ts +89 -13
  182. package/src/openapi/render-mdx.ts +48 -8
  183. package/src/openapi/scalar.ts +5 -12
  184. package/src/openapi/source.ts +91 -23
  185. package/src/registry/eject.ts +11 -0
  186. package/src/search/documents.ts +229 -37
  187. package/src/search/orama-index.ts +9 -5
  188. package/src/seo/jsonld.ts +293 -51
  189. package/src/theme/code-block-padding.ts +16 -0
  190. package/src/theme/entry.ts +65 -11
  191. package/src/theme/fonts.ts +189 -16
  192. package/src/translate/prompts.ts +2 -0
  193. package/src/translate/run.ts +7 -0
  194. package/src/translate/work-list.ts +0 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: Ignored by the including page
3
+ ---
4
+
5
+ :::tip
6
+ This callout lives in `_snippets/include-demo.mdx` — it renders here because the page splices it with an `<include>` statement.
7
+ :::
@@ -12,7 +12,7 @@ openapi: {
12
12
  }
13
13
  ```
14
14
 
15
- That mounts the reference at `/reference` (an overview page) with each operation at `/reference/<tag>/<operation>`. The `spec` is either an `http(s)` URL or a path to a local file in your project. Blume parses it with [Scalar's OpenAPI parser](https://github.com/scalar/scalar) — Swagger 2.0 and OpenAPI 3.0 specs are upgraded to 3.1 automatically.
15
+ That mounts the reference at `/reference` (an overview page) with each operation at `/reference/<tag>/<operation>`. The `spec` is either an `http(s)` URL or a path to a local file in your project. Blume parses it with [Scalar's OpenAPI parser](https://github.com/scalar/scalar) — Swagger 2.0 and OpenAPI 3.0 specs are upgraded to 3.1 automatically. Documenting a GraphQL API instead? See the [GraphQL reference](/docs/advanced/graphql).
16
16
 
17
17
  The reference doesn't add a header tab on its own. To surface it, point a [navigation tab](/docs/content/navigation#tabs) at its route — this also scopes the operations sidebar for the native renderer:
18
18
 
@@ -165,7 +165,7 @@ openapi: {
165
165
  }
166
166
  ```
167
167
 
168
- A Scalar-rendered reference is a self-contained embed on its own route — it doesn't weave into Blume's sidebar, search, or `llms.txt`, and Blume's [`playground`](#try-it-playground) config doesn't apply to it. Scalar brings its own request client, which calls your **target API directly from the browser** (the `playground.proxy` route isn't available here), so the API must allow cross-origin requests from the docs site (`Access-Control-Allow-Origin`). `theme` applies to the Scalar renderer only.
168
+ A Scalar-rendered reference is a self-contained embed on its own route — it doesn't weave into Blume's sidebar, search, or `llms.txt`, and Blume's [`playground`](#try-it-playground) config doesn't apply to it. It does follow Blume's light/dark toggle: the embed is pinned to the page's theme when it mounts and switches with it, so Scalar's own theme switch is hidden (set `scalar.forceDarkModeState` or `scalar.darkMode` to hand color mode back to Scalar). Scalar brings its own request client, which calls your **target API directly from the browser** (the `playground.proxy` route isn't available here), so the API must allow cross-origin requests from the docs site (`Access-Control-Allow-Origin`). `theme` applies to the Scalar renderer only.
169
169
 
170
170
  ### Passing Scalar options
171
171
 
@@ -225,7 +225,7 @@ asyncapi: {
225
225
  ```
226
226
 
227
227
  :::note
228
- `playground.proxy` is OpenAPI-only. It forwards HTTP requests, and a WebSocket connect goes straight from the browser to the server named in the URL, so there's nothing for a proxy to sit in front of.
228
+ `playground.proxy` doesn't apply to event operations. It forwards HTTP requests, and a WebSocket connect goes straight from the browser to the server named in the URL, so there's nothing for a proxy to sit in front of.
229
229
  :::
230
230
 
231
231
  The event composer collects no broker credentials. Each operation page's **Authorization** section documents what the broker expects, and a WebSocket connect carries only what's already in the URL. Nothing is persisted for event operations.
@@ -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, ask, og, analytics, feedback, structuredData, toc, codeThemes, codeWrap, and imageZoom.",
77
+ "Resolved site settings: title, description, logo, favicon, appleIcon, banner, theme, site, repoUrl, github (owner, repo, host, and the REST api base — null when unset), search, i18n, mcp, ask, og, analytics, feedback, structuredData, toc, codeThemes, codeWrap, and imageZoom.",
78
78
  },
79
79
  navigation: {
80
80
  type: "Navigation",
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: GraphQL
3
+ description: Drop in a GraphQL schema and get a native API reference — one real page per operation and per type, in your sidebar and search.
4
+ ---
5
+
6
+ Point Blume at a GraphQL schema and it generates a native API reference: one **real page per root field** — queries, mutations, and subscriptions — plus one **page per named type** (objects, input objects, enums, interfaces, unions, and custom scalars). Every page shows arguments, defaults, deprecations, and usage backlinks, alongside a generated example operation, code samples, and an interactive [Try it](#try-it-playground) panel. Because each page 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.
7
+
8
+ ```ts blume.config.ts lineNumbers
9
+ graphql: {
10
+ enabled: true,
11
+ spec: "./schema.graphql",
12
+ endpoint: "https://api.example.com/graphql",
13
+ }
14
+ ```
15
+
16
+ That mounts the reference at `/graphql` (an overview page), with root fields at `/graphql/queries/<field>`, `/graphql/mutations/<field>`, and `/graphql/subscriptions/<field>`, and types grouped by kind at `/graphql/objects/<type>`, `/graphql/enums/<type>`, and so on.
17
+
18
+ The `spec` is either a path to a local file in your project or an `http(s)` URL, and accepts two formats:
19
+
20
+ - **SDL text** — a `.graphql` file with type definitions.
21
+ - **An introspection result** — the JSON produced by running the standard introspection query, either the raw `{ "__schema": … }` shape or the full `{ "data": { "__schema": … } }` response envelope.
22
+
23
+ The `endpoint` is the live GraphQL API URL. A schema, unlike an OpenAPI document, names no server — so the endpoint is what the Try it panel and the generated code samples target. Leave it off and the samples render with a placeholder URL readers replace.
24
+
25
+ The reference doesn't add a header tab on its own. To surface it, point a [navigation tab](/docs/content/navigation#tabs) at its route — this also scopes the reference sidebar:
26
+
27
+ ```ts blume.config.ts
28
+ navigation: {
29
+ tabs: [{ label: "GraphQL", path: "/graphql" }],
30
+ }
31
+ ```
32
+
33
+ ## Generated examples
34
+
35
+ Every operation page carries a complete, valid example operation — one variable per argument, typed off the schema, with a bounded-depth selection set over the return type — plus matching example variables and an example response that mirrors the same selection. Code samples show the exact HTTP request (a JSON `POST` of `{ query, variables }`) in each configured language:
36
+
37
+ ```ts blume.config.ts lineNumbers
38
+ graphql: {
39
+ enabled: true,
40
+ spec: "./schema.graphql",
41
+ codeSamples: ["curl", "js"], // built in: curl, js, python
42
+ }
43
+ ```
44
+
45
+ ## Type pages
46
+
47
+ Named types get their own deep-linkable pages, grouped by kind in the sidebar: fields and input fields with their types linked, enum values, union members, interface implementations, and a **Used by** section listing the operations that return or accept the type and the other types that reference it. Spec-defined scalars (`String`, `Int`, …) don't get pages; custom scalars do, including their `specifiedBy` URL.
48
+
49
+ ## Multiple schemas
50
+
51
+ Each entry in `sources` renders one schema on its own route. A per-source `endpoint` overrides the block-level one:
52
+
53
+ ```ts blume.config.ts lineNumbers
54
+ graphql: {
55
+ enabled: true,
56
+ endpoint: "https://api.example.com/graphql",
57
+ sources: [
58
+ { label: "Public API", spec: "./schema.graphql" },
59
+ {
60
+ label: "Admin API",
61
+ route: "/graphql-admin",
62
+ spec: "./admin.graphql",
63
+ endpoint: "https://admin.example.com/graphql",
64
+ },
65
+ ],
66
+ }
67
+ ```
68
+
69
+ Each source takes the same per-source controls as the [OpenAPI block](/docs/advanced/api-reference): `includeInSearch`, `includeInLlms`, and `noindex`.
70
+
71
+ ## Try it playground
72
+
73
+ Query and mutation pages render an interactive panel: edit the request body (the query and variables), point it at your endpoint or a custom URL, and send — the code samples update live so what you copy is byte-for-byte what was sent. Disable it with `playground: false`. Subscription pages show the generated operation and an example event instead: subscriptions run over a stateful transport (WebSocket or SSE) that the playground's single HTTP `POST` can't speak.
74
+
75
+ If your GraphQL API doesn't allow cross-origin requests from the docs site, route sends through a CORS proxy — a URL of your own, or `true` for the built-in `/_api-proxy` endpoint (which requires `deployment.output: "server"`). The built-in proxy only forwards to origins your documented specs declare — each configured GraphQL `endpoint`, plus any absolute `servers[].url` from a documented [OpenAPI spec](/docs/advanced/api-reference) — so a public docs deployment can't be aimed at other hosts. That makes `endpoint` required for a working proxy: without one, the proxy has no origin to allow for this reference and refuses every send (the build warns about this).
76
+
77
+ ```ts blume.config.ts lineNumbers
78
+ graphql: {
79
+ enabled: true,
80
+ spec: "./schema.graphql",
81
+ endpoint: "https://api.example.com/graphql",
82
+ playground: { proxy: true },
83
+ }
84
+ ```
@@ -2,6 +2,13 @@ import { defineMeta } from "blume";
2
2
 
3
3
  export default defineMeta({
4
4
  order: 5,
5
- pages: ["skills", "custom-pages", "changelog", "blog", "api-reference"],
5
+ pages: [
6
+ "skills",
7
+ "custom-pages",
8
+ "changelog",
9
+ "blog",
10
+ "api-reference",
11
+ "graphql",
12
+ ],
6
13
  title: "Advanced",
7
14
  });
@@ -33,6 +33,22 @@ ai: {
33
33
  }
34
34
  ```
35
35
 
36
+ The object form also takes `details`: Markdown placed right after the title and summary in `llms.txt`, before the page sections — the [llms.txt spec](https://llmstxt.org)'s free-form "details" block. It's the place to tell agents _when_ to reach for your product and how to call it, which readiness scanners look for explicitly; an install command and the package name belong here too:
37
+
38
+ ```ts blume.config.ts lineNumbers
39
+ ai: {
40
+ llmsTxt: {
41
+ details: [
42
+ "## When to use Acme",
43
+ "",
44
+ "Reach for Acme when a project needs hosted feature flags. Install the CLI with `npm install -g acme`; the API reference below covers every endpoint.",
45
+ ].join("\n"),
46
+ },
47
+ }
48
+ ```
49
+
50
+ `llms.txt` closes with two generated sections that need no configuration. **Agent skills** lists each skill published through [`ai.skills`](#skills-discovery) with its description (where a skill says when to use it). **Agent resources** links every machine-readable artifact the build emits — `llms-full.txt`, the per-page [raw Markdown](#raw-markdown) mirror, the [MCP server](#mcp-server) and its discovery document, the skills index, the [API catalog](#api-catalog), [`agent-readability.json`](#agent-readability), and the [sitemap](/docs/configuration/seo#sitemap) — each only when it exists, so an agent that reads nothing but `llms.txt` still finds the whole surface.
51
+
36
52
  To keep an individual page out of both files, set `ai.exclude` in its frontmatter:
37
53
 
38
54
  ```mdx
@@ -59,7 +75,7 @@ Append `.md` or `.mdx` to any page's URL to fetch its raw Markdown source — pe
59
75
 
60
76
  Nested routes work the same way (`/content/syntax.md`), and the home page is served at `/index.md`.
61
77
 
62
- 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. Props are evaluated with the page's `frontmatter` in scope, so a prop like `title={frontmatter.status}` resolves to the same value the rendered page shows. 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.
78
+ 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, `<Card>` its title as a link over its body (and `<CardGroup>` the cards it holds), and `<YouTube>` a link. Props are evaluated with the page's `frontmatter` in scope, so a prop like `title={frontmatter.status}` resolves to the same value the rendered page shows. 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.
63
79
 
64
80
  ### Content negotiation
65
81
 
@@ -85,7 +101,7 @@ export default defineConfig({
85
101
  });
86
102
  ```
87
103
 
88
- 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.
104
+ For container components, `childComponents("Name")` extracts direct children by tag — the same way the built-in `<Steps>` serializer collects its `<Step>` items — and `childBlocks()` returns every direct child in order, components and prose alike, each already downleveled to a block of Markdown (the built-in `<CardGroup>` serializer is just those blocks joined by blank lines). A same-name entry replaces a built-in serializer, so you can restyle how `<Callout>` downlevels — or return `null` to opt one out entirely.
89
105
 
90
106
  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.
91
107
 
@@ -101,6 +117,8 @@ The **Open in chat** action opens the current page in an AI assistant — v0, Ch
101
117
 
102
118
  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.
103
119
 
120
+ The prompt is part of the [UI dictionary](/docs/content/i18n#translated-ui) (`actions.openInChatPrompt`), so localized sites send it in their language, and `i18n.ui` can override the wording — keep the `{url}` placeholder, which is replaced with the page's raw-Markdown URL.
121
+
104
122
  To tailor the action, set `ai.openInChat`. `false` hides it entirely, and an array of provider keys — `"v0"`, `"chatgpt"`, `"claude"`, `"t3"`, `"scira"`, `"cursor"` — shows just those providers, in the order you list them:
105
123
 
106
124
  ```ts blume.config.ts lineNumbers
@@ -306,7 +324,7 @@ ai: {
306
324
  | `name` | title | Server name shown to clients (defaults to title). |
307
325
  | `instructions` | — | Optional system hint passed to connecting agents. |
308
326
 
309
- 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`. The server card follows the [SEP-2127](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127) Server Card extension schema (reverse-DNS `name`, `remotes` transport endpoints), with initialize-shaped compat fields (`serverInfo`, `capabilities`, `transports`) for scanners built against the proposal's earlier revision. 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).
327
+ The server exposes read-only tools — `search_docs`, `get_page`, `list_pages`, and `get_navigation` — and every page as an MCP resource (`resources/list` enumerates the pages at their served URLs with a `text/markdown` type; `resources/read` returns the page's agent Markdown, the same output as `get_page`), so clients that attach context by URI can browse the docs without calling a tool. It publishes discovery documents at `/.well-known/mcp.json` and `/.well-known/mcp/server-card.json`. The server card follows the [SEP-2127](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127) Server Card extension schema (reverse-DNS `name`, `remotes` transport endpoints), with initialize-shaped compat fields (`serverInfo`, `capabilities`, `transports`) for scanners built against the proposal's earlier revision. 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).
310
328
 
311
329
  `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.
312
330
 
@@ -285,6 +285,30 @@ github: {
285
285
  | `repo` | — | Repository name. |
286
286
  | `branch` | `"main"` | Branch that edit links point at. |
287
287
  | `dir` | — | Path from the repo root to the project root (for monorepos). |
288
+ | `host` | `"https://github.com"` | Origin of the GitHub instance, for Enterprise installations. Must be HTTP(S); normalized to its origin. |
289
+ | `api` | derived from `host` | REST API base, for `<GithubInfo>` against an Enterprise instance. Must be HTTP(S); normalized to an origin and path. |
290
+
291
+ ### GitHub Enterprise
292
+
293
+ Docs whose repository lives on a GitHub Enterprise instance set `host`, and every repo-derived link — the header mark, edit links, the agent manifest — points at that instance instead of the public site:
294
+
295
+ ```ts blume.config.ts lineNumbers
296
+ github: {
297
+ host: "https://github.acme.com",
298
+ owner: "acme",
299
+ repo: "docs",
300
+ }
301
+ ```
302
+
303
+ The REST API base that [`<GithubInfo>`](/docs/content/components) queries is derived from `host`: an Enterprise Cloud tenant with data residency (`acme.ghe.com`) is served from its `api.` subdomain, and any other host is treated as Enterprise Server (`/api/v3`). Set `api` explicitly when your instance sits somewhere else.
304
+
305
+ :::warning
306
+ An instance reachable only over plain HTTP still renders its counts, but `GITHUB_TOKEN` is withheld from the request rather than sent in cleartext — so a private repo's card comes back without them.
307
+ :::
308
+
309
+ :::note
310
+ `host` covers links Blume derives from `github`. To point only the header mark somewhere else — an organization, say, when the docs repo itself is private — use [`navigation.repo`](/docs/content/navigation#repository-link) with an absolute URL.
311
+ :::
288
312
 
289
313
  ## Last modified
290
314
 
@@ -40,7 +40,19 @@ Write `href` as if the site were mounted at the root — a `basePath` is applied
40
40
 
41
41
  ## What's indexed
42
42
 
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.
43
+ For Orama, FlexSearch, Algolia, Orama Cloud, and Typesense — and the MCP server's `search_docs` tool — Blume indexes each page's title, description, and body reduced to plain text: code blocks, images, and markup are stripped, so results stay relevant. These indexes are built from your source files, so they're identical in dev and production. Pagefind indexes the built HTML instead, and Mixedbread syncs your raw Markdown, so both always search code.
44
+
45
+ If your docs rely on code examples for searchable terms like options, methods, or error names, opt fenced code into the source-built indexes:
46
+
47
+ ```ts blume.config.ts lineNumbers
48
+ search: {
49
+ indexing: {
50
+ includeCodeBlocks: true,
51
+ },
52
+ },
53
+ ```
54
+
55
+ Each fence's body and title (`blume.config.ts` above) become searchable; the language and fence markers don't. On `.mdx` pages the index reads components as the text they show — a Card's title, a Tab's label, a TypeTable's descriptions — using the same serializers as the [agent surfaces](/docs/configuration/ai), so an `ai.markdownComponents` entry covers your own components too. The option has no effect on Pagefind or Mixedbread. Expect the index to grow with your fenced content — the client index ships to every reader, hosted providers cap record size (Algolia rejects the sync batch when one page's record exceeds its plan's limit, leaving the previous index live), and a hit inside a fence shows flattened code in the result excerpt.
44
56
 
45
57
  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
58
 
@@ -247,6 +247,33 @@ Each page includes:
247
247
 
248
248
  URLs are absolute when `deployment.site` is set. Pages marked `seo.noindex` are skipped.
249
249
 
250
+ ### Site identity
251
+
252
+ The WebSite node says a site exists; it doesn't say _what_ it is or _who_ runs it — which is what AI agents read JSON-LD for before they recommend or cite you. Two optional blocks fill that in, both needing `deployment.site` (the nodes carry absolute identifiers):
253
+
254
+ ```ts blume.config.ts lineNumbers
255
+ seo: {
256
+ organization: {
257
+ name: "Acme", // defaults to the site title
258
+ email: "hello@acme.com",
259
+ telephone: "+1 555 0100",
260
+ address: { addressLocality: "Sydney", addressCountry: "AU" },
261
+ logo: "/logo.svg",
262
+ sameAs: ["https://github.com/acme", "https://x.com/acme"],
263
+ },
264
+ software: {
265
+ license: "MIT",
266
+ operatingSystem: "Node.js 22+",
267
+ price: 0, // emitted as an Offer; 0 marks it free
268
+ sameAs: ["https://www.npmjs.com/package/acme"],
269
+ },
270
+ }
271
+ ```
272
+
273
+ `organization` adds an **Organization** node to every page — the WebSite and article nodes cite it as `publisher` — with the email and telephone as a `ContactPoint` (`contactType` defaults to `"customer support"`) and the address as a `PostalAddress`, the two fields business-verification checks look for. `name` and `url` default to the site's; a root-relative `logo` is absolutized like any page URL.
274
+
275
+ `software` adds a **SoftwareApplication** node to the homepage: the product's name and description (defaulting to the site's), `applicationCategory` (default `"DeveloperApplication"`), operating system, license, an `Offer` when `price` is set, and the registry or repository URLs in `sameAs`. Pass `software: true` to take every default. The organization, when configured, is cited as its `publisher`.
276
+
250
277
  ## Sitemap
251
278
 
252
279
  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:
@@ -105,8 +105,25 @@ theme: {
105
105
  - **`name`** — the family name exactly as the provider lists it.
106
106
  - **`provider`** — where the family comes from: `google` (default), `fontsource`, `bunny`, or `fontshare`.
107
107
  - **`weights`** — the weights to load, as numbers or a variable range like `"100..900"`. Defaults to `[400, 500, 600, 700]`.
108
+ - **`subsets`** — the character subsets to load, by the provider's names (`latin`, `latin-ext`, `vietnamese`, `cyrillic`, `greek`, …). Defaults to `latin` plus whatever your configured locales need — see below.
108
109
  - **`fallback`** — the system stack shown while the font loads and for missing glyphs: `sans`, `serif`, or `mono`. Defaults to `mono` for the mono role and `sans` otherwise.
109
110
 
111
+ #### Subsets and locales
112
+
113
+ Google, Bunny, and Fontsource split each family into per-script subsets, and only the subsets you load get a `@font-face`. Blume derives the list from your [`i18n.locales`](/docs/content/i18n): a site with Vietnamese, Polish, Russian, or Greek locales loads `vietnamese`, `latin-ext`, `cyrillic`, or `greek` alongside `latin`, so diacritics and non-Latin letters render in your chosen font instead of the system fallback. Sites without an `i18n` block, or with only Latin-1 languages, load `latin` alone. Browsers download a subset only when a page uses its characters, and preloads follow the same list.
114
+
115
+ Set `subsets` on a family to override the derived list — say, for a single-locale site whose content still needs a script its locale doesn't imply:
116
+
117
+ ```ts blume.config.ts lineNumbers
118
+ theme: {
119
+ fonts: {
120
+ body: { name: "Be Vietnam Pro", subsets: ["latin", "vietnamese"] },
121
+ },
122
+ }
123
+ ```
124
+
125
+ Curated slugs like `inter` follow the locale-derived list; use the object form to pin subsets for those families too.
126
+
110
127
  #### Local font files
111
128
 
112
129
  For a font you own (or one no provider serves), point a role at font files in your project. Each variant becomes one `@font-face`:
@@ -71,6 +71,8 @@ Switch between equivalent content in place — language variants, OS-specific co
71
71
 
72
72
  Add `inline` to render borderless — a tab strip on a full-width rule with the content flowing beneath as prose — instead of the bordered box. Add `param` to sync the active tab to a URL query param instead of the hash, which makes the selection shareable: a link ending in `?install=windows` opens on the Windows tab. Each group syncs to its own `param`, so you can use several independent, deep-linkable groups on one page.
73
73
 
74
+ Groups with same-titled tabs switch together — pick "macOS" in one and every group with a macOS tab follows. Add `syncKey` to scope that syncing: only groups sharing the same key switch together, so unrelated groups that happen to share a tab title stay independent.
75
+
74
76
  <Tabs inline param="install">
75
77
  <Tab title="macOS">Use Homebrew to install the toolchain.</Tab>
76
78
  <Tab title="Windows">Use winget to install the toolchain.</Tab>
@@ -546,6 +548,8 @@ export interface ButtonProps {
546
548
 
547
549
  A card linking to a GitHub repository with its live star and fork counts. Counts are fetched at build time — no client JavaScript — and the card still renders if the API is unreachable. Pass `owner` and `repo`, or omit them to use the repository from your `blume.config`. Set a `GITHUB_TOKEN` environment variable to lift the API rate limit.
548
550
 
551
+ The card reads the instance from [`github.host`](/docs/configuration#github-enterprise), so on an Enterprise-hosted site explicit `owner`/`repo` address that instance too. Pass `host` to point one card somewhere else — a public project from an Enterprise site, say; the REST base is derived from it the same way it is from `github.host`.
552
+
549
553
  <GithubInfo owner="haydenbleasel" repo="blume" />
550
554
 
551
555
  ```astro lineNumbers
@@ -554,6 +558,9 @@ A card linking to a GitHub repository with its live star and fork counts. Counts
554
558
 
555
559
  <!-- Or point it at any repository -->
556
560
  <GithubInfo owner="haydenbleasel" repo="blume" />
561
+
562
+ <!-- Or at a repository on another instance -->
563
+ <GithubInfo host="https://github.com" owner="haydenbleasel" repo="blume" />
557
564
  ```
558
565
 
559
566
  ## Component
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: Includes
3
+ description: Reuse content across pages — splice shared Markdown, MDX, or code files into any page with the include syntax.
4
+ ---
5
+
6
+ Write a snippet once and splice it into any page. An `<include>` statement on its own line embeds another file at build time, as if its content were written inline — headings join the page's table of contents, text is indexed by search, and the content appears in the page's `.md` mirror and llms-full.txt.
7
+
8
+ ```mdx
9
+ <include>./_snippets/prerequisites.mdx</include>
10
+ ```
11
+
12
+ The path is resolved relative to the including file. Paths starting with `/` resolve from your content root, so deeply nested pages can reference shared snippets without `../../..` chains:
13
+
14
+ ```mdx
15
+ <include>/_snippets/prerequisites.mdx</include>
16
+ ```
17
+
18
+ The syntax matches Fumadocs' include syntax, so migrated content works unchanged.
19
+
20
+ Here it is live — this next callout is spliced from a shared snippet:
21
+
22
+ <include>../_snippets/include-demo.mdx</include>
23
+
24
+ ## Partials
25
+
26
+ Any file whose name (or folder) starts with an underscore is excluded from routing, navigation, search, and sitemaps by default — that convention is the natural home for shared snippets:
27
+
28
+ ```text
29
+ docs/
30
+ _snippets/
31
+ prerequisites.mdx
32
+ cli-flags.md
33
+ guides/
34
+ quickstart.mdx ← <include>../_snippets/prerequisites.mdx</include>
35
+ index.mdx
36
+ ```
37
+
38
+ A partial is a normal Markdown or MDX file. Its front matter is stripped when spliced (the including page's front matter wins), and everything else — callouts, code fences, components, math — renders exactly as it would inline. Partials can include other partials; a circular include is reported as an error.
39
+
40
+ Relative image references inside a partial keep working: they're rebased onto the including page, so a colocated `![diagram](./diagram.png)` next to the partial resolves wherever the partial is spliced.
41
+
42
+ Editing a partial while `blume dev` is running reloads every page that includes it.
43
+
44
+ ## Including code files
45
+
46
+ A target that isn't `.md`/`.mdx` is embedded as a fenced code block, with the language inferred from the extension. Use `lang` to override the language (or to show a Markdown file as source rather than splicing it), and `meta` to pass a fence meta string such as a title:
47
+
48
+ ```mdx
49
+ <include>./examples/config.ts</include>
50
+
51
+ <include lang="ts" meta='title="blume.config.ts"'>
52
+ ../blume.config.ts
53
+ </include>
54
+
55
+ <include lang="mdx">./_snippets/prerequisites.mdx</include>
56
+ ```
57
+
58
+ ## Rules and diagnostics
59
+
60
+ Include statements must occupy their own line — they're block-level, not inline. Statements inside fenced code blocks are left alone, so you can document the syntax itself (like this page does).
61
+
62
+ Targets must live inside your content root: a file outside it would be silently missing from version snapshots and ejected projects, so `blume` reports `BLUME_INCLUDE_OUTSIDE_ROOT` instead of splicing it. A target that doesn't exist is `BLUME_INCLUDE_NOT_FOUND`, and a loop of includes is `BLUME_INCLUDE_CYCLE` — all three fail `blume build` (pass `--no-strict` to build anyway) and appear in `blume validate`.
63
+
64
+ Broken links inside a partial are reported against the partial file, not the pages that splice it, so you fix them where they live.
65
+
66
+ :::note
67
+ Partials are shared across locales and are not translated by `blume translate` — keep partials language-neutral (code, tables, diagrams), or create per-locale partials and include them from each locale's pages.
68
+ :::
@@ -6,6 +6,7 @@ export default defineMeta({
6
6
  "navigation",
7
7
  "meta",
8
8
  "syntax",
9
+ "includes",
9
10
  "components",
10
11
  "islands",
11
12
  "sources",
@@ -240,6 +240,21 @@ navigation: {
240
240
 
241
241
  Each item is a page route (a string), a group (`label` + `items`), or a link (`label` + `href`). Groups can nest, override the global [`display` mode](#display-modes), and start `collapsed`.
242
242
 
243
+ ## Header actions
244
+
245
+ `navigation.actions` puts plain links in the header, left of the icon buttons, and `navigation.cta` is the one filled button:
246
+
247
+ ```ts blume.config.ts lineNumbers
248
+ navigation: {
249
+ actions: [{ href: "/changelog", label: "Changelog" }],
250
+ cta: { href: "https://example.com/signup", label: "Start free" },
251
+ }
252
+ ```
253
+
254
+ `cta` is singular on purpose — a docs header has room for exactly one thing a reader is being asked to do, and a row of buttons asks for nothing. Secondary links belong in `actions`, or in [`featured`](#featured-links) if they should sit with the sidebar instead.
255
+
256
+ An `http(s)` or protocol-relative href opens in a new tab; a route stays in the tab, and is validated against your pages at build time like a `featured` link — so a page served by another app on the same host (`/signup` on the product, say) should be written as an absolute URL. `actions` are hidden below the `sm` breakpoint, where the header has room for the logo and the navigation toggle and nothing else. `cta` hides there too, except on a page with no navigation toggle — a landing page on `PageLayout` with no tabs — where it stays, since nothing else can surface it on a phone.
257
+
243
258
  ## Repository link
244
259
 
245
260
  When you set [`github`](/docs/configuration) in your config, Blume shows a GitHub icon in the header — beside the theme toggle — that links to your repository. It's on by default; hide it with `navigation.repo`:
@@ -252,6 +267,16 @@ navigation: {
252
267
 
253
268
  The link only appears when `github` is configured, so projects without a repo are unaffected either way.
254
269
 
270
+ `repo` also takes an absolute URL, which points the header mark anywhere on GitHub:
271
+
272
+ ```ts blume.config.ts lineNumbers
273
+ navigation: {
274
+ repo: "https://github.com/acme",
275
+ }
276
+ ```
277
+
278
+ That's for a project whose docs repo is private. `github` drives the per-page edit link, the header mark and the [agent manifest](/docs/configuration/ai)'s repository together, so such a project has to leave `github` unset — and a URL is what lets it still show a mark pointing somewhere public. The icon stays the GitHub mark, so a link to another host belongs in [`actions`](#header-actions).
279
+
255
280
  ## Breadcrumbs and pagination
256
281
 
257
282
  These come for free from the sidebar tree — no configuration:
@@ -43,6 +43,45 @@ export default defineConfig({
43
43
 
44
44
  If two sources resolve to the same route, Blume reports a `BLUME_DUPLICATE_ROUTE` build error — give each source a distinct `prefix`.
45
45
 
46
+ ## Obsidian
47
+
48
+ The built-in `obsidian` source reads an [Obsidian](https://obsidian.md) vault in place. There is no export step and nothing generated into your repo: the vault stays the source of truth, and Blume lowers Obsidian's dialect to Markdown as it loads.
49
+
50
+ ```ts blume.config.ts
51
+ import { defineConfig } from "blume";
52
+
53
+ export default defineConfig({
54
+ content: {
55
+ sources: [
56
+ { type: "filesystem", root: "docs" },
57
+ {
58
+ type: "obsidian",
59
+ prefix: "notes",
60
+ vault: "vault",
61
+ // Vault folder names to skip at any depth, on top of dot-folders
62
+ exclude: ["Templates", "Daily"],
63
+ },
64
+ ],
65
+ },
66
+ });
67
+ ```
68
+
69
+ `[[Wikilinks]]` become route links, addressed by note name across the whole vault rather than by path, the way Obsidian addresses notes. Custom link text (`[[Note|label]]`), heading anchors (`[[Note#Install]]`), full paths (`[[folder/Note]]` and `[[folder/Note.md]]`), the partial paths Obsidian's default "shortest path when possible" setting writes (`[[guides/Note]]`), and the `[[Note\|label]]` form Obsidian writes inside a table cell all work, and a note that sets `slug` in its frontmatter is linked at the route that slug publishes. When two notes share a name, a note whose full vault path is exactly that name wins — Obsidian resolves a link as a path before a name — then the first in vault order (folders before notes, case-insensitively, like Obsidian's file explorer). Blume warns only when a wikilink actually resolves through such a collision; write a longer path to disambiguate. A block reference (`[[Note#^id]]`) links to its note without an anchor: blocks render with no id to land on. A heading anchor resolves against the target note's real headings, matched the way Obsidian's autocomplete writes them (with `**bold**`, `` `code` ``, and link syntax stripped) and slugged by the same `extractHeadings` pass that fills the page manifest — so a link to `#Install` lands on the heading rather than on an id no page emits. `[[#Install]]` addresses a heading in the note you are writing. A link to a heading that doesn't exist keeps the page link, drops the anchor, and warns.
70
+
71
+ Frontmatter keeps what Blume's [page schema](/docs/reference/frontmatter) accepts plus any key you declare in [`frontmatter.extend`](/docs/reference/frontmatter#custom-keys) (or, for notes of that `type`, a content type's `frontmatter`); every other Obsidian property — Dataview fields, Templater dates, `publish`, and Obsidian's own `tags`, `aliases`, and `cssclasses` — is dropped when a note is lowered, so a vault written with the Properties UI builds without frontmatter errors. `aliases` is dropped rather than resolved — alias link targets are not supported yet. A relative Markdown image beside a note (`![chart](./chart.png)`) is served from the vault, and when the vault lives inside your git repository, vault pages get git-derived ["Last updated" dates](/docs/configuration#last-modified) like any other page. "Edit this page" links resolve through `github.dir`, so a vault that sits beside the docs app in a monorepo still links to its file; a vault outside the repository gets no link.
72
+
73
+ Locale directories and version snapshots inside the vault are read the same way the filesystem source reads them: `fr/Note.md` publishes under `/fr/` with [i18n](/docs/content/i18n) configured, `v1.0/Note.md` under `/v1.0/` with [versions](/docs/content/versioning), and wikilinks to those notes point at the route each one publishes.
74
+
75
+ :::note
76
+ A heading that itself contains a link gets its manifest anchor from the heading's Markdown and its rendered `id` from its text content. The two differ for that heading, so a wikilink to it may land on the page rather than on the section.
77
+ :::
78
+
79
+ A link to an `index` note lands on its folder's route rather than a phantom `/index`. **An unresolved wikilink degrades to plain text with a build warning instead of failing the build**, so a vault mid-refactor still publishes. Single-line `%%comments%%` are stripped, a wikilink inside an HTML comment (`<!-- [[Draft]] -->`) is left alone since Obsidian hides it too, and a note with no `title` in its frontmatter is titled by its filename — the same rule Obsidian itself applies. An `index` note is the one exception: it names a route rather than a note, so its title falls through to Blume's usual derivation (first heading, then the humanized segment). Fenced, indented, and inline code passes through verbatim, so a note documenting the syntax survives.
80
+
81
+ Dot-folders are skipped, including Obsidian's own `.obsidian` config directory and `.trash` — which the dev watcher also ignores, so moving a pane in the app or deleting a note to the trash doesn't rebuild your site. Editing a note does. The directories no content scan reads (`node_modules`, `dist`, `.git`, …) are skipped too, so a vault rooted at the project itself doesn't publish dependency READMEs. Symlinks inside the vault are followed, the way the filesystem source follows them, so a shared folder linked into the vault publishes with it. A vault that sits inside `content.root` must be excluded from the filesystem source (`exclude: ["vault/**"]`); `blume version cut` then leaves it out of the snapshot, since the vault keeps publishing its own notes as current.
82
+
83
+ Not yet lowered: callouts (`> [!note]`) render as plain blockquotes, embeds (`![[image.png]]`) pass through untouched, multi-line `%%comments%%` are left in place, and there is no backlink graph.
84
+
46
85
  ## Remote MDX
47
86
 
48
87
  The built-in `mdx-remote` source fetches raw `.md`/`.mdx` over HTTP. Enumerate files either from a GitHub repo subtree (`github`) or explicitly against a raw base URL (`url` + `files`):
@@ -124,7 +163,7 @@ A read token for a private dataset comes from the `SANITY_TOKEN` environment var
124
163
 
125
164
  ## Notion
126
165
 
127
- The built-in `notion` source turns a Notion database into a collection: each row becomes a page, its properties become frontmatter, and its block tree becomes MDX. Callouts, toggles, columns, and code blocks map to the matching Blume components. `@notionhq/client` is an optional peer dependency.
166
+ The built-in `notion` source turns a Notion database into a collection: each row becomes a page, its properties become frontmatter, and its block tree becomes MDX. Callouts, toggles, columns, and code blocks map to the matching Blume components. `@notionhq/client` (v5 or later) is an optional peer dependency; Blume reads the database through its first data source.
128
167
 
129
168
  ```ts blume.config.ts
130
169
  import { defineConfig } from "blume";
@@ -194,3 +233,5 @@ export default defineConfig({
194
233
  ```
195
234
 
196
235
  A source normalizes its native shape (Portable Text, Notion blocks, remote HTML) to Markdown/MDX text, so the same components and markdown features apply no matter where a page comes from.
236
+
237
+ A custom source that reads local files should set `sourcePath` on each entry and `contentRoot` on the source itself. `sourcePath` names the file in diagnostics and resolves relative images beside it; `contentRoot` bounds the git `log` that dates pages, so without it the source's pages get no git-derived ["Last updated" date](/docs/configuration#last-modified).
@@ -17,6 +17,38 @@ Structure a page with headings. Blume renders your frontmatter `title` as the pa
17
17
  #### Detail
18
18
  ```
19
19
 
20
+ ### Custom anchors
21
+
22
+ Anchor ids are generated from the heading text, so a reworded heading gets a new anchor. Append `[#custom-id]` to pin the anchor instead — the marker never renders, and links keep working no matter how the heading reads. Pinned anchors also stay identical across [translated locales](/docs/content/i18n), where auto-generated ids would otherwise differ per language.
23
+
24
+ ```md
25
+ ## Getting started [#setup]
26
+ ```
27
+
28
+ Link to it as `/page#setup`. The syntax matches Fumadocs, so migrated content works verbatim.
29
+
30
+ The `{#custom-id}` form used by Pandoc, kramdown, and Markdown-based specification toolchains is accepted as an equivalent in `.md` files. In `.mdx`, a bare `{…}` is a JSX expression and the page fails to compile — `blume check` reports the marker as `BLUME_MDX_CURLY_ANCHOR` — so write `[#custom-id]` there, or escape the braces. The escaped form pins the same anchor in both formats, which makes it the right spelling for a partial that `.mdx` pages [include](/docs/content/includes):
31
+
32
+ ```md
33
+ ## Getting started \{#setup\}
34
+ ```
35
+
36
+ Fragment links can also target a raw HTML element with an `id` (`<a id="setup"></a>`); `blume validate` accepts those alongside heading anchors.
37
+
38
+ ### Table of contents markers
39
+
40
+ Two more trailing markers control how a heading appears in the table of contents. `[!toc]` keeps a heading on the page but out of the TOC; `[toc]` does the reverse — the heading shows only in the TOC, as an invisible anchor target, which is useful for labeling sections built from components rather than prose. Markers chain in any order. One exception, inherited from CommonMark: a trailing bracket whose label has a link-reference definition anywhere on the page (`[toc]: /url`) is a shortcut reference link, not a marker, and stays in the heading text.
41
+
42
+ ```md
43
+ ## Appears on the page only [!toc]
44
+
45
+ ## Appears in the TOC only [toc]
46
+
47
+ ## Both markers together [toc] [#custom-id]
48
+ ```
49
+
50
+ Markers are always parsed — a heading that literally ends in marker-shaped text would be treated as marked. Backslash-escaping doesn't help (Markdown resolves `\[` to `[` before the marker parse runs); to show literal marker text at the end of a heading, wrap it in inline code: `` ## Using `[toc]` ``.
51
+
20
52
  ## Emphasis
21
53
 
22
54
  Inline formatting for stressing words, marking deletions, and showing code or keystrokes mid-sentence.
@@ -285,6 +317,42 @@ config.title;
285
317
  ```
286
318
  ````
287
319
 
320
+ ### TypeScript and JavaScript tabs
321
+
322
+ Mark a `ts` or `tsx` block `ts2js` to render it as a tab pair: your TypeScript alongside an auto-generated JavaScript variant, so you maintain one snippet and readers pick their dialect. The conversion strips type syntax and type-only imports while keeping your formatting, comments, and JSX exactly as written — and tabs sync, so choosing JavaScript once switches every pair on the page. Like diagrams and math, this is an MDX-only feature — in a `.md` file the block renders as a plain TypeScript fence.
323
+
324
+ ```ts ts2js
325
+ import { defineConfig } from "blume";
326
+
327
+ interface Author {
328
+ name: string;
329
+ }
330
+
331
+ const author: Author = { name: "Hayden" };
332
+
333
+ export default defineConfig({
334
+ title: `${author.name}'s docs`,
335
+ });
336
+ ```
337
+
338
+ ````md
339
+ ```ts ts2js
340
+ import { defineConfig } from "blume";
341
+
342
+ interface Author {
343
+ name: string;
344
+ }
345
+
346
+ const author: Author = { name: "Hayden" };
347
+
348
+ export default defineConfig({
349
+ title: `${author.name}'s docs`,
350
+ });
351
+ ```
352
+ ````
353
+
354
+ Other fence meta composes: a `title="..."` shows on both tabs, while `{1,4-5}` line ranges apply only to the TypeScript tab (line numbers shift once types are gone). The one exception is `twoslash` — hover types can't carry over to generated code, so a fence marked both stays a plain Twoslash block.
355
+
288
356
  :::note
289
357
  Hide the language icons or wrap long lines instead of scrolling with `markdown: { code: { icons: false, wrap: true } }` in `blume.config.ts`.
290
358
  :::
@@ -542,7 +610,7 @@ $$
542
610
  ```
543
611
 
544
612
  :::note
545
- Math is block-only and on automatically — write `$$…$$` and it renders; write none and KaTeX's stylesheet never ships. There's no inline `$…$` math: a lone `$` (currency, shell variables, code) is always left as literal text, so there's no delimiter to escape and no setting to toggle. Math is an MDX-only feature.
613
+ Math is block-only and on automatically — write `$$…$$` and it renders; write none and KaTeX's stylesheet never ships. There's no inline `$…$` math: a lone `$` (currency, shell variables, code) is always left as literal text, so there's no delimiter to escape and no setting to toggle. Math is an MDX-only feature. The class names inside the rendered markup (`.katex-html`, `.katex-base`, …) are KaTeX's own internals, not a styling contract Blume maintains — they can change when KaTeX upgrades, so target the `.katex-display` wrapper for custom styles.
546
614
  :::
547
615
 
548
616
  ## Smart punctuation