blume 1.5.3 → 1.6.1

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 (209) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/cli/index.js +3949 -1403
  3. package/dist/cli/index.js.map +111 -96
  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 +33 -2
  10. package/dist/types/core/github.d.ts +35 -0
  11. package/dist/types/core/i18n-ui.d.ts +10 -0
  12. package/dist/types/core/navigation.d.ts +69 -0
  13. package/dist/types/core/schema.d.ts +122 -1
  14. package/dist/types/core/sources/types.d.ts +31 -6
  15. package/dist/types/core/types.d.ts +29 -2
  16. package/dist/types/markdown/features.d.ts +21 -0
  17. package/dist/types/openapi/references.d.ts +26 -1
  18. package/dist/types/seo/jsonld.d.ts +105 -0
  19. package/dist/types/theme/fonts.d.ts +34 -4
  20. package/docs/07-faq.mdx +9 -9
  21. package/docs/_snippets/include-demo.mdx +7 -0
  22. package/docs/advanced/api-reference.mdx +13 -4
  23. package/docs/advanced/custom-pages.mdx +4 -2
  24. package/docs/advanced/graphql.mdx +84 -0
  25. package/docs/advanced/meta.ts +8 -1
  26. package/docs/configuration/ai.mdx +25 -3
  27. package/docs/configuration/index.mdx +24 -0
  28. package/docs/configuration/search.mdx +13 -1
  29. package/docs/configuration/seo.mdx +30 -3
  30. package/docs/configuration/theming.mdx +23 -0
  31. package/docs/content/components.mdx +15 -1
  32. package/docs/content/includes.mdx +68 -0
  33. package/docs/content/meta.ts +1 -0
  34. package/docs/content/navigation.mdx +25 -0
  35. package/docs/content/sources.mdx +42 -1
  36. package/docs/content/syntax.mdx +69 -1
  37. package/docs/content/versioning.mdx +15 -9
  38. package/docs/reference/cli.mdx +2 -1
  39. package/package.json +66 -57
  40. package/skills/blume-migrate/SKILL.md +16 -7
  41. package/skills/blume-migrate/references/docusaurus.md +5 -3
  42. package/skills/blume-migrate/references/fumadocs.md +10 -2
  43. package/skills/blume-migrate/references/mintlify.md +3 -2
  44. package/skills/blume-migrate/references/nextra.md +2 -2
  45. package/skills/blume-migrate/references/starlight.md +1 -1
  46. package/src/ai/agent-readability.ts +2 -1
  47. package/src/ai/ask-data.ts +2 -1
  48. package/src/ai/component-markdown.ts +199 -36
  49. package/src/ai/llms.ts +93 -6
  50. package/src/ai/markdown.ts +2 -2
  51. package/src/ai/mcp/discovery.ts +10 -2
  52. package/src/ai/mcp/server.ts +74 -2
  53. package/src/astro/examples.ts +29 -2
  54. package/src/astro/generate.ts +282 -177
  55. package/src/astro/include-hmr.ts +81 -0
  56. package/src/astro/include-refresh.ts +0 -0
  57. package/src/astro/index.ts +10 -5
  58. package/src/astro/markdown-negotiation.ts +1 -1
  59. package/src/astro/runtime-modules.ts +196 -0
  60. package/src/astro/templates.ts +365 -113
  61. package/src/cli/commands/build.ts +91 -16
  62. package/src/cli/commands/dev.ts +6 -3
  63. package/src/cli/host-args.ts +18 -0
  64. package/src/cli/index.ts +2 -1
  65. package/src/cli/init/questions.ts +1 -0
  66. package/src/cli/init/scaffold.ts +27 -4
  67. package/src/components/colors.ts +142 -0
  68. package/src/components/content/Badge.astro +5 -12
  69. package/src/components/content/Callout.astro +19 -36
  70. package/src/components/content/Card.astro +15 -21
  71. package/src/components/content/Component.astro +10 -1
  72. package/src/components/content/GithubInfo.astro +28 -9
  73. package/src/components/content/Tabs.astro +27 -5
  74. package/src/components/content/github-info.ts +20 -5
  75. package/src/components/copy-feedback.ts +93 -9
  76. package/src/components/dropdown-dismiss.ts +122 -0
  77. package/src/components/islands/ask-ai.tsx +4 -1
  78. package/src/components/islands/hooks.ts +3 -1
  79. package/src/components/layout/Fonts.astro +15 -8
  80. package/src/components/layout/Header.astro +44 -0
  81. package/src/components/layout/LanguageSwitcher.astro +9 -1
  82. package/src/components/layout/NavSelector.astro +12 -3
  83. package/src/components/layout/NavTree.astro +6 -18
  84. package/src/components/layout/PageActions.astro +54 -22
  85. package/src/components/layout/PageLayout.astro +2 -0
  86. package/src/components/layout/ReferenceLayout.astro +6 -1
  87. package/src/components/layout/RootLayout.astro +42 -15
  88. package/src/components/layout/Search.astro +36 -4
  89. package/src/components/layout/TableOfContents.astro +8 -2
  90. package/src/components/layout/head-scripts.ts +30 -1
  91. package/src/components/openapi/ApiOverview.astro +13 -3
  92. package/src/components/openapi/AsyncApiOperation.astro +7 -14
  93. package/src/components/openapi/GraphqlChip.astro +33 -0
  94. package/src/components/openapi/GraphqlFieldsTable.astro +111 -0
  95. package/src/components/openapi/GraphqlOperation.astro +186 -0
  96. package/src/components/openapi/GraphqlType.astro +154 -0
  97. package/src/components/openapi/MethodBadge.astro +3 -14
  98. package/src/components/openapi/Operation.astro +12 -5
  99. package/src/components/openapi/OperationPanel.astro +43 -0
  100. package/src/components/openapi/RequestPanel.astro +5 -10
  101. package/src/components/openapi/Responses.astro +1 -16
  102. package/src/components/openapi/graphql-helpers.ts +466 -0
  103. package/src/components/openapi/playground-client.ts +15 -0
  104. package/src/components/openapi/sample-panels.ts +45 -0
  105. package/src/components/openapi/snippets.ts +13 -35
  106. package/src/core/base-path.ts +11 -0
  107. package/src/core/config-input.ts +209 -2
  108. package/src/core/config.ts +6 -4
  109. package/src/core/content-assets.ts +15 -4
  110. package/src/core/data.ts +28 -3
  111. package/src/core/define-components.ts +2 -0
  112. package/src/core/diagnostics.ts +8 -0
  113. package/src/core/frontmatter.ts +20 -8
  114. package/src/core/github.ts +71 -0
  115. package/src/core/graph.ts +22 -8
  116. package/src/core/heading-markers.ts +96 -0
  117. package/src/core/i18n-ui.ts +12 -0
  118. package/src/core/includes.ts +633 -0
  119. package/src/core/last-modified.ts +36 -11
  120. package/src/core/links.ts +79 -13
  121. package/src/core/manifest.ts +10 -0
  122. package/src/core/meta.ts +2 -1
  123. package/src/core/nav-diagnostics.ts +11 -2
  124. package/src/core/navigation.ts +27 -6
  125. package/src/core/project-graph.ts +61 -9
  126. package/src/core/schema.ts +235 -36
  127. package/src/core/server-features.ts +5 -9
  128. package/src/core/sources/github-releases.ts +2 -2
  129. package/src/core/sources/normalize.ts +502 -115
  130. package/src/core/sources/notion.ts +43 -8
  131. package/src/core/sources/obsidian.ts +1038 -0
  132. package/src/core/sources/read.ts +36 -1
  133. package/src/core/sources/resolve.ts +34 -1
  134. package/src/core/sources/types.ts +28 -6
  135. package/src/core/sources/watch.ts +12 -8
  136. package/src/core/tsconfig-aliases.ts +48 -35
  137. package/src/core/types.ts +31 -2
  138. package/src/core/ui-packs/ar.ts +2 -0
  139. package/src/core/ui-packs/bg.ts +3 -0
  140. package/src/core/ui-packs/bn.ts +2 -0
  141. package/src/core/ui-packs/ca.ts +3 -0
  142. package/src/core/ui-packs/cs.ts +2 -0
  143. package/src/core/ui-packs/da.ts +2 -0
  144. package/src/core/ui-packs/de.ts +3 -0
  145. package/src/core/ui-packs/el.ts +3 -0
  146. package/src/core/ui-packs/es.ts +3 -0
  147. package/src/core/ui-packs/fa.ts +2 -0
  148. package/src/core/ui-packs/fi.ts +2 -0
  149. package/src/core/ui-packs/fr.ts +3 -0
  150. package/src/core/ui-packs/he.ts +2 -0
  151. package/src/core/ui-packs/hi.ts +2 -0
  152. package/src/core/ui-packs/hr.ts +3 -0
  153. package/src/core/ui-packs/hu.ts +3 -0
  154. package/src/core/ui-packs/id.ts +3 -0
  155. package/src/core/ui-packs/it.ts +2 -0
  156. package/src/core/ui-packs/ja.ts +3 -0
  157. package/src/core/ui-packs/ko.ts +3 -0
  158. package/src/core/ui-packs/nl.ts +3 -0
  159. package/src/core/ui-packs/no.ts +3 -0
  160. package/src/core/ui-packs/pl.ts +3 -0
  161. package/src/core/ui-packs/pt-br.ts +3 -0
  162. package/src/core/ui-packs/pt.ts +3 -0
  163. package/src/core/ui-packs/ro.ts +3 -0
  164. package/src/core/ui-packs/ru.ts +3 -0
  165. package/src/core/ui-packs/sk.ts +2 -0
  166. package/src/core/ui-packs/sr.ts +2 -0
  167. package/src/core/ui-packs/sv.ts +3 -0
  168. package/src/core/ui-packs/th.ts +2 -0
  169. package/src/core/ui-packs/tr.ts +3 -0
  170. package/src/core/ui-packs/uk.ts +3 -0
  171. package/src/core/ui-packs/vi.ts +2 -0
  172. package/src/core/ui-packs/zh-tw.ts +2 -0
  173. package/src/core/ui-packs/zh.ts +2 -0
  174. package/src/core/version-cut.ts +26 -6
  175. package/src/core/yaml.ts +26 -0
  176. package/src/deploy/function-bundle.ts +251 -0
  177. package/src/deploy/vercel-negotiation.ts +49 -6
  178. package/src/eval/schema.ts +3 -1
  179. package/src/markdown/code-title.ts +22 -16
  180. package/src/markdown/features.ts +21 -0
  181. package/src/markdown/fence-meta.ts +50 -0
  182. package/src/markdown/heading-anchors.ts +198 -37
  183. package/src/markdown/include.ts +247 -0
  184. package/src/markdown/index.ts +43 -34
  185. package/src/markdown/language-icon.ts +2 -2
  186. package/src/markdown/mdast.ts +7 -3
  187. package/src/markdown/ts2js.ts +264 -0
  188. package/src/og/card.ts +1 -1
  189. package/src/openapi/asyncapi.ts +4 -1
  190. package/src/openapi/graphql-build.ts +293 -0
  191. package/src/openapi/graphql.ts +212 -0
  192. package/src/openapi/model.ts +38 -5
  193. package/src/openapi/parse.ts +34 -0
  194. package/src/openapi/proxy.ts +30 -5
  195. package/src/openapi/references.ts +97 -13
  196. package/src/openapi/render-mdx.ts +66 -12
  197. package/src/openapi/scalar.ts +5 -16
  198. package/src/openapi/source.ts +91 -23
  199. package/src/registry/eject.ts +47 -17
  200. package/src/search/documents.ts +229 -37
  201. package/src/search/orama-index.ts +9 -5
  202. package/src/seo/jsonld.ts +293 -51
  203. package/src/theme/code-block-padding.ts +16 -0
  204. package/src/theme/entry.ts +67 -13
  205. package/src/theme/fonts.ts +189 -16
  206. package/src/theme/sources.ts +49 -0
  207. package/src/translate/prompts.ts +2 -0
  208. package/src/translate/run.ts +7 -0
  209. package/src/translate/work-list.ts +0 -0
