- 16 cross-shell utilities converted to sh/bash chezmoi executables: ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj (ghg-clone not named ghg: unrelated cargo binary owns that name) - cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files - fish keeps only shell-state functions; converted ones now hit PATH - cp-appid ignore gating moved to .local/bin path - dot_agents/skills: 82 skills materialized from dotstate storage so dot_claude skill symlinks resolve on fresh bootstrap; dotstate profile/symlinks.json released .agents/skills .bashrc .zshrc
10 KiB
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.jsonfiles. fumadocs-ui/fumadocs-core/fumadocs-mdxdeps and asource.config.ts.- A
loader({ baseUrl: "/docs" })call inlib/source.ts(orapp/source.ts,src/lib/source.ts,source.ts).
Config
Fumadocs declares almost nothing Blume needs — only two things map:
- Title — read
package.jsonname, 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
baseUrlin the source loader (required in Fumadocs, so it's always declared). If it's a real prefix (e.g./docs), set top-levelbasePath: "/docs"— it prefixes every route while staying invisible to the sidebar, which matches Fumadocs' behavior. (Acontent.sourcesprefixalso works but adds a wrapping nav group — only use it when you want the group.) IfbaseUrlis"/", 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.mdxout of the box (there is nomarkdown.mathfield). Inline$…$is not supported — convert inline math to$$…$$or drop it (report).- A Twoslash transformer → Blume supports the
twoslashfence 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!pageresurfaces in the sidebar. Set that page's frontmattersidebar.hidden: trueinstead."---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'smeta.tstitleinstead."...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)andexternal:variants) → a top-level/utility link belongs innavigation.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 explicitnavigation.sidebar).pagesIndex→ make that page the folder'sindex(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.
Components
- Callouts:
<Callout type="x">→:::directive. Fumadocs types areinfo(the default),warn,warning,error,success,idea:warn/warning→:::warning,error→:::danger,success→:::success,idea→:::tip,infoand bare<Callout>→:::info.title→:::type[Title]; dropicon. - Cards:
<Cards>→<CardGroup>.<Card>needs prop surgery, not pass-through: Fumadocsiconis a JSX element (icon={<Cpu />}imported fromlucide-react) → Blume takes a string name (icon="cpu"); Fumadocsdescription="…"→ Blume has nodescriptionprop — move the text into the card body. - Accordions (container/item inversion):
<Accordions>→<Accordion>(container);<Accordion>→<AccordionItem>(item). - File trees:
<Files>→<Tree>(the JSX container forTree.*children — not<FileTree>, which wraps a Markdown list);<Folder>→<Tree.Folder>(defaultOpencarries over);<File>→<Tree.File>. Or convert the whole block to a list-driven<FileTree>. - Tabs: Fumadocs declares labels on the parent (
<Tabs items={['npm','pnpm']}>) and selects with<Tab value="npm">. Blume's<Tab>carries its owntitle. Stripitems={[…]}from<Tabs>and give each child<Tab>atitle(from itsvalue, or the positionalitemsentry). - Pass through unchanged:
<Steps>/<Step>,<TypeTable>(Blume's was modeled on Fumadocs' — identicaltyperecord shape),<GithubInfo>. <Banner>(layout-mounted, not per-page) → thebannerconfig field ({ content, link, dismissible, id }).<include>./partial.mdx</include>— Blume has no runtime include. Inline the partial's body (strip its frontmatter) at migration time; resolve nested includes recursively.- No equivalent — report:
<DynamicCodeBlock>,<ImageZoom>(Blume zooms content images by default),<InlineTOC>. - Strip or convert every import — not just
fumadocs-*:lucide-reactimports (icon JSX → string names),next/image/next/link(→ Markdown image/link), and local components. Inventorymdx-components.tsxbefore deleting it — components registered there are used import-free in MDX bodies; port or inline each usage first.
Code fences
```npmfences (Fumadocs' remark-npm accepts both) →```package-install.- Adjacent fences sharing
tab="…"meta render as grouped tabs in Fumadocs → wrap them in a Blume<CodeGroup>and drop thetab=attrs.
OpenAPI
fumadocs-openapi writes generated MDX stubs into the content tree (generateFiles() output: pages containing <APIPage>/<OpenAPIPage> 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; <DynamicCodeBlock>/<InlineTOC>; custom Shiki transformers; any icon with no Lucide equivalent after casing conversion.