# Fumadocs → Blume Fumadocs is code-first: navigation comes from the folder tree + per-folder `meta.json`. This maps cleanly to Blume — most content passes through, and `meta.json` becomes `meta.ts`. Fumadocs v16 runs on Next.js, React Router, TanStack Start, Astro, or Waku — detect by the Fumadocs files, not by Next.js files, and adjust the teardown list to whichever host framework is present. ## Detect - Content under **`content/docs/`** (the Fumadocs convention). - Per-folder **`meta.json`** files. - `fumadocs-ui` / `fumadocs-core` / `fumadocs-mdx` deps and a `source.config.ts`. - A `loader({ baseUrl: "/docs" })` call in `lib/source.ts` (or `app/source.ts`, `src/lib/source.ts`, `source.ts`). ## Config Fumadocs declares almost nothing Blume needs — only two things map: - **Title** — read `package.json` `name`, prettified (drop scope, split on `-_`, Title-Case). For a generic monorepo name (`web`, `app`, `docs`…), use the repo-root directory name instead. - **Route prefix** — the `baseUrl` in the source loader (required in Fumadocs, so it's always declared). If it's a real prefix (e.g. `/docs`), set top-level **`basePath: "/docs"`** — it prefixes every route while staying invisible to the sidebar, which matches Fumadocs' behavior. (A `content.sources` `prefix` also works but adds a wrapping nav group — only use it when you _want_ the group.) If `baseUrl` is `"/"`, serve from the site root (nothing to set). Everything else is `defineConfig({ title })`. **Also read `source.config.ts` `mdxOptions`** — remark/rehype plugins live there: - `remark-math`/`rehype-katex` → nothing to configure: block math `$$…$$` renders in `.mdx` out of the box (there is **no** `markdown.math` field). Inline `$…$` is **not** supported — convert inline math to `$$…$$` or drop it (report). - A Twoslash transformer → Blume supports the `twoslash` fence meta natively; drop the plugin. - Custom Shiki transformers/themes or other plugins → report. ## Navigation: `meta.json` → `meta.ts` **Every `meta.json` becomes a `meta.ts` — this is the primary navigation carry-over for Fumadocs, always required, never optional.** `meta.json` is Fumadocs' canonical nav source; no filesystem-only shortcut reproduces its ordering, icons, and collapse state, so don't skip it in favor of filename inference. Move content from `content/docs/` to your chosen `content.root` (e.g. `docs/`), then convert each `meta.json` to a `meta.ts` (`defineMeta`): | Fumadocs `meta.json` | Blume `meta.ts` | | --- | --- | | `title` | `title` | | `icon` | `icon` — **convert the casing** (see Icons below) | | `defaultOpen: false` | `collapsed: true` | | `defaultOpen: true` | `collapsed: false` | | `root: true` | a **`navigation.tabs` entry** (see below) — **not** a `meta.ts` field | | `pages: [...]` slugs | `pages: [...]` (ordering) | | `description` | **drop** (folders have no description) | | `collapsible: false` | **drop** (report — no per-folder equivalent) | `meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages` — there is **no `display` field** (writing one is a build error). Collapse rendering is global: when any folder needs collapsing, set `navigation.sidebar.display: "group"` once in `blume.config.ts`. **`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`. Handle the `pages` array items: - **`"..."`** (rest marker) / `""` → drop; Blume appends unlisted pages automatically. **`"z...a"`** (reversed rest) → no equivalent; list the pages explicitly in the intended order. - **`"!page"`** (exclusion) → Blume _appends_ unlisted pages, so a dropped `!page` **resurfaces in the sidebar**. Set that page's frontmatter `sidebar.hidden: true` instead. - **`"---Section---"`** (separator; also the `---[Icon]Label---` variant) → Blume has no flat separator. Turn each section into a **`(Section)/` group folder** (route-transparent — the `(…)` segment is stripped from URLs), and move the section's pages into it. If a section wraps a single existing folder, leave it in place and set that folder's `meta.ts` `title` instead. - **`"...folder"`** (extract) → Blume can't flatten a folder inline; keep it as a normal group at that ordering position and report it. - **`"[Text](url)"`** (link; also `[Icon][Text](url)` and `external:` variants) → a top-level/utility link belongs in **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route). A link buried deep in one folder has no folder-meta home — drop and report (or model that one folder via an explicit `navigation.sidebar`). - **`pagesIndex`** → make that page the folder's `index` (rename the file); report the route change and add a redirect. ## Frontmatter Fumadocs' core frontmatter (`title`, `description`, `icon`) matches Blume — but **convert `icon` casing** (see Icons). Drops: **`full`** (Fumadocs' full-width/no-TOC layout — no equivalent; report) and **`_openapi`** (generated API stubs — delete the whole page, see OpenAPI). Any other non-schema key is a build error, so drop and report. ## Icons: lucide-react names → kebab-case Fumadocs icons are strings resolved by the repo's own `icon` handler in `loader()` — in practice **PascalCase lucide-react export names** (`"BookOpen"`, `"HomeIcon"`). Blume resolves **kebab-case** Lucide names and silently renders nothing on a miss, so pass-through loses every icon. Read the loader's icon handler to confirm the scheme, then convert each name: strip any `Icon` suffix, kebab-case it (`BookOpen` → `book-open`, `HomeIcon` → `home`), and verify it exists at [lucide.dev/icons](https://lucide.dev/icons). ## Components - **Callouts:** `` → `:::` directive. Fumadocs types are `info` (the default), `warn`, `warning`, `error`, `success`, `idea`: `warn`/`warning`→`:::warning`, `error`→`:::danger`, `success`→`:::success`, `idea`→`:::tip`, `info` and **bare ``** → `:::info`. `title` → `:::type[Title]`; drop `icon`. - **Cards:** `` → ``. `` needs **prop surgery**, not pass-through: Fumadocs `icon` is a JSX element (`icon={}` imported from `lucide-react`) → Blume takes a string name (`icon="cpu"`); Fumadocs `description="…"` → Blume has no `description` prop — move the text into the card body. - **Accordions (container/item inversion):** `` → `` (container); `` → `` (item). - **File trees:** `` → **``** (the JSX container for `Tree.*` children — _not_ ``, which wraps a Markdown list); `` → `` (`defaultOpen` carries over); `` → ``. Or convert the whole block to a list-driven ``. - **Tabs:** Fumadocs declares labels on the parent (``) and selects with ``. Blume's `` carries its own `title`. Strip `items={[…]}` from `` and give each child `` a `title` (from its `value`, or the positional `items` entry). - **Pass through unchanged:** ``/``, `` (Blume's was modeled on Fumadocs' — identical `type` record shape), ``. - **``** (layout-mounted, not per-page) → the `banner` config field (`{ content, link, dismissible, id }`). - **`./partial.mdx`** — Blume has no runtime include. **Inline** the partial's body (strip its frontmatter) at migration time; resolve nested includes recursively. - **No equivalent — report:** ``, `` (Blume zooms content images by default), ``. - **Strip or convert every import** — not just `fumadocs-*`: `lucide-react` imports (icon JSX → string names), `next/image`/`next/link` (→ Markdown image/link), and local components. **Inventory `mdx-components.tsx` before deleting it** — components registered there are used import-free in MDX bodies; port or inline each usage first. ## Code fences - ` ```npm ` fences (Fumadocs' remark-npm accepts both) → ` ```package-install `. - Adjacent fences sharing `tab="…"` meta render as grouped tabs in Fumadocs → wrap them in a Blume `` and drop the `tab=` attrs. ## OpenAPI `fumadocs-openapi` writes **generated MDX stubs into the content tree** (`generateFiles()` output: pages containing ``/`` with `_openapi` frontmatter), plus `lib/openapi.ts` (`createOpenAPI`) and a generate script. Treat these exactly like Mintlify endpoint stubs: **delete the generated pages**, point `openapi: { enabled: true, sources: [{ spec }] }` at the spec (vendor it locally), add a `navigation.tabs` entry for the reference route, and remove `fumadocs-openapi`, `lib/openapi.ts`, and the generate script. Keep hand-written conceptual pages (intro/auth) under the reference route. ## i18n A `loader({ i18n })` setup (locale-suffixed files or locale dirs) → Blume `i18n: { defaultLocale, locales: [{ code, label }] }`. Locale **directories** match Blume's `dir` parser as-is; locale **file suffixes** (`page.cn.mdx`) need restructuring into locale folders. Report whichever transform you apply. ## Package.json & teardown Repoint scripts (`dev`→`blume dev`, `build`→`blume build`, `start`→`blume preview`), remove the `fumadocs-*` deps and the host framework's deps (`next`, `react-router`, `@tanstack/*`…), add `blume`. Safe to delete after harvesting (see above for what to read first): `source.config.*`, `mdx-components.tsx`, the app/route dir, and host-framework config (`next.config.*`, `next-env.d.ts`, the `next` tsconfig plugin — or the React Router/TanStack equivalents). ## Dropped — report these Folder `description` and `collapsible: false`; reversed rest (`z...a`) ordering; the extract (`...folder`) flatten semantics; deep-folder sidebar links; frontmatter `full`; ``/``; custom Shiki transformers; any icon with no Lucide equivalent after casing conversion.