@@ -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
@@ -51,10 +51,13 @@ With versions configured, the header grows a version dropdown automatically. Swi
51
51
  Every archived page also shows a non-dismissible notice with a "Go to latest" link pointing at the page's live equivalent. Customize or disable it per version:
52
52
 
53
53
  ```ts blume.config.ts
54
- archived: [
55
- { id: "v1.0", banner: "These docs cover the 1.x SDK." },
56
- { id: "v0.9", banner: false },
57
- ];
54
+ versions: {
55
+ current: { label: "v2.0" },
56
+ archived: [
57
+ { id: "v1.0", banner: "These docs cover the 1.x SDK." },
58
+ { id: "v0.9", banner: false },
59
+ ],
60
+ }
58
61
  ```
59
62
 
60
63
  ## SEO
@@ -64,11 +67,14 @@ Old docs are search engines' favorite trap: the stale page outranks the live one
64
67
  Per version you can pick a different treatment:
65
68
 
66
69
  ```ts blume.config.ts
67
- archived: [
68
- { id: "v1.0" }, // canonical → latest (default)
69
- { id: "v0.9", canonical: "self" }, // every page authoritative
70
- { id: "v0.8", noindex: true }, // deindexed entirely
71
- ];
70
+ versions: {
71
+ current: { label: "v2.0" },
72
+ archived: [
73
+ { id: "v1.0" }, // canonical → latest (default)
74
+ { id: "v0.9", canonical: "self" }, // every page authoritative
75
+ { id: "v0.8", noindex: true }, // deindexed entirely
76
+ ],
77
+ }
72
78
  ```
