All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
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.
On projects with a components.json, the CLI leaves the button, popover and collapsible of your ui directory as they are and imports them from installed components, they share the API of Shadcn UI. cn is imported from your utils alias instead of a new lib/cn.ts, configurable in cli.json:
{ "aliases": { "utils": "./lib/utils" }}
Fumadocs' copies are only installed when you don't have them yet. The tabs and accordion primitives are Fumadocs specific, they are installed next to their components instead of the ui directory.
customize updates the imports of your route files to the installed layouts.
feature ai and feature feedback install the primitives of your configured uiLibrary, they always used the Radix UI ones before.
Mark packages side-effect free
All packages now declare sideEffects in package.json, so bundlers can tree-shake unused modules. Packages shipping stylesheets list them as side effects to keep CSS imports.
@fumadocs/base-ui@16.15.10
fumadocs-core@16.15.10
fumadocs-ui@16.15.10
Fix language switching with hidden locale prefixes
Root Provider's i18n option now accepts defaultLanguage and hideLocale. The language switcher uses these options instead of guessing from the current URL, so switching from /zh/docs to the default language en with hideLocale: 'default-locale' navigates to /docs.
i18nProvider() and defineI18nUI() pass these options from your i18n config automatically.
Fix same-page anchors on Tanstack Start
Tanstack Router's Link takes a pathname in to and reads the hash from a separate hash prop, so a same-page anchor like [link](#installation) was rendered as a link to the current page with the hash dropped, clicking it did nothing. The Tanstack adapter now renders a native <a> for those hrefs:
Jump to [Installation](#installation).
Search providers and shared CLI features
--search accepts algolia, typesense and mixedbread in addition to orama and orama-cloud.
Search, linter, OG image and Ask AI options are now applied by the features of @fumadocs/cli, the same code that configures them on an existing app, instead of template plugins. create-fumadocs-versions pins the versions of the new search dependencies.
page(page) renders one page, for the per-page Markdown route.
full(lang?) renders every page and joins them, for llms-full.txt.
// app/llms-full.txt/route.tsexport const GET = async () => new Response(await docsLlms.full());
Both methods exist only when renderPage is given, in types and at runtime. Fumadocs cannot know how your content source exposes Markdown: page.data.getText('processed') on Fumadocs MDX, page.data.content on @fumadocs/local-md.
fumadocs-core/mcp: docs tools for your MCP server
Register the docs tools on a server you own, rather than on a handler Fumadocs builds for you:
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp';import { createFromSource } from 'fumadocs-core/search/server';import { docsLlms, source } from '@/lib/source';const handler = createMcpHandler(() => { const mcp = new McpServer({ name: 'docs', version: '1.0.0' }); registerSourceTools(mcp, source, docsLlms); registerSearchTool(mcp, createFromSource(source)); return mcp;});export const GET = (req: Request) => handler.fetch(req);export const POST = (req: Request) => handler.fetch(req);export const DELETE = (req: Request) => handler.fetch(req);
Function
Tools
registerSourceTools(mcp, source, llms)
list_pages, get_page
registerSearchTool(mcp, server)
search
Each takes the integration it reads from, so you can add your own tools next to them, point the search tool at another server, or register only one of the two.
@modelcontextprotocol/server is an optional peer dependency, and registerSourceTools needs a llms() output with renderPage.
toDocuments() for Algolia and Orama Cloud
Build the search indexes of every page, instead of mapping pages by hand:
// app/static.json/route.tsimport { toDocuments } from 'fumadocs-core/search/algolia';import { source } from '@/lib/source';export const GET = async () => Response.json(await toDocuments(source));
It awaits structuredData when your collection is async (React Router and TanStack Start), a step that was easy to miss. Pass tag to filter results by a value of your choice:
Exported from fumadocs-core/search/algolia and fumadocs-core/search/orama-cloud.
Marked as side-effect free
fumadocs-core now declares "sideEffects": false. No module in the package imports for side effects or ships CSS, so bundlers that rely on the hint (webpack in particular) can drop unused modules instead of keeping them alive:
import { createGetUrl } from 'fumadocs-core/source';
path in operation renderers
renderOperationLayout and generateCodeSamples now receive the operation's path, so custom layouts no longer need a React context to reach it:
A route handler for the EPUB export, replacing the auth check and response headers you had to write yourself:
// app/export/epub/route.tsimport { createEpubExportAPI } from 'fumadocs-epub';import { source } from '@/lib/source';export const { GET } = createEpubExportAPI({ source, title: 'Documentation', author: 'Your Team', secret: process.env.EXPORT_SECRET,});
secret is required: requests must send Authorization: Bearer <secret>, and the route answers 503 while the secret itself is unset, so a missing environment variable cannot leave the export open. It also takes filename (default docs.epub) and every option of exportEpub(), which stays available for public routes and scripts.
init and feature commands
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').