Fix Tabs reverting to the hash target after a tab click
The hash-to-tab logic of Tabs ran on every render instead of only on mount and hashchange, because the effect depended on a useEffectEvent callback, which is not referentially stable.
rehype-code awaits transformer._fd_prepare(code, options) on every transformer before highlighting a code block. Transformers can run async work (or coalesce work across the code blocks being highlighted concurrently) and serve it from the synchronous Shiki hooks afterwards.
Template transforms (routes, prerender config, RootProvider search dialog, AI chat layout) now parse files with oxc-parser and edit the source text in place, instead of ts-morph and the TypeScript 6 compiler it bundles. Edits preserve the original formatting, including trailing commas.
The CLI now configures Fumadocs on an existing app and adds features to it, beyond installing UI components. Supported on Next.js, React Router, TanStack Start and Waku.
init adds the docs pages in a dedicated route group (e.g. app/(docs)) with their own provider, layout and search route, so your existing setup stays untouched: it only registers the Fumadocs MDX plugin in your bundler config, imports the styles into your global CSS and adds suppressHydrationWarning to <html>. Pass --i18n to set up internationalization with locale-prefixed routes.
feature <id> installs the components and routes of a feature and wires them into your app: ai (Ask AI dialog), llms (llms.txt, llms-full.txt and per-page Markdown, with the /docs/*.md rewrite on Next.js), mcp, webmcp (experimental), og, search (Orama Cloud, Algolia, Typesense, Mixedbread), feedback, epub and lint. Features follow your setup: the baseUrl of your docs, src/ directory, i18n, static export and SPA modes, Fumadocs MDX with the macro API or source.config.ts. Route URLs are read from the constants of lib/shared.ts (docsRoute, docsContentRoute, docsImageRoute), and route features add URL helpers like getPageMarkdownUrl(page) to lib/source.ts.
Every command takes -y to skip prompts and --no-install to only write dependencies to package.json.
Components are installed at the directories of your shadcn components.json by default, so the same component isn't duplicated in two places. export epub no longer scaffolds the export route, run feature epub instead.
create-fumadocs-app -y (--yes) skips every prompt and uses the default for options you didn't pass, so scripts and AI coding agents can scaffold a project without a TTY:
Previously this required setting CI=1. In --yes mode, a non-empty target directory fails instead of prompting for deletion.
Run Twoslash on TypeScript 7
fumadocs-twoslash now runs on the native TypeScript 7 compiler (typescript/unstable/sync) instead of bundling its own TypeScript 6, so the types in popups come from the same compiler as your project.
Code blocks of documents compiled concurrently are analyzed together in one snapshot of the TypeScript project (requires fumadocs-core 16.15.7 for the _fd_prepare hook of rehype-code), which removes most of the per-block cost of loading the project. Cold builds of the Fumadocs docs take 40% less time in total, with the TypeScript part 3.5x faster.
The Shiki transformer and renderer are now implemented in fumadocs-twoslash, @shikijs/twoslash and twoslash are no longer dependencies (nor twoslash a peer of TypeScript 6). The rendered HTML and the transformer options are unchanged, except that rendererRich only takes the options that apply to the popups of Fumadocs (jsdoc, processHoverInfo, processHoverDocs, completionIcons, customTagIcons); the hast extensions, errorRendering, queryRendering, classExtra and the custom renderer option are removed.
The twoslashOptions are simplified to what the native API supports:
compilerOptions takes tsconfig.json values (e.g. moduleResolution: 'bundler') instead of enum values from the typescript package.
cwd replaces vfsRoot.
tsModule, tsLibDirectory, fsMap, cache, customTransformers and the per-call positionQueries, positionCompletions, positionHighlights are removed.
@showEmit is not supported, TypeScript 7 has no emit API yet.
Generate type tables with the native TypeScript compiler
fumadocs-typescript no longer uses ts-morph. It drives the TypeScript 7 native compiler (tsgo) through the typescript/unstable/sync API, bundled as a dependency, so it works regardless of the TypeScript version of your project, including TypeScript 7 before its programmatic API is stable.
Only the documented files (and what they import) are loaded into the compiler, instead of the entire tsconfig.json project. On the Fumadocs docs site this makes cold generation about 5x faster (first table 500 ms → 50 ms, later tables 20 ms → 5 ms) with roughly half the memory.
Behavior changes
transform and typeSimplifier hooks receive TypeScript 7 API objects (Type, Symbol, Checker, Node) instead of ts-morph wrappers. this.program is the TypeScript 7 Project, and this.checker is available.
The project option of createGenerator() takes the Project returned by createProject().
Members of mapped types (e.g. Pick) and unions may be printed in a different order, following the native compiler.
Replace cnfast with cn
Internal refactor only.
Bundle TypeScript for Twoslash
fumadocs-twoslash now depends on its own TypeScript 6 and passes it to Twoslash, so the transformer keeps working in projects on TypeScript 7 (whose package no longer provides the compiler API Twoslash requires). Previously, every Twoslash code block that was not already in the types cache failed with Cannot read properties of undefined (reading 'readFile').
Replace cnfast with cn
Internal refactor only.
Generate controls with the native TypeScript compiler
@fumadocs/story no longer depends on ts-morph. Story controls are generated with the TypeScript 7 native compiler (tsgo), bundled as a dependency, independent of the TypeScript version of your project.
Transforming a story file is about 5x faster (first file 300 ms → 30 ms, edits 20 ms → 5 ms) and uses a fraction of the memory, which mostly matters in dev where every edit regenerates controls.
Behavior changes
Custom handlers of @fumadocs/story/type-tree receive TypeScript 7 API objects (Type, Node, and a Project exposing project.checker) instead of ts-morph wrappers.
Properties of mapped types (e.g. Pick<Props, ...>) and members of string literal unions may appear in a different order in the generated controls, following the native compiler.
Support installing the API playground via Fumadocs CLI
npx @fumadocs/cli add fumadocs/openapi/playground
Use it with the playground.provider and playground.render options, see Customise UI.
Pass full props to schemaUI.render
It now receives the same props as the built-in Schema UI (including renderMarkdown and renderCodeblock), so a customised Schema UI can act as a drop-in replacement:
OpenAPI schema types from fumadocs-openapi (e.g. OperationObject, HttpMethods).
useRenderContext, useServerContext and useOperationContext from fumadocs-openapi/ui.
@fumadocs/base-ui@16.15.5
fumadocs-core@16.15.5
fumadocs-ui@16.15.5
Root types: version your docs with root: "<type>"
root in meta.json now accepts a string, the type of root folder. Root folders of the same type under the same parent are interchangeable, which is how you keep multiple versions of the same docs in one site:
{ "title": "1.0.0", "root": "version"}
{ "title": "2.0.0", "root": "version"}
The sidebar only shows the opened version, and docs layouts render a dropdown to switch between them. Switching keeps your place: it navigates to the same page in the other version (/docs/v1/guide to /docs/v2/guide), or its index page when the page doesn't exist there.
root: true is simply the default type, displayed as tabs. See Versioning for the guide and Root Type for the reference.
Tabs are grouped by root folder
Layout tabs are now grouped by the root folders on the current page's path, with one dropdown per group. This changes a few behaviours of the existing root: true tabs:
Clicking a tab navigates to the same page in the target folder when it exists, otherwise its index page as before.
With nested root folders, each level gets its own dropdown instead of one flat list. Tab lists (tabMode: 'top' on Docs layout, tabMode: 'navbar' on Notebook layout) show the innermost root: true group, and are hidden on pages outside of any root folder.
getLayoutTabs() includes typed root folders too, so a custom transform also decorates them. Custom tabs entries bound to a page tree folder are grouped the same way, other entries are appended to the root: true dropdown.
tabs={false} disables the dropdowns of typed root folders as well.
findProjection() in fumadocs-core/page-tree
Find the structural projection of a page in another root folder, the page at the same file path relative to the root folder:
import { findProjection } from 'fumadocs-core/page-tree';findProjection(v1, v2, page)?.url;
Add API Playground & Schema UI components
Install the UI of API integrations into your codebase:
intersection treated any difference in the type keyword as an impossible schema and returned false, so an allOf member using a type array (e.g. ['string', 'null'], emitted for nullable: true) merged with a typed member (type: 'string') produced false instead of type: 'string' — dropping every property of the schema in the Schema UI and the playground.
Intersecting overlapping type sets now keeps the shared members, and only genuinely disjoint types (e.g. ['string'] with ['number']) resolve to false.
Use the explicit title of composed schemas in Schema UI
mergeAllOf now keeps an explicit title as the display alias of the composed schema (ChildA | ChildB), skips titles already covered by the other side of an intersection (DerivedConfig, array<NamedItem>), and still combines the titles of untitled intersections (Readable & Writable).
@fumadocs/base-ui@16.15.4
fumadocs-core@16.15.4
fumadocs-ui@16.15.4
Fix Next.js <Link> not scrolling to top under docs layouts
The page container rendered <main style="display: contents">, which Next.js' scroll handler treats as a hidden element: display: contents generates no box, so its getBoundingClientRect() is all-zero, indistinguishable from display: none. The handler skips it (and the sticky TOC siblings) without ever descending into children, dropping the scroll-to-top on navigation entirely.
The <main> element in Docs, Notebook and Flux page containers is now a real grid item (display: grid; grid-area: main) wrapping the unchanged #nd-page article, which centers via the grid instead of mx-auto. Rendering is identical, but if your custom CSS has element rules on main that were previously inert, they now apply.
@fumadocs/base-ui@16.15.3
fumadocs-core@16.15.3
fumadocs-ui@16.15.3
Fix duplicated search result for pages whose description repeats in the content
Pages generated by Fumadocs OpenAPI emit the operation description as both the page description and a content section, so every endpoint page listed the same line twice in the search dialog. Search indexing now skips the page description when an identical content record exists, keeping the record with the heading anchor.
Remark LLMs: export a component with output: "function"
With output: "function", _markdown becomes a component instead of a string: Markdown content is still stringified at compile time, while JSX elements stay as JSX, receiving their original props.
Render it with renderToMarkdown from fumadocs-core/server. Elements resolve from props.components: a component can call asMarkdown() to output its own Markdown form, other components (including missing ones) are serialized as JSX syntax.
import { renderToMarkdown } from 'fumadocs-core/server';const { _markdown: Content } = await page.data.load();const text = await renderToMarkdown(<Content components={getMDXComponents()} />);
getText('processed') keeps working: it renders the component for you, with an optional components map:
const text = await page.data.getText('processed', { components: getMDXComponents() });
Supported in bundler collections with both compilers, and in dynamic: true collections & @fumadocs/satteri/local-md with the Sätteri compiler.
fumadocs-core/server: render React trees into Markdown
The Markdown renderer of Fumapress is now part of Fumadocs core. renderToMarkdown() converts RSC output into Markdown, and calling asMarkdown() is how a server component opts in with its own Markdown form:
import { asMarkdown, md, renderToMarkdown } from 'fumadocs-core/server';async function Callout({ title, children }) { if (asMarkdown()) return md.linePrefix('> ')`**${title}**\n${children}`; return <div className="callout">...</div>;}const text = await renderToMarkdown( <Callout title="Note"> <p>Hello</p> </Callout>,);// > **Note**// >// > Hello
Components that never call asMarkdown() are kept as JSX syntax with their serializable props, so are client components, which never run on the server. Host elements returned by an opted-in component are converted with a built-in HTML to Markdown table.
renderRoute() renders a page element only when its component opts in, for giving arbitrary routes a Markdown version. In the browser the module resolves to a stub where asMarkdown() is always false.