73
79
 
74
80
  The sitemap follows suit: archived pages whose canonical points at a live equivalent are left out, `noindex` versions are left out wholesale, and version-only pages stay listed. A page's own `seo.canonical` frontmatter always wins.
@@ -122,7 +122,8 @@ Without a project `tsconfig.json`, only the generated runtime is checked.
122
122
  `blume validate` checks every link discovered in your content:
123
123
 
124
124
  - **Internal page links** (`/guides/intro`, `./sibling`) must resolve to a real page — broken ones are reported as errors.
125
- - **Anchor links** (`#section`, `/guides/intro#setup`) must match a heading on the target page — misses are warnings.
125
+ - **Anchor links** (`#section`, `/guides/intro#setup`) must match an anchor on the target page — a heading's id (generated or [pinned](/docs/content/syntax#custom-anchors)) or a raw HTML element's `id` attribute — with misses reported as warnings. Ids inside code blocks, inline code, HTML comments, and `<Prompt>` blocks don't count.
126
+
126
127
  - **Asset links** are checked where the file lives: an absolute path (`/logo.png`) against the `public/` directory, a relative image embed (`![](./diagram.png)`) against the page's own folder. A plain link to a relative path still resolves as a site route — only image embeds go through the image pipeline.
127
128
  - **External links** are only checked with `--external` (off by default since it requires the network); dead links (404/410/unreachable) are errors, while rate-limited or transient responses (403/429/5xx/timeout) are warnings.
128
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "1.5.3",
3
+ "version": "1.6.1",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -66,98 +66,107 @@
66
66
  "typecheck": "tsgo --noEmit && tsgo -p test/tsconfig.json --noEmit"
67
67
  },
