Keep collection dir outside the project in file paths
When a collection's dir was outside the project (e.g. ../content/docs), the leading ../ was dropped from info.fullPath and absolutePath, so getText('raw') failed with ENOENT.
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 moves into a dialog, opened from Try in Playground on the endpoint bar of API pages.
The URL bar selects the server, edits its variables, and fills path parameters inline.
Auth, parameters and the request body are edited in one panel, one row per field with its type in the input. Descriptions and constraints like ranges and defaults open from the info button next to the field name.
Nested objects and arrays open in their own panel, navigated with breadcrumbs. The current breadcrumb switches between the nested fields of its parent, and the body can be edited as JSON.
OAuth 2.0 authorization opens in the same panel instead of a nested dialog, with the flow, client credentials and scopes to request. When a flow redirects to authorize, the playground reopens with the token once it returns to the page.
The response shows beside the code usages with its status, duration, size and headers. Until a request is sent, the documented response examples are shown instead.
Send requests with Cmd/Ctrl + Enter.
components.CollapsiblePanel of the playground options is removed, with the DefaultCollapsiblePanel and CollapsiblePanelProps exports of fumadocs-openapi/ui/playground/client.
Extra props of <PlaygroundClient /> like className now go to its endpoint bar instead of a form, so form props like onSubmit are no longer accepted.
Introduce Spacious Layout
A less compact version of Docs Layout, the page sits in an inset panel beside the sidebar, with page-level actions at the top of the panel.
Use it from fumadocs-ui/layouts/spacious and fumadocs-ui/layouts/spacious/page, and import the styles from fumadocs-ui/css/generated/spacious.css.
Pass aiChat.panel to render your AI chat in the layout, docked beside the page on wide screens and floating over it on smaller ones.
Customize it with npx @fumadocs/cli customize, only available for Base UI.
The Chinese presets of @fumadocs/language include translations for its new strings.
Place AI chat in layouts
Docs, Notebook, and Glass layouts accept an aiChat option, pass your chat as aiChat.panel and the layout places it beside the page on wide viewports, and floats it over the page on smaller ones.
Your chat component no longer needs layout-specific positioning, like targeting #nd-docs-layout or overriding --fd-right-width.
The ai feature of Fumadocs CLI now renders your docs layout from a client component at ai/layout.tsx in your components directory, which passes the installed chat to aiChat. It supports Docs, Notebook, Glass, and Spacious layouts, and only adds a floating trigger to the layouts without their own.
Introduce Spacious Layout
A less compact version of Docs Layout, the page sits in an inset panel beside the sidebar, with page-level actions at the top of the panel.
Use it from fumadocs-ui/layouts/spacious and fumadocs-ui/layouts/spacious/page, and import the styles from fumadocs-ui/css/generated/spacious.css.
Pass aiChat.panel to render your AI chat in the layout, docked beside the page on wide screens and floating over it on smaller ones.
Customize it with npx @fumadocs/cli customize, only available for Base UI.
The Chinese presets of @fumadocs/language include translations for its new strings.
Download response bodies from the playground
The result panel of the API playground has a Download button for non-empty responses. The file is named from the Content-Disposition header, preferring filename* and without directories, otherwise response, which browsers complete with the extension of the media type.
For the browser to read Content-Disposition, cross-origin APIs have to list it in Access-Control-Expose-Headers, or be requested through the proxy.
Search your docs with Meilisearch, self-hosted or on Meilisearch Cloud.
toDocuments() and sync() from fumadocs-core/search/meilisearch export your pages and replace the documents of an index, old documents stay searchable until the new ones are indexed.
meilisearchClient() from fumadocs-core/search/client/meilisearch searches from the browser with useDocsSearch(), filtered by tag and locale.
Search your docs with Meilisearch, self-hosted or on Meilisearch Cloud.
toDocuments() and sync() from fumadocs-core/search/meilisearch export your pages and replace the documents of an index, old documents stay searchable until the new ones are indexed.
meilisearchClient() from fumadocs-core/search/client/meilisearch searches from the browser with useDocsSearch(), filtered by tag and locale.
feature search writes the path of the pre-rendered static.json into scripts/sync-content.ts, and the build script runs it without arguments.
On TanStack Start, the path follows the Nitro output: .output/public, .vercel/output/static with the vercel preset, or dist/client without Nitro.
Typesense documents are built in the static.json route, without a separate lib/export-search-indexes.ts.
Download response bodies from the playground
The result panel of the API playground has a Download button for non-empty responses. The file is named from the Content-Disposition header, preferring filename* and without directories, otherwise response, which browsers complete with the extension of the media type.
For the browser to read Content-Disposition, cross-origin APIs have to list it in Access-Control-Expose-Headers, or be requested through the proxy.
Search your docs with Meilisearch, self-hosted or on Meilisearch Cloud.
toDocuments() and sync() from fumadocs-core/search/meilisearch export your pages and replace the documents of an index, old documents stay searchable until the new ones are indexed.
meilisearchClient() from fumadocs-core/search/client/meilisearch searches from the browser with useDocsSearch(), filtered by tag and locale.
feature search writes the path of the pre-rendered static.json into scripts/sync-content.ts, and the build script runs it without arguments.
On TanStack Start, the path follows the Nitro output: .output/public, .vercel/output/static with the vercel preset, or dist/client without Nitro.
Typesense documents are built in the static.json route, without a separate lib/export-search-indexes.ts.
@fumadocs/base-ui@16.15.17
fumadocs-core@16.15.17
fumadocs-ui@16.15.17
Apply the Vite base path to the "View as Markdown" link
<ViewOptionsPopover /> linked to markdownUrl without the Vite base, so sites served under a base path got a 404. It now prefixes the URL like <MarkdownCopyButton /> does.
<MarkdownCopyButton /> copied the body of error responses, like a 404 page, and kept it cached until a full reload. Failed responses are rejected, and only successful ones are cached.
getPage() accepts both URI encoded and decoded slugs. React Router and TanStack Router pass decoded params, so pages with spaces or non-ASCII characters in their slugs were not found.
Render request bodies of unsupported media types read-only
A request body with a media type that has no adapter no longer throws. It is shown as usual, left out of code usages, and cannot be sent from the playground.
text/plain with parameters, like text/plain; charset=utf-8, is also handled like text/plain.
isMediaTypeSupported() is removed, use resolveMediaAdapter() instead.
A route handler to use as the single OAuth redirect URI of API playgrounds, instead of registering every page. Pass its URL to createOpenAPIPage({ oauthRedirectUrl }).
app/api/oauth/route.ts
import { openapi } from '@/lib/openapi';export const GET = openapi.createOAuthHandler();
The client credentials flow can send the client credentials in an HTTP Basic Authorization header, like the password flow (#3609).
Relative authorizationUrl and tokenUrl are resolved against the selected server, requestOAuthToken() requires a serverUrl for it (#3610).
The redirect_uri leaves out the fragment and query of the page, a fragment is not allowed in redirect URIs.
Multiple scopes are space-delimited, they were sent as a single scope joined by +.
Query params of authorizationUrl, like audience, are kept.
HTTP Basic client credentials are form-encoded, as RFC 6749 requires.
The implicit and authorization code flows send a random state, and keep the flow in sessionStorage instead of the URL: the client secret is no longer sent to the authorization server, the token URL resolves against the server selected when the flow starts, and only flows started in the same tab are accepted.
Skip response examples without a value
An Example Object can omit value, like when it uses externalValue, which crashed the operation page. These examples are skipped.
Examples with the OpenAPI 3.2 dataValue are rendered too.
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.