Name schema property link buttons for screen readers
Give the icon-only property link button a translated label and announce when its link has been copied.
Include Simplified and Traditional Chinese translations for both labels.
Name schema property link buttons for screen readers
Give the icon-only property link button a translated label and announce when its link has been copied.
Include Simplified and Traditional Chinese translations for both labels.
Name schema property link buttons for screen readers
Give the icon-only property link button a translated label and announce when its link has been copied.
Include Simplified and Traditional Chinese translations for both labels.
Name schema property link buttons for screen readers
Give the icon-only property link button a translated label and announce when its link has been copied.
Include Simplified and Traditional Chinese translations for both labels.
fumadocs-mdx's Node codegen now sorts glob-matched files before generating collections, so the output (and anything derived from getPages()) is stable across builds of unchanged content. The Vite codegen path was checked separately: Vite's own import.meta.glob already sorts matched files internally, so it did not need the same fix.
Copy buttons are polite live regions whose label switches to "Copied" after a successful copy, so screen readers announce it. The code block's copy button no longer reports success when the clipboard write fails.
The playground installed by npx @fumadocs/cli add openapi/playground imported useAuthFields, requestOAuthToken and their types from fumadocs-openapi/playground, which did not export them. They are now exported.
With a warm build cache, ?only=frontmatter imports were served the fully compiled page from cache instead of the frontmatter module, so every page was bundled two more times. The cache now only applies to full compilations.
Fix infinite recursion in the MDX stringifier with mdast-util-to-markdown@2.1.3
The stringifier wraps every toMarkdown handler but dropped their attention and peek properties, which mdast-util-to-markdown@2.1.3 relies on to serialize bold and italic text. Pages containing them overflowed the stack during build.
Stop recrawling node_modules on every Vite config resolution
The config hook of fumadocs-mdx/vite walked the dependency tree below Fumadocs packages once per chain reaching a package, so a docs app with a few Fumadocs packages read ~10k package.json files (~0.9s) each time Vite resolved its config, which it does once per build environment.
The crawl now visits each package once, breadth-first, and still records the shortest chain to every CommonJS dependency (fumadocs-ui > @base-ui/react > use-sync-external-store/shim and friends). The result is memoized for the process until the package manager's install state changes, so a build with several environments crawls once.
@fumadocs/base-ui@16.15.14
fumadocs-core@16.15.14
fumadocs-ui@16.15.14
Announce copy confirmation to screen readers
Copy buttons are polite live regions whose label switches to "Copied" after a successful copy, so screen readers announce it. The code block's copy button no longer reports success when the clipboard write fails.
Expose the search dialog as a combobox
The search input is now a combobox that controls a listbox of option results and reports the highlighted result through aria-activedescendant, so screen readers announce results as you move through them. Hidden result buttons are removed from the tab order, and the empty state is announced as a status message.
Remove hidden sidebar controls from the tab order
The collapsed sidebar and its floating pill are now inert while hidden, and toggling the sidebar moves focus to the trigger that becomes visible.
Add block TOC style
A TOC style without the track line: headings are indented by depth, and a block slides behind the active headings.
<DocsPage tableOfContent={{ style: 'block' }} />
Wrap page footer descriptions instead of truncating
The previous/next page footer cards now wrap long descriptions instead of cutting them off with an ellipsis.
Move proxy.ts into src when the /src directory is enabled
The Next.js template ships a proxy.ts that rewrites <page>.md and Accept: text/markdown requests to the Markdown route.
With the /src option, the CLI moved app, lib and components into src but left proxy.ts at the project root, where Next.js silently ignores it.
The CLI now moves proxy.ts (and the other root convention files Next.js reads next to app) into src, so the Markdown rewrite runs in generated projects.
@fumadocs/base-ui@16.15.13
fumadocs-core@16.15.13
fumadocs-ui@16.15.13
Keep the collapsed sidebar's controls off the page title
With the sidebar collapsed, the docs layout floats the reopen and search buttons in a fixed pill at the top-left of the page and starts the article at the same row.
Wherever the article is not centered with room to spare, every viewport below about 1280px, the pill covered the page title.
The article now leaves room for the pill while the sidebar is collapsed.
AI page actions name the page by the URL the reader is on
The "Open in ..." prompts built the page URL from the router pathname and the origin.
Next's usePathname() omits a configured basePath, so a site mounted under one sent assistants a URL that did not exist.
The prompt now uses the reader's current URL, without query and hash, and falls back to the pathname during server rendering.
A new pageUrl prop on ViewOptionsPopover sets a canonical URL instead.
Optimize Performance
Use useSyncExternalStore() from React.
Keep TOC step numbers after an HTML re-parse
remarkSteps marks each step heading with a numeric data-fd-step, and the TOC plugin only read it as a number.
A later rehype-raw pass re-parses the tree from HTML, so the property came back as the canonical dataFdStep string and every step number silently vanished from the table of contents.
rehypeToc now accepts both shapes, so pipelines that render raw HTML in Markdown keep their numbered TOC.
New registry format
The CLI is upgraded to Fuma CLI 0.3, the registry is now a manifest with the raw files instead of one JSON per component.
Installing fetches every needed file in parallel, and no longer parses the installed files to link their imports.
Layouts are imported from fumadocs-ui unless you have installed them, without a Fumadocs-specific plugin.
Older versions of the CLI cannot read the new registry, upgrade to install components.
Moved files
Some components are installed to a location that follows their source, update your imports if you install them again:
Slots are named after their path, like layouts/docs/slots/sidebar instead of slots/docs/sidebar. fumadocs customise is unchanged.
@fumadocs/base-ui@16.15.12
fumadocs-core@16.15.12
fumadocs-ui@16.15.12
Fix filterElement being ignored by remarkLLMs
remarkLLMs wrote its own filterElement over yours, so the option did nothing:
remarkLLMs({ // never ran filterElement: (node) => node.name !== 'Callout',});
Your function now runs for every node except mdxjsEsm, which stays excluded either way.
Fumadocs MDX passes this option through postprocess.includeProcessedMarkdown.
Headless API pages
API pages are now built on a headless layer, use it to build your own UI:
fumadocs-openapi: createOpenAPIRenderer() with your own components, and the hooks of a page: useOpenAPI(), useComponents(), useServer() and useRenderContext(). createOpenAPIBaseRenderer() is the same with nothing built in: pass shiki, codeUsages and generateTypeScriptDefinitions yourself.
fumadocs-openapi/operation: <OperationProvider /> and the hooks of an operation, like useOperation() and useExampleRequests().
fumadocs-openapi/playground: useAuthFields() turns the security requirements of an operation into form fields, the ones the API playground renders and encodes into request data. requestOAuthToken() runs an OAuth flow of a security scheme.
the Schema UI, installed with npx @fumadocs/cli add fumadocs/api-docs/schema: its generation and navigation state (generateSchemaUI(), useSchemaTabs()) come with the copy.
useServer() also resolves the URL of a request: resolveUrl(pathname) fills in the variables of the selected server, against the page origin.
Code samples from x-codeSamples and generateCodeSamples are now rendered when their id isn't a built-in generator.
Data info tags in Schema UI
SchemaData.infoTags entries are data rendered by the UI: { label, value, block? } or { label, list }. Custom nodes ({ node }) are still accepted, code reading tag.node must handle all shapes.
playground.provider is removed, the page provides the auth state of API playground.
playground.render and generateTypeScriptDefinitions no longer receive ctx, read the document from useOpenAPI().doc, or the doc passed to generateTypeScriptDefinitions.
operation.APIExampleSelector is removed, install fumadocs/openapi/operation and edit the selector in usage-tabs.tsx.
<PlaygroundClient /> reads the operation from useOperation(), its route, method, operation and pathItem props are gone: playground.render becomes () => <PlaygroundClient writeOnly readOnly={false} />.
The ctx of content render options is the render options of the page, what useRenderContext() returns: shiki, content, playground, showResponseSchema and schemaUI. ctx.schema, ctx.SchemaUI and ctx._default_processMarkdown are removed, read the document from useOpenAPI().doc and the components from useComponents().
Migrate hooks
The hooks of fumadocs-openapi/ui are replaced by the headless ones:
v11
v12
useRenderContext()
useOpenAPI() (schema is renamed to doc), useComponents(), and useRenderContext() for the render options only
useServerContext()
useServer()
useOperationContext()
useOperation(), useExampleRequests(), useExampleRequest() (on /operation)
- const { route, examples, example, setExample, setExampleData } = useOperationContext();+ const { path } = useOperation();+ const { items, selected, select, update } = useExampleRequests();+ // data of the selected example, replaces `addListener()`+ const data = useExampleRequest();
Components installed from v11 with Fumadocs CLI (e.g. the API playground) use the old hooks, reinstall them.
Remove deprecated APIs
Removed
Use
fumadocs-openapi/ui/create-client
createOpenAPIPage() from fumadocs-openapi/ui
ApiPageProps
OpenAPIPageProps
OperationItem and WebhookItem of fumadocs-openapi/ui
import them from fumadocs-openapi
getAPIPageProps() and getClientAPIPageProps()
getOpenAPIPageProps()
defineI18nOpenAPI()
i18n.translations().extend(openapiTranslations())
APIPage of MDX components
OpenAPIPage, generated files only render it
Moved exports
Everything that renders sits under /ui:
Was
Now
fumadocs-openapi/playground/client
fumadocs-openapi/ui/playground/client
fumadocs-openapi/scalar
fumadocs-openapi/ui/scalar
GenerateTypeScriptDefinitionsContext now comes from the package entry, next to the runtime option it types.
Client-safe package entry
generateFiles() reads and writes files, so the package entry ships a stubbed build under the browser condition. Client components can import createOpenAPIRenderer() and the hooks without pulling node:fs into the bundle.
Default page components
createOpenAPIRenderer(), createAsyncAPIRenderer() and createGraphQLRenderer() fill the Markdown, CodeBlock and Heading components you didn't pass, rendering Markdown through Remark and code blocks through Shiki:
Select and Input follow the Shadcn UI API, so a project that already has them keeps its own. @fumadocs/story no longer ships a second copy of either.
labelVariants moved to the installed label component, leaving the input a plain Shadcn-compatible primitive.
The integrations share one implementation of these internally, instead of each keeping a copy: the selected server and its variables, the state of an async request, the coloured label of methods and kinds, and the plain-object check of both schema layers.
The request pipeline of the playground stays in the package too, so an installed playground drives it instead of copying it: encodeRequestData(), resolveMediaAdapter(), isMediaTypeSupported() and the request data types come from fumadocs-openapi/requests, and createBrowserFetcher() with usePlaygroundAuth() from fumadocs-openapi/playground.
@fumadocs/json-schema
The JSON Schema utilities of API pages are now their own package, with no Fumadocs dependencies:
import { dereference, matches, mergeAllOf, sample, stringify } from '@fumadocs/json-schema';import { bundle } from '@fumadocs/json-schema/bundle';
bundle() is a separate entry because it reads files and URLs, everything else runs in the browser.
@fumadocs/json-schema/react renders a schema into the data an API page draws: generateSchemaUI() with the SchemaData and InfoTag types. It was in the Schema UI before, where every install copied it. A labelled tag can be prose, for values like a Markdown deprecation reason.
They were @fumadocs/api-docs/schema/* before, and the API was cleaned up while moving:
Before
Now
ParsedSchema
JsonSchema
NoReference / NoReferenceSwallow
Dereferenced / DereferencedShallow
dereferenceShallow(schema)
dereference(schema)
matchesSchema(schema, value)
matches(schema, value)
typeMatches(value, type)
matchesType(value, type)
schemaToString(schema, FormatFlags.UseAlias)
stringify(schema, { alias: true })
Trim code usages and TypeScript definitions
createOpenAPIBaseRenderer() from fumadocs-openapi registers no code usage generators and no TypeScript definitions, so a page built on it bundles only what you pass:
import { createCodeUsageGeneratorRegistry } from 'fumadocs-openapi/requests/generators';import { curl } from 'fumadocs-openapi/requests/generators/curl';createOpenAPIBaseRenderer({ shiki, codeUsages: createCodeUsageGeneratorRegistry().register(curl), components: { ... },});
createOpenAPIRenderer() and fumadocs-openapi/ui register every language and TypeScript definitions for you.
Remove useStorageKey()
The hook returned (name) => storageKeyPrefix + name. Read the prefix from the page instead:
It held the UI the integrations share, and that UI is now either bundled into them or installed with Fumadocs CLI, so nothing imports it by name any more. If you imported it directly:
Before
Now
@fumadocs/api-docs/schema/*
@fumadocs/json-schema
@fumadocs/api-docs/components/schema*
npx @fumadocs/cli add fumadocs/api-docs/schema
@fumadocs/api-docs/components/* (the UI)
installed with the component that uses it
@fumadocs/api-docs/i18n
the integration's own Translations covers its keys
@fumadocs/api-docs/css/preset.css
already included by the integration's preset
The CLI namespace is unchanged, fumadocs/api-docs/schema still installs the Schema UI.
This exported _mdast with no value, and getMDAST() then reported that includeMDAST was disabled. removePosition strips positions in place and returns nothing, so JSON.stringify received undefined.
The tree is now cloned, stripped, and serialized from the clone.
Fix SOURCEMAP_BROKEN warnings on Vite
With build.sourcemap enabled, Vite warned once per content and meta file because the loaders returned no source map. They now return an empty map when nothing is generated.
Source maps for MDX stay opt-in, pass SourceMapGenerator from source-map to MDX options:
createOpenAPIRenderer(), createAsyncAPIRenderer() and createGraphQLRenderer() fill the Markdown, CodeBlock and Heading components you didn't pass, rendering Markdown through Remark and code blocks through Shiki:
Select and Input follow the Shadcn UI API, so a project that already has them keeps its own. @fumadocs/story no longer ships a second copy of either.
labelVariants moved to the installed label component, leaving the input a plain Shadcn-compatible primitive.
The integrations share one implementation of these internally, instead of each keeping a copy: the selected server and its variables, the state of an async request, the coloured label of methods and kinds, and the plain-object check of both schema layers.
The request pipeline of the playground stays in the package too, so an installed playground drives it instead of copying it: encodeRequestData(), resolveMediaAdapter(), isMediaTypeSupported() and the request data types come from fumadocs-openapi/requests, and createBrowserFetcher() with usePlaygroundAuth() from fumadocs-openapi/playground.
Replace the UI of stories
The control panel is no longer fixed, defineStoryFactory() takes your own:
lib/story.tsx
import { defineStoryFactory } from '@fumadocs/story/vite/client';import { WithControl } from '@/components/story';export const { defineStory } = defineStoryFactory({ WithControl });
Install the built-in one and edit it:
npx @fumadocs/cli add fumadocs/story/controls
Or build your own on @fumadocs/story: <StoryProvider /> holds the selected variant and the form engine, useStory() exposes the presets and useStoryArgs() the arguments of the rendered component.
The entry ships a stubbed build under the browser condition, so client components can import them without pulling the story factory's Node dependencies. @fumadocs/story/type-tree does the same: collapse() is stubbed under the browser condition, so the installed controls import the sampler, stringifier and validator from it.
The control of a union prop validated the field against the index of each branch, so it picked an arbitrary one. It now validates against the current value, falling back to the first branch.
validate() from @fumadocs/story/type-tree also rejected null for null nodes, it compared against the string 'null'.
@fumadocs/json-schema
The JSON Schema utilities of API pages are now their own package, with no Fumadocs dependencies:
import { dereference, matches, mergeAllOf, sample, stringify } from '@fumadocs/json-schema';import { bundle } from '@fumadocs/json-schema/bundle';
bundle() is a separate entry because it reads files and URLs, everything else runs in the browser.
@fumadocs/json-schema/react renders a schema into the data an API page draws: generateSchemaUI() with the SchemaData and InfoTag types. It was in the Schema UI before, where every install copied it. A labelled tag can be prose, for values like a Markdown deprecation reason.
They were @fumadocs/api-docs/schema/* before, and the API was cleaned up while moving:
Before
Now
ParsedSchema
JsonSchema
NoReference / NoReferenceSwallow
Dereferenced / DereferencedShallow
dereferenceShallow(schema)
dereference(schema)
matchesSchema(schema, value)
matches(schema, value)
typeMatches(value, type)
matchesType(value, type)
schemaToString(schema, FormatFlags.UseAlias)
stringify(schema, { alias: true })
Trim code usages and TypeScript definitions
createOpenAPIBaseRenderer() from fumadocs-openapi registers no code usage generators and no TypeScript definitions, so a page built on it bundles only what you pass:
import { createCodeUsageGeneratorRegistry } from 'fumadocs-openapi/requests/generators';import { curl } from 'fumadocs-openapi/requests/generators/curl';createOpenAPIBaseRenderer({ shiki, codeUsages: createCodeUsageGeneratorRegistry().register(curl), components: { ... },});
createOpenAPIRenderer() and fumadocs-openapi/ui register every language and TypeScript definitions for you.
Remove useStorageKey()
The hook returned (name) => storageKeyPrefix + name. Read the prefix from the page instead:
It held the UI the integrations share, and that UI is now either bundled into them or installed with Fumadocs CLI, so nothing imports it by name any more. If you imported it directly:
Before
Now
@fumadocs/api-docs/schema/*
@fumadocs/json-schema
@fumadocs/api-docs/components/schema*
npx @fumadocs/cli add fumadocs/api-docs/schema
@fumadocs/api-docs/components/* (the UI)
installed with the component that uses it
@fumadocs/api-docs/i18n
the integration's own Translations covers its keys
@fumadocs/api-docs/css/preset.css
already included by the integration's preset
The CLI namespace is unchanged, fumadocs/api-docs/schema still installs the Schema UI.