68
68
  "dependencies": {
69
- "@astrojs/check": "^0.9.0",
70
- "@astrojs/markdown-satteri": "^0.3.2",
71
- "@astrojs/mdx": "^7.0.0",
72
- "@astrojs/node": "^11.0.0",
73
- "@astrojs/react": "^6.0.0",
74
- "@astrojs/vercel": "^11.0.3",
69
+ "@astrojs/check": "^0.9.10",
70
+ "@astrojs/markdown-satteri": "^0.4.0",
71
+ "@astrojs/mdx": "^8.0.0",
72
+ "@astrojs/node": "^11.1.5",
73
+ "@astrojs/react": "^6.0.5",
74
+ "@astrojs/vercel": "^11.0.10",
75
75
  "@asyncapi/converter": "^2.0.2",
76
76
  "@clack/prompts": "^1.7.0",
77
- "@iconify-json/lucide": "^1.2.115",
77
+ "@iconify-json/lucide": "^1.2.129",
78
78
  "@iconify/types": "^2.0.0",
79
- "@iconify/utils": "^3.1.3",
80
- "@modelcontextprotocol/sdk": "^1.29.0",
79
+ "@iconify/utils": "^3.1.5",
80
+ "@modelcontextprotocol/sdk": "^1.30.0",
81
81
  "@orama/orama": "^3.1.18",
82
- "@pierre/diffs": "^1.2.11",
83
- "@scalar/astro": "^0.4.5",
84
- "@scalar/openapi-parser": "^0.28.8",
85
- "@scalar/openapi-types": "^0.9.1",
86
- "@shikijs/transformers": "^4.2.0",
87
- "@shikijs/twoslash": "^4.2.0",
82
+ "@pierre/diffs": "^1.4.1",
83
+ "@scalar/astro": "^0.4.17",
84
+ "@scalar/openapi-parser": "^0.29.0",
85
+ "@scalar/openapi-types": "^0.9.5",
86
+ "@shikijs/transformers": "^4.4.3",
87
+ "@shikijs/twoslash": "^4.4.3",
88
88
  "@tailwindcss/typography": "^0.5.20",
89
- "@tailwindcss/vite": "^4",
89
+ "@tailwindcss/vite": "^4.3.3",
90
90
  "@types/mdast": "^4.0.4",
91
91
  "@vercel/analytics": "^2.0.1",
92
- "ai": "^7.0.42",
93
- "astro": "^7.1.0",
92
+ "ai": "^7.0.93",
93
+ "astro": "^7.3.1",
94
94
  "babel-plugin-react-compiler": "^1.0.0",
95
95
  "chokidar": "^5.0.0",
96
- "citty": "^0.1.6",
97
- "consola": "^3.4.0",
96
+ "citty": "^0.2.2",
97
+ "consola": "^3.4.2",
98
98
  "cross-spawn": "^7.0.6",
99
- "dompurify": "^3.4.13",
99
+ "dompurify": "^3.4.14",
100
100
  "dotenv": "^17.4.2",
101
101
  "epub-gen-memory": "^1.1.2",
102
- "fast-xml-parser": "^5.10.1",
103
- "get-tsconfig": "^4.14.1",
102
+ "fast-xml-parser": "^5.11.1",
104
103
  "github-slugger": "^2.0.0",
104
+ "graphql": "^17.0.2",
105
105
  "gray-matter": "^4.0.3",
106
106
  "html-escaper": "^3.0.3",
107
107
  "image-size": "^2.0.2",
108
- "jiti": "^2.4.0",
109
- "js-yaml": "^4.3.1",
110
- "katex": "^0.18.1",
108
+ "jiti": "^2.7.0",
109
+ "js-yaml": "^5.4.1",
110
+ "katex": "^0.18.6",
111
111
  "markdown-table": "^3.0.4",
112
- "marked": "^18.0.5",
112
+ "marked": "^18.0.11",
113
113
  "mdast-util-from-markdown": "^2.0.3",
114
114
  "mdast-util-gfm": "^3.1.0",
115
115
  "mdast-util-to-string": "^4.0.0",
116
116
  "medium-zoom": "^1.1.0",
117
- "mermaid": "^11.16.1",
117
+ "mermaid": "^11.17.2",
118
118
  "micromark-extension-gfm": "^3.0.0",
119
119
  "nanotar": "^0.3.0",
120
- "node-html-parser": "^9.0.0",
121
- "openapi-sampler": "^1.7.4",
122
- "p-limit": "^7.3.1",
123
- "p-map": "^7.0.6",
124
- "p-retry": "^8.0.0",
120
+ "node-html-parser": "^9.0.3",
121
+ "openapi-sampler": "^1.7.5",
122
+ "p-limit": "^7.3.2",
123
+ "p-map": "^7.0.7",
124
+ "p-retry": "^8.0.1",
125
125
  "package-manager-detector": "^1.8.0",
126
- "pagefind": "^1.3.0",
127
- "pathe": "^2.0.0",
126
+ "pagefind": "^1.5.2",
127
+ "pathe": "^2.0.3",
128
128
  "perfect-debounce": "^2.1.0",
129
- "picomatch": "^4.0.5",
130
- "react": "^19.0.0",
131
- "react-dom": "^19.0.0",
129
+ "picomatch": "^4.0.7",
130
+ "react": "^19.2.8",
131
+ "react-dom": "^19.2.8",
132
132
  "robots-parser": "^3.0.1",
133
- "satteri": "^0.9.5",
133
+ "satteri": "^0.10.5",
134
134
  "semver": "^7.8.5",
135
- "sharp": "^0.35.3",
136
- "shiki": "^4.2.0",
137
- "simple-icons": "^13.0.0",
138
- "string-width": "^8.1.0",
135
+ "sharp": "^0.35.4",
136
+ "shiki": "^4.4.3",
137
+ "simple-icons": "^16.29.0",
138
+ "string-width": "^8.2.2",
139
+ "sucrase": "^3.35.1",
139
140
  "tailwindcss": "^4.3.3",
140
- "takumi-js": "^2.2.1",
141
- "tinyglobby": "^0.2.10",
141
+ "takumi-js": "^2.13.6",
142
+ "tinyglobby": "^0.2.17",
142
143
  "twoslash": "^0.3.9",
143
144
  "typescript": "^6.0.3",
144
145
  "ufo": "^1.6.4",
145
- "undici": "^8.9.0",
146
+ "undici": "^8.10.2",
146
147
  "write-file-atomic": "^8.0.0",
147
- "zod": "^4.3.6"
148
+ "zod": "^4.5.4"
148
149
  },
149
150
  "devDependencies": {
151
+ "@ai-sdk/openai-compatible": "^3.0.44",
152
+ "@mixedbread/sdk": "^0.77.0",
153
+ "@notionhq/client": "^5.26.0",
154
+ "@openrouter/ai-sdk-provider": "^3.0.0",
155
+ "@oramacloud/client": "^2.1.4",
156
+ "@sanity/client": "^8.5.0",
150
157
  "@types/cross-spawn": "^6.0.6",
151
158
  "@types/html-escaper": "^3.0.4",
152
- "@types/js-yaml": "^4.0.9",
153
- "@types/node": "^22.10.0",
159
+ "@types/node": "^22.20.1",
154
160
  "@types/picomatch": "^4.0.3",
155
- "@types/react": "^19.0.0",
156
- "@types/react-dom": "^19.0.0",
161
+ "@types/react": "^19.2.18",
162
+ "@types/react-dom": "^19.2.7",
157
163
  "@types/semver": "^7.8.0",
158
164
  "@types/write-file-atomic": "^4.0.3",
159
- "@typescript/native-preview": "^7.0.0-dev.20260626.1",
160
- "bun-types": "^1.3.14"
165
+ "@typescript/native-preview": "^7.0.0-dev.20260707.2",
166
+ "algoliasearch": "^5.57.0",
167
+ "bun-types": "^1.4.2",
168
+ "flexsearch": "^0.8.212",
169
+ "typesense": "^3.0.6"
161
170
  },
162
171
  "peerDependencies": {
163
172
  "@ai-sdk/openai-compatible": "^3.0.0",
@@ -165,11 +174,11 @@
165
174
  "@astrojs/netlify": "^8.0.0",
166
175
  "@astrojs/svelte": "^9.0.0",
167
176
  "@astrojs/vue": "^7.0.0",
168
- "@mixedbread/sdk": "^0.76.0",
169
- "@notionhq/client": "^2.2.15",
177
+ "@mixedbread/sdk": "^0.77.0",
178
+ "@notionhq/client": "^5.0.0",
170
179
  "@openrouter/ai-sdk-provider": "^3.0.0",
171
180
  "@oramacloud/client": "^2.1.0",
172
- "@sanity/client": "^6.21.0 || ^7.0.0",
181
+ "@sanity/client": "^6.21.0 || ^7.0.0 || ^8.0.0",
173
182
  "algoliasearch": "^5.55.0",
174
183
  "flexsearch": "^0.8.0",
175
184
  "typesense": "^3.0.0"
@@ -28,10 +28,10 @@ Throughout this skill (including the `references/` files), **`<skill>` means the
28
28
  - `astro.config.*` calling `starlight({…})` → **Starlight** (`references/starlight.md`).
29
29
  - Anything else → apply this file's mental model directly; there's no framework-specific reference, so inventory by hand.
30
30
  - **Also note the host repo, independent of source framework:** a pnpm/Turbo workspace, a non-`docs/` content layout, or a Vercel deploy each need integration steps (`content.root` scoping, `minimumReleaseAge`, lockfile, `vercel.json`, an Astro/Vite patch) — all in `references/monorepo.md`. Read it whenever the target isn't a bare single-package docs folder.
31
- 2. **Inventory the repo** before changing anything: the config file(s), the content tree, the nav definition, snippets/partials/includes, static assets, OpenAPI/AsyncAPI specs, redirects, i18n locales, custom components, and icon usage. Note what's declared vs. defaulted.
31
+ 2. **Inventory the repo** before changing anything: the config file(s), the content tree, the nav definition, snippets/partials/includes, static assets, OpenAPI/AsyncAPI specs and GraphQL schemas, redirects, i18n locales, custom components, and icon usage. Note what's declared vs. defaulted.
32
32
  3. **Write `blume.config.ts`** with `defineConfig` from `blume`. Map only declared fields (see the reference's mapping table); rely on defaults everywhere else. A minimal result is `defineConfig({ title: "…" })`.
33
33
  4. **Restructure content.** Choose `content.root` (default `docs`) — **detect where `.md`/`.mdx` actually live, don't assume a `docs/` folder.** Many repos keep content directly under an app dir (`apps/docs/api/`, `.../getting-started/`) with no `docs/` subfolder; when so, set `content.root` to that dir and scope `content.include` to the real content folders rather than leaving a bare `content.root: "."` that scans everything (see `references/monorepo.md` §1). Order with numeric prefixes (`01-intro.mdx`), group without a URL segment via `(group)/` folders, and add a `meta.ts` (`defineMeta`) only where filesystem order isn't enough. **A source that already declares per-folder navigation in a sidecar file — Fumadocs `meta.json`, Nextra `_meta.*` — _is_ that case: convert each one to a `meta.ts`, carrying over its title/icon/order/collapse, rather than dropping it and hoping filenames reproduce the intent. Filesystem inference is the fallback for folders that declare no per-folder nav, never a reason to discard one that does.** Reach for an explicit `navigation.sidebar` only when the source nav genuinely can't be expressed by files. **Reshaping into folder-per-tab moves URLs** — track every old→new path as you go; you'll turn them into `redirects` in step 5.
34
- 5. **Rewrite pages.** Map frontmatter to Blume's strict schema; convert callout JSX to `:::` directives — **directives (and math/mermaid/package-install fences) are MDX-only, so rename any `.md` page that needs them to `.mdx`**; rename components; inline snippets/partials (Blume has no import-based includes); fix asset paths; **rewrite internal links** to their new routes (including OpenAPI operation links — see the OpenAPI section, their slugs differ from most sources); **add a `redirects` entry for every route you moved** in step 4; **convert every icon name to Lucide** (Blume is Lucide-only — no FontAwesome/Tabler). Remove any duplicated H1 in the body (`title` renders the H1; bodies start at `##`). **If the source has a hand-maintained changelog and the repo is open source on GitHub, offer to swap it for the `github-releases` source** (see "Changelogs" below) rather than porting the entries. For **Mintlify**, run the bundled codemod first — `node <skill>/scripts/mintlify-codemod.mjs --write <content-dir>` deterministically remaps icons and drops/renames unsupported frontmatter keys, and reports the rest (unknown icons, OpenAPI-stub flags) for you to finish by hand (see `references/mintlify.md`).
34
+ 5. **Rewrite pages.** Map frontmatter to Blume's strict schema; convert callout JSX to `:::` directives — **directives (and math/mermaid/package-install fences) are MDX-only, so rename any `.md` page that needs them to `.mdx`**; rename components; inline snippets/partials (Blume has no import-based includes); fix asset paths; **rewrite internal links** to their new routes (including OpenAPI/GraphQL operation links — see the API-reference sections, their slugs differ from most sources); **add a `redirects` entry for every route you moved** in step 4; **convert every icon name to Lucide** (Blume is Lucide-only — no FontAwesome/Tabler). Remove any duplicated H1 in the body (`title` renders the H1; bodies start at `##`). **If the source has a hand-maintained changelog and the repo is open source on GitHub, offer to swap it for the `github-releases` source** (see "Changelogs" below) rather than porting the entries. For **Mintlify**, run the bundled codemod first — `node <skill>/scripts/mintlify-codemod.mjs --write <content-dir>` deterministically remaps icons and drops/renames unsupported frontmatter keys, and reports the rest (unknown icons, OpenAPI-stub flags) for you to finish by hand (see `references/mintlify.md`).
35
35
  6. **Adopt `package.json`.** Repoint `dev`/`build`/`start` → `blume dev`/`blume build`/`blume preview`, remove the old framework's deps, add `blume`. A config-only source (e.g. a bare Mintlify `docs.json`) has no manifest — scaffold one. **In a pnpm workspace:** if `pnpm-workspace.yaml`/`.npmrc` sets `minimumReleaseAge`, add **only** `blume` to `minimumReleaseAgeExclude` (don't disable the guard) so the just-published version installs. **Always regenerate the lockfile in the same change:** after editing deps run a plain `pnpm install` (from the workspace root) and commit `pnpm-lock.yaml` alongside `package.json` — CI/Vercel use `--frozen-lockfile`, so a stale lockfile fails the build before it starts. **If the repo uses (or the user wants) [Ultracite](https://www.ultracite.ai) for formatting:** its oxfmt formatter mangles the `:::` directives you just wrote unless you ship the bundled `assets/oxfmt@0.55.0.patch` and register it under `patchedDependencies` — see `references/monorepo.md` §6. See `references/monorepo.md` §2–3.
36
36
  7. **Wire up the host repo & deploy (non-trivial repos).** For a monorepo on Vercel, emit the root-aware install/build recipe and `apps/docs/vercel.json`, and tell the user the two settings you can't commit (Vercel Root Directory, Node 22). If the workspace pins Vite and `blume build` crashes inside Astro/Vite, apply the pnpm-patch workaround. All copy-pasteable in `references/monorepo.md` §4–5.
37
37
  8. **Verify.** Run `blume build --strict` (frontmatter schema, duplicate routes, config — **without `--strict` a build exits 0 despite content errors**, silently dropping invalid pages) and `blume validate --strict` (internal links, heading anchors, assets — the link checker lives in `validate`, not `build`), fix diagnostics, then `blume dev` for a visual pass. End with a written summary of what was migrated, dropped, and approximated — **and every repo-specific edit you made** (pnpm-workspace, vercel.json, config globs) with the reason, plus any manual step left to the user (the Astro patch, Vercel dashboard settings).
@@ -53,6 +53,7 @@ The single biggest shift for most sources — especially Mintlify — is that **
53
53
 
54
54
  - **`navigation.tabs`** (`{ label, path, icon? }`) render top-of-header sections and **scope the sidebar by route** — the folder at a tab's `path` becomes the section, so this needs no config beyond the tabs themselves; structure content as **one folder per tab**. **A source's top-level tabs (Mintlify `navigation.tabs`, a top-level product/section switcher) map to these header tabs — keep them as tabs; don't flatten them into a single global `navigation.sidebar`.** Blume picks the active tab by **URL prefix** (longest tab `path` that prefixes the route), so every page in a tab must live under that tab's single `path`; a source tab that mixes arbitrary routes isn't portable as-is — either move its pages under one prefix (route change → add `redirects`) or accept the closest shape, and say which in the report (details in `references/mintlify.md`). The filtering runs both ways: on a route **under** a tab's `path`, the sidebar shows **only** that tab's folder (a tab also highlights when the current route is under it); on a **root or untabbed** route (or a tab whose `path` is `/`), the tab folders are **hidden** and the sidebar shows only the loose pages that belong to no tab (full tree as a fallback, so it's never blank). Consequence for migrations: once you add tabs, the landing sidebar automatically drops the sectioned content — that's intended, not lost pages; don't hand-build excludes for it.
55
55
  - **`navigation.selectors`** (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`, `kind` = `dropdown`/`product`/`version`/`language`) partition a whole site (products, versions) via a header dropdown keyed on the current route.
56
+ - **`navigation.actions`** (`[{ label, href }]`) puts plain links in the **header**, left of the icon buttons, and **`navigation.cta`** (`{ label, href }`, singular) is the header's one filled call-to-action button. This is the home for a source's header bar — a "Log in"/"Status" link goes in `actions`, a "Sign up"/"Get started" button in `cta`. An `http(s)` href opens in a new tab; an internal route is validated against your pages at build time, so a route served by another app on the same host must be written as an absolute URL. Both hide on phones (below `sm`), except `cta` on a page with no navigation toggle; a link that must survive on a phone on docs pages belongs in `featured`.
56
57
  - **`navigation.featured`** (`{ label, href, icon? }`) pins links to the **top of the sidebar, above every section** — a blog, changelog, or support page that should always be one click away. These are the **exception to tab scoping**: unlike the generated tree, featured links show on **every** route and breakpoint. `href` points anywhere — an external URL opens in a new tab, an internal route (`/contact`) is validated against your pages at build time. `icon` is a Lucide name (or image path/URL/inline SVG), as everywhere else. This is the home for a source's always-visible header/utility links (Mintlify anchors, Blog/Contact links) — see `references/mintlify.md`.
57
58
 
58
59
  ### Routes and pathing
@@ -65,12 +66,12 @@ The single biggest shift for most sources — especially Mintlify — is that **
65
66
 
66
67
  - **Site:** `title`, `description`, `logo` (string SVG, or `{ image: string | { light, dark, alt }, text, href }`), `banner` (`{ content, link, dismissible, id }` — no color/type). A logo renders beside `title` in the header, so a **wordmark logo doubles the brand** ("Acme Acme") — set `text: ""` to render the mark alone. **Prefer the string form over `{ light, dark }`:** if you have the logo SVG locally and it's monochrome (solid black or white), rewrite its `fill`/`stroke` to `currentColor` and use `logo: "/logo.svg"` — it then inherits the theme's text color and adapts to light/dark automatically, so you don't need separate light/dark files.
67
68
  - **`theme`:** `accent` (a color string for both modes, or `{ light, dark }` per mode), `action` (color), `mode` (`light`/`dark`/`system`), `radius`, `fonts` (`{ body, display, mono }` — each a curated Google-font slug, a `{ name, provider?, weights? }` object for any Google/Fontsource/Bunny/Fontshare family, or `{ name, variants: [{ src, weight?, style? }] }` for local font files), `background` and `backgroundImage` (each a string, or `{ light, dark }` per mode). The old `accentDark`/`backgroundDark`/`backgroundImageDark` fields were **merged into these per-mode objects** — a bare string still applies to both modes, so only reach for `{ light, dark }` when the two modes differ. There is **no** `theme.strict` and **no** `theme.css` config field — custom CSS goes in a project-root **`theme.css` file** (auto-picked-up), and a source's "strict appearance" flags drop.
68
- - **`content`:** `root` (default `"docs"`, relative to the project dir where `blume` runs), `include`/`exclude` (arrays of globs **relative to `content.root`**; defaults `["**/*.{md,mdx}"]` / `["**/_*", "**/.*"]`), `sources` (staged sources: `filesystem`, `github-releases`, `notion`, `sanity`, `mdx-remote`, `custom` — OpenAPI is **not** one of these; it's the top-level `openapi` field), `pages` (custom `.astro` dir), `defaultType`. When docs sit directly under the project dir (no `docs/` subfolder), set `root` there and **scope `include` to the real content folders** instead of scanning everything — `references/monorepo.md` §1.
69
+ - **`content`:** `root` (default `"docs"`, relative to the project dir where `blume` runs), `include`/`exclude` (arrays of globs **relative to `content.root`**; defaults `["**/*.{md,mdx}"]` / `["**/_*", "**/.*"]`), `sources` (staged sources: `filesystem`, `obsidian`, `github-releases`, `notion`, `sanity`, `mdx-remote`, `custom` — OpenAPI/GraphQL are **not** among these; they're the top-level `openapi`/`graphql` fields), `pages` (custom `.astro` dir), `defaultType`. When docs sit directly under the project dir (no `docs/` subfolder), set `root` there and **scope `include` to the real content folders** instead of scanning everything — `references/monorepo.md` §1.
69
70
  - **`basePath`** (top-level): a site-wide mount point (e.g. `"/docs"`) prepended to **every** route while staying invisible to the sidebar (no wrapper group). This is the right target for a source that served all docs under a prefix (Docusaurus `routeBasePath`, a Fumadocs `baseUrl` of `/docs`) — distinct from a per-source `prefix` (which adds a nav group) and from `deployment.base` (host subdirectory).
70
- - **`navigation`:** `tabs`, `selectors`, `featured` (links pinned above the sidebar on every route), `sidebar` (`{ display, items }` — `display` is the global render mode above; `items` is an explicit tree), `repo`. **Avoid an explicit `navigation.sidebar` unless you have to** — lean on the filesystem-derived sidebar. It only works when the file tree roughly matches the intended sidebar layout, so reshape folders to match first; reach for `sidebar.items` only for a shape files genuinely can't express (see "Config-declared nesting" above).
71
- - **`search`** (Orama default, Pagefind opt-in), **`ai`** (llms.txt, Ask AI, the MCP server), **`openapi`**, **`redirects`**, **`seo`**, **`markdown`**, **`analytics`**, **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`**.
71
+ - **`navigation`:** `tabs`, `selectors`, `actions` and `cta` (header links and the one filled button), `featured` (links pinned above the sidebar on every route), `sidebar` (`{ display, items }` — `display` is the global render mode above; `items` is an explicit tree), `repo` (`true`/`false`, or an absolute GitHub URL for the header mark when the docs repo is private and `github` must stay unset). **Avoid an explicit `navigation.sidebar` unless you have to** — lean on the filesystem-derived sidebar. It only works when the file tree roughly matches the intended sidebar layout, so reshape folders to match first; reach for `sidebar.items` only for a shape files genuinely can't express (see "Config-declared nesting" above).
72
+ - **`search`** (Orama default, Pagefind opt-in), **`ai`** (llms.txt, Ask AI, the MCP server), **`openapi`**, **`graphql`**, **`redirects`**, **`seo`**, **`markdown`**, **`analytics`**, **`deployment`**, **`i18n`**, **`toc`**, **`lastModified`**, **`github`** (`{ owner, repo, branch?, dir?, host?, api? }` — set `host` whenever the source's edit URL is on a GitHub Enterprise origin rather than `github.com`).
72
73
  - **Don't set `deployment.site`.** Blume auto-fills it: the dev server's `localhost` URL in dev, and the deployment URL (`VERCEL_PROJECT_PRODUCTION_URL`/`VERCEL_URL`) on Vercel. Hardcoding it in `blume.config.ts` overrides that auto-detection and pins the wrong absolute URL (canonical links, sitemap, OG, llms.txt) everywhere but the one host you typed — so leave it unset even when a source config had a `url`/`site` field. (Sitemap still generates in production because the deploy URL is present there.)
73
- - **Favicon is a filename convention, not config.** Drop `icon`/`favicon.{svg,png,ico}` (and `apple-icon.png`) in the project root or `public/` — Blume auto-detects it. There is **no** `favicon` config field. A source favicon given as `{ light, dark }` maps to a filename pair: copy the light file to a conventional name (e.g. `public/icon.png`) and the dark file to its `-dark` sibling — same directory and extension, `-dark` before the extension (`public/icon-dark.png`). If the two files have different formats, convert one so the extensions match; only an exact sibling of the resolved icon is picked up.
74
+ - **Favicon is a filename convention, not config.** Drop `icon.{svg,png,ico}` or `favicon.{svg,png,ico}` (and `apple-icon.png`) in the project root or `public/` — Blume auto-detects it. There is **no** `favicon` config field. A source favicon given as `{ light, dark }` maps to a filename pair: copy the light file to a conventional name (e.g. `public/icon.png`) and the dark file to its `-dark` sibling — same directory and extension, `-dark` before the extension (`public/icon-dark.png`). If the two files have different formats, convert one so the extensions match; only an exact sibling of the resolved icon is picked up.
74
75
 
75
76
  The schema is exported from `blume/schema`; the full field reference is in the `docs/configuration/` directory of the installed `blume` package (see "Full documentation" below for how to locate it).
76
77
 
@@ -125,6 +126,14 @@ Also valid: `date`/`authors` (blog/changelog feeds), `changelog` (changelog meta
125
126
  - **Operation routes have their own slug scheme** — `<route>/<slugified-tag>/<slugified-operationId>` (e.g. tag `Models`, id `listModels` → `/api-reference/models/listmodels`). This rarely matches the source's endpoint links (Mintlify/others kebab-case differently), so **rewrite every inbound link to an operation**. `blume validate` resolves operation pages like any other route, so it catches the ones you miss.
126
127
  - **Keep hand-written conceptual pages.** Sources often pair a written "Introduction/Authentication" page with the endpoint group in the same tab. A normal content page placed under the openapi `route` merges into the reference tab's sidebar — so keep those (auth, errors, rate limits) and delete only the per-endpoint stubs.
127
128
 
129
+ ### GraphQL
130
+
131
+ `graphql: { enabled: true, spec, endpoint? }` is the GraphQL counterpart: Blume lowers a schema — SDL text or an introspection JSON result, local path or URL — into **one real page per root field** (grouped as Queries/Mutations/Subscriptions) plus **one page per named type** (Objects, Input Objects, Enums, Interfaces, Unions, custom Scalars). Every OpenAPI rule above carries over: never hand-migrate a source's generated GraphQL reference pages (delete them and point `spec` at the schema), vendor a remote schema locally, keep hand-written conceptual pages under the `route` (default `/graphql`), add the `navigation.tabs` entry, and rewrite inbound links — routes are `<route>/<slugified-group>/<slugified-name>` (e.g. `/graphql/queries/pets`, `/graphql/objects/pet`), which `blume validate` resolves like any other page. Differences from the OpenAPI block:
132
+
133
+ - **Set `endpoint` to the live GraphQL URL.** A schema, unlike an OpenAPI document, names no server — `endpoint` is what the Try It playground and code samples target (without it they render a placeholder URL, and a `playground.proxy: true` block warns at build time because the proxy has no origin to allow). Multiple schemas use `sources: [{ spec, endpoint?, label?, route? }]`; a per-source `endpoint` overrides the block-level one.
134
+ - **No `renderer`/`scalar`/`theme` opt-outs** — the reference is always Blume-rendered (the Scalar embed reads OpenAPI documents only). A source's GraphQL playground/explorer embed (GraphiQL, Apollo Explorer) has no direct equivalent beyond the built-in Try It panel; report anything it did that the panel doesn't.
135
+ - **Only SDL and introspection JSON are accepted.** A source that builds its schema programmatically (a `GraphQLSchema` instance in code) must be printed to SDL (`printSchema` from `graphql`) and committed; report that conversion.
136
+
128
137
  ### Changelogs
129
138
 
130
139
  If the source ships a **hand-maintained changelog** (a `changelog.mdx`, a folder of dated entries, Mintlify `<Update>` blocks) **and the project is open source on GitHub**, offer to replace it with the **`github-releases`** content source — release notes become the changelog automatically, with no files to maintain. It's an offer, not an automatic rewrite: some teams keep a curated changelog that doesn't map 1:1 to GitHub releases, so confirm the release notes are the source of truth before deleting their pages.
@@ -158,7 +167,7 @@ A `redirects: [{ from, to, status? }]` array **in `blume.config.ts`** maps old U
158
167
 
159
168
  1. Run **`blume build --strict`** — it validates the frontmatter schema, duplicate routes, and config, and `--strict` makes diagnostics fail the build (without it, `blume build` **exits 0 despite content errors** and silently drops invalid pages). Then run **`blume validate --strict`** — links, heading anchors, and assets live here, not in `build` (add `--external` to also check outbound HTTP links). OpenAPI operation pages are real routes to `validate`, so dead links to them are caught too. Iterate until both are clean.
160
169
  2. Run `blume dev` and review the site visually — nav structure, tabs, theme, rendered components.
161
- 3. **Write a migration summary** covering: what was migrated (config, N pages, nav, OpenAPI), what was **dropped** (navbar CTAs, footers, custom theming, dynamic redirects, unmappable icons, unsupported components), and suggested follow-ups (`blume eject` for full control, `blume add` to vendor a component for customization).
170
+ 3. **Write a migration summary** covering: what was migrated (config, N pages, nav, API references), what was **dropped** (navbar CTAs, footers, custom theming, dynamic redirects, unmappable icons, unsupported components), and suggested follow-ups (`blume eject` for full control, `blume add` to vendor a component for customization).
162
171
 
163
172
  ## Full documentation
164
173