Skip to content
Blume is now publicly available.
Blume
Esc
navigateopen⌘Jpreview
On this page

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 build produces 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 eject turns the project into a standalone Astro app that still uses the blume package 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.ts and every meta.ts are 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.

  1. 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": {
  2. Register it with your package manager’s patchedDependencies. With Bun or pnpm, add to package.json:

    {
      "patchedDependencies": {
        "oxfmt@0.58.0": "patches/oxfmt@0.58.0.patch"
      }
    }
  3. Reinstall so the patch is applied:

    npm install
    pnpm install
    yarn install
    bun install

Was this page helpful?