FAQ
Common questions about Blume — how it compares to other documentation tools, and why a Markdown formatter might collapse your callout directives.
Answers to questions that come up often. Missing one? Open an issue or ask the in-page assistant.
How is Blume different from Mintlify, Fumadocs, and others?
Most documentation tools sit at one of two extremes. Managed platforms like Mintlify give you a polished result fast, but the build and hosting are their service — you author inside their system and deploy to their infrastructure. Component libraries and starters like Fumadocs, Nextra, or Docusaurus are open-source and flexible, but they hand you an application (a Next.js or React project) that you scaffold, wire up, and maintain before and after you write a word.
Blume takes a third path: the framework is the template. You point it at a folder of Markdown and it generates and drives the whole site — navigation, search, theming, Open Graph images, SEO, and AI endpoints — with no app to own. It’s fully open-source and self-hostable, so there’s no managed service and no vendor lock-in, but there’s also no boilerplate to maintain.
| Blume | Mintlify | Fumadocs / Nextra / Docusaurus | |
|---|---|---|---|
| Model | Zero-config framework; content only | Hosted platform | Library + app you scaffold |
| Source | Open-source (MIT) | Closed core | Open-source |
| Hosting | Anywhere — static or a server function | Their managed infrastructure | Anywhere; you build and deploy |
| You maintain | Your Markdown | Your Markdown + platform config | Your Markdown + the app around it |
| Rendering | Astro; core theme ships zero client JS | Their runtime | React/Next.js runtime |
| AI features | llms.txt, raw Markdown, Ask AI, MCP — built in, no hosted service |
Built in (hosted) | Bring your own |
A few consequences worth calling out:
- You own the output.
blume buildproduces a plain site you host on Vercel, Netlify, Cloudflare, S3, or your own box. Nothing phones home. - No lock-in, two ways out. Your content is portable Markdown, and
blume ejectturns the project into a standalone Astro app that still uses theblumepackage when you want full control. - Fast by default. The core theme is React-free and renders static HTML, so pages score well on Core Web Vitals without tuning. You opt into server features (Ask AI, MCP) only when you need them.
- Type-safe configuration.
blume.config.tsand everymeta.tsare real TypeScript validated by a schema — not loosely-typed YAML.
See Why Blume exists for the longer version.
Is Blume free and open-source?
Yes — Blume is MIT-licensed and free. You install the blume package, keep your content in your own repository, and host the build wherever you like. There’s no paid tier, no per-seat pricing, and no account to sign up for. The source lives on GitHub.
Do I need to know Astro, React, or Tailwind?
No. A folder of Markdown is a complete site — navigation, search, and theming are inferred or set with a handful of tokens. You only reach for the underlying stack when you want to customize: interactive islands (React), component overrides, or theme tokens (Tailwind). Even then, blume.config.ts is typed, so your editor guides you.
Can I use React components and MDX?
Yes. Any page can be .md or .mdx, and MDX lets you drop in the built-in components with no imports. You can also add your own .tsx/.jsx islands — Blume auto-enables React only for the pages that use them, so the core theme stays JavaScript-free everywhere else.
Where can I deploy it?
Anywhere. blume build outputs static HTML by default, which you can serve from any static host or CDN — Vercel, Netlify, Cloudflare Pages, GitHub Pages, S3, or your own server. Server-only features (Ask AI, the MCP server, on-demand rendering) switch the build to a server function through an adapter for Vercel, Node, Netlify, or Cloudflare. See Deployment.
Does search need a hosted service?
No. Orama builds a local index that works in both dev and production with nothing to host or pay for. For very large sites, Pagefind is one flag away. Either way the index ships as part of your site.
How do I customize the look?
Start with theme tokens — accent color, fonts, radius, and a theme.css for anything else Tailwind can express. Go further by overriding built-in components or adding custom pages. When you want the Astro project itself, blume eject hands you a standalone app that still uses the blume package.
Why is oxfmt / Ultracite collapsing my directives?
If you format your Markdown with Ultracite (which runs oxlint + oxfmt) — as Blume itself does — you may notice that container directives get flattened onto a single line after a format pass:
:::note
Regenerate the project with blume dev.
:::
becomes
:::note Regenerate the project with blume dev. :::
Once the opening :::note fence is joined to the prose, it’s no longer a directive, so it renders as literal text instead of a callout.
Why it happens
This is a bug in oxfmt’s Markdown formatter (inherited from Prettier’s Markdown printer — see prettier/prettier#19040). When it wraps prose, it treats the ::: fence lines as ordinary text and joins them with the adjacent line, breaking the directive. It affects every container directive type — :::note, :::tip, :::info, :::warning, :::danger, :::success.
We reported it upstream in oxc-project/oxc#24096; until it’s fixed there, the patch below is the workaround.
The fix
Patch oxfmt so it preserves the line break that sits directly against a ::: fence. Blume ships the same fix in its own repo, and you can apply it in any project.
-
Save the patch as
patches/oxfmt@0.58.0.patch:diff --git a/dist/markdown-BjyDFyaO.js b/dist/markdown-BjyDFyaO.js index 1905aa7563e612808426e26f1ed28bebbef3456f..42bb9f66829068676c5697fc9fc39279bf937f3e 100644 --- a/dist/markdown-BjyDFyaO.js +++ b/dist/markdown-BjyDFyaO.js @@ -4830,7 +4830,43 @@ function lu(e, t, r) { case "sentence": return Mh(e, r); case "word": return t.parser !== "mdx" ? Vh(e, t) : Gh(e); case "whitespace": { - let { next: i } = e, u = i && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(i.value) && !ME(e) && !(t.proseWrap === "preserve" && zE(e)) ? "never" : t.proseWrap; + let { next: i, previous: oxfmtFencePrev } = e; + // Preserve line breaks that sit directly against a `:::` container + // directive fence, so `proseWrap: "never"` keeps the opening/closing + // fence on their own lines instead of joining them into the prose (which + // breaks the directive). Ordinary prose still wraps per proseWrap. + // See prettier/prettier#19040. + let oxfmtIsFence = (w) => w != null && typeof w.value === "string" && w.value.startsWith(":::"); + // A titled directive (`:::warning[Heads up]`) parses its `[title]` as a + // linkReference between two sentence nodes at the paragraph level: the + // fence word ends the sentence before the reference, and the body's + // leading newline opens the sentence after it. So when this whitespace + // starts its sentence, climb to the paragraph and check whether the two + // preceding siblings are a (link) reference and a sentence ending in a + // `:::` fence word. + let oxfmtPrevIsTitledFence = !1; + if (oxfmtFencePrev == null && e.index === 0 && e.grandparent != null && Array.isArray(e.grandparent.children)) { + let oxfmtSibs = e.grandparent.children, oxfmtSentIdx = oxfmtSibs.indexOf(e.parent); + if (oxfmtSentIdx >= 2) { + let oxfmtLink = oxfmtSibs[oxfmtSentIdx - 1], oxfmtBefore = oxfmtSibs[oxfmtSentIdx - 2]; + let oxfmtLastWord = oxfmtBefore && oxfmtBefore.type === "sentence" && Array.isArray(oxfmtBefore.children) ? oxfmtBefore.children[oxfmtBefore.children.length - 1] : null; + oxfmtPrevIsTitledFence = oxfmtLink != null && (oxfmtLink.type === "linkReference" || oxfmtLink.type === "link") && oxfmtIsFence(oxfmtLastWord); + } + } + // The plain-markdown parser keeps a titled fence's `[title]` as literal + // words, so the whole directive is one sentence. For a newline + // whitespace, walk back to the start of its visual line within the + // sentence; a line led by a `:::` word is a fence whose break must stay. + if (!oxfmtPrevIsTitledFence && e.node.value.includes("\n") && e.parent != null && Array.isArray(e.parent.children)) { + let oxfmtLineFirst = null; + for (let oxfmtJ = e.index - 1; oxfmtJ >= 0; oxfmtJ--) { + let oxfmtSib = e.parent.children[oxfmtJ]; + if (oxfmtSib.type === "whitespace" && typeof oxfmtSib.value === "string" && oxfmtSib.value.includes("\n")) break; + oxfmtLineFirst = oxfmtSib; + } + oxfmtPrevIsTitledFence = oxfmtIsFence(oxfmtLineFirst); + } + let u = oxfmtIsFence(oxfmtFencePrev) || oxfmtPrevIsTitledFence || oxfmtIsFence(i) ? "preserve" : i && /^>|^(?:[*+-]|#{1,6}|\d+[).])$/.test(i.value) && !ME(e) && !(t.proseWrap === "preserve" && zE(e)) ? "never" : t.proseWrap; return ou(e, n.value, u, !1, t); } case "emphasis": { -
Register it with your package manager’s
patchedDependencies. With Bun or pnpm, add topackage.json:{ "patchedDependencies": { "oxfmt@0.58.0": "patches/oxfmt@0.58.0.patch" } } -
Reinstall so the patch is applied:
npm installpnpm installyarn installbun install