# Use feature flags in Fumadocs with the Vercel Toolbar

**Author:** Rich Haines

---

[Feature flags](https://vercel.com/docs/feature-flags) let you roll out product changes to specific users, test features internally, and run A/B tests before a full release. The [Flags SDK](https://vercel.com/docs/feature-flags/feature-flags-pattern) and [Vercel Toolbar](https://vercel.com/docs/vercel-toolbar) give you control over which content your users see.

This guide shows you how to set up the Flags SDK in a [Fumadocs](https://fumadocs.dev/) project with the Vercel Toolbar. Fumadocs is a popular framework for building documentation sites.

You'll build an application that:

- Toggles content visibility within MDX pages
  
- Hides entire pages behind a feature flag
  
- Removes flagged pages from the sidebar
  
- Tests flag states using the Vercel Toolbar
  

## Prerequisites

Before you begin, make sure you have:

- A Next.js project using [Fumadocs](https://fumadocs.dev/docs/ui)
  
- A Vercel account
  
- Basic knowledge of [Next.js](https://nextjs.org/docs) and MDX
  

## Install and configure

To use the Flags SDK and Vercel Toolbar in your project, install the following packages:

`pnpm add flags @vercel/toolbar zod@3.24.2`

> **Note:** Fumadocs version 11.6.6 requires zod version 3.24.2.

Then update your `next.config.js` file to wrap the default MDX config with the Vercel Toolbar:

`import { createMDX } from 'fumadocs-mdx/next'; import createWithVercelToolbar from '@vercel/toolbar/plugins/next'; const withMDX = createMDX(); /** @type {import('next').NextConfig} */ const config = { }; const withVercelToolbar = createWithVercelToolbar(config); export default withVercelToolbar(withMDX);`

## Add the toolbar for testing in dev

Update your root layout file to conditionally render the Vercel Toolbar in development:

`import './global.css'; import { RootProvider } from 'fumadocs-ui/provider'; import { Inter } from 'next/font/google'; import type { ReactNode } from 'react'; import { VercelToolbar } from '@vercel/toolbar/next'; const inter = Inter({ subsets: ['latin'], }); export default function Layout({ children }: { children: ReactNode }) { const shouldInjectToolbar = process.env.NODE_ENV === 'development'; return ( <html lang="en" className={inter.className} suppressHydrationWarning> <body className="flex flex-col min-h-screen"> <RootProvider> {children} {shouldInjectToolbar && <VercelToolbar />} </RootProvider> </body> </html> ); }` ## Define a feature flag Create a new file called `flags.ts` at your project root: `import { flag } from 'flags/next'; export const enableInternalDocsFlag = flag({ key: 'enable-internal-docs', defaultValue: false, decide: () => false, });` This code defines: - `key`: A unique identifier for the flag that you reference in code    - `defaultValue`: The initial state of the flag. This value must be a type that the `decide` function can return. For a boolean flag, the default is typically `false`    - `decide`: A function that determines which value a user sees. It can return any value that matches the type of `defaultValue`    ## Create a discovery endpoint for the flag The Vercel Toolbar discovers available flags through an API route. Create the endpoint with the following code: `import { getProviderData, createFlagsDiscoveryEndpoint } from 'flags/next'; import * as flags from '../../../../flags'; export const GET = createFlagsDiscoveryEndpoint(() => getProviderData(flags));` ## Hide inline content in MDX You can hide parts of your documentation from certain users with a feature flag. Create a component called `InternalContent`: `import { enableInternalDocsFlag } from "@/flags"; import { ReactNode } from "react"; export async function InternalContent({ children }: { children: ReactNode }) { const isEnabled = await enableInternalDocsFlag(); if (!isEnabled) { return null; } return <>{children}</>; }` Register it as a globally available component in all MDX files: `import defaultMdxComponents from "fumadocs-ui/mdx"; import type { MDXComponents } from "mdx/types"; import { Highlight } from "./app/components/highlight"; import { InternalContent } from "./app/components/internal-content"; export function getMDXComponents(components?: MDXComponents): MDXComponents { return { ...defaultMdxComponents, ...components, Highlight, InternalContent, }; }` Import your new component in an MDX file, then add content you want hidden when the flag is disabled: `--- title: Welcome to the Docs description: Getting started guide --- This content is always visible to everyone. <InternalContent> <div className="text-red-500"> ⚠️ This is internal documentation only visible to the team! </div> </InternalContent> ## Getting Started More public content here...` ## Hide entire pages and navigation items Open your `source.config.ts` file and extend the Fumadocs schema to add a new property to the MDX pages frontmatter fields: `import { defineConfig, defineDocs, frontmatterSchema, metaSchema, } from 'fumadocs-mdx/config'; import { z } from 'zod'; // Extend the schema to include the internal field const extendedSchema = (frontmatterSchema as any).extend({ internal: z.boolean().optional(), }); export const docs = defineDocs({ docs: { schema: extendedSchema as any, }, meta: { schema: metaSchema, }, }); export default defineConfig({ mdxOptions: { // MDX options }, });` > **Note:** Use the `as any` casts to avoid "Type instantiation is excessively deep" errors due to complex type inference in Zod with Fumadocs schemas. A later version of Fumadocs might resolve this. ### Protect the page route Check the flag in your page component and return a 404 if the flag is disabled: `import { source } from "@/lib/source"; import { DocsPage, DocsBody, DocsDescription, DocsTitle, } from "fumadocs-ui/page"; import { notFound } from "next/navigation"; import { createRelativeLink } from "fumadocs-ui/mdx"; import { getMDXComponents } from "@/mdx-components"; import { enableInternalDocsFlag } from "@/flags"; export default async function Page(props: { params: Promise<{ slug?: string[] }>; }) { const params = await props.params; const page = source.getPage(params.slug); if (!page) notFound(); // Check if page is marked as internal if (page.data.internal) { const isEnabled = await enableInternalDocsFlag(); if (!isEnabled) notFound(); // Return 404 if flag is off } const MDXContent = page.data.body; return ( <DocsPage toc={page.data.toc} full={page.data.full}> <DocsTitle>{page.data.title}</DocsTitle> <DocsDescription className="flex flex-col border-b pb-4 mb-4"> {page.data.description} </DocsDescription> <DocsBody> <MDXContent components={getMDXComponents({ a: createRelativeLink(source, page), })} /> </DocsBody> </DocsPage> ); } export async function generateStaticParams() { return source.generateParams(); } export async function generateMetadata(props: { params: Promise<{ slug?: string[] }>; }) { const params = await props.params; const page = source.getPage(params.slug); if (!page) notFound(); return { title: page.data.title, description: page.data.description, }; }` ### Hide the page in the navigation When you set the `internal` property in your MDX pages' frontmatter, it hides the page from navigation, preventing users from landing on a 404. This code filters out internal pages from the sidebar by removing those pages from the tree: `import { DocsLayout } from "fumadocs-ui/layouts/notebook"; import type { ReactNode } from "react"; import { baseOptions } from "@/app/layout.config"; import { source } from "@/lib/source"; import { enableInternalDocsFlag } from "@/flags"; import type { PageTree } from "fumadocs-core/server"; export default async function Layout({ children }: { children: ReactNode }) { const isEnabled = await enableInternalDocsFlag(); let tree = source.pageTree; // Filter internal pages if flag is disabled if (!isEnabled) { const internalUrls = new Set( source .getPages() .filter((page) => page.data.internal) .map((page) => page.url), ); tree = { ...tree, children: tree.children .map((node) => filterNode(node, internalUrls)) .filter((n): n is PageTree.Node => n !== null), }; } return ( <DocsLayout tree={tree} {...baseOptions}> {children} </DocsLayout> ); } /** * Recursively filter nodes from the page tree */ function filterNode( node: PageTree.Node, internalUrls: Set<string>, ): PageTree.Node | null { // Hide individual pages if (node.type === "page") { return internalUrls.has(node.url) ? null : node; } // Handle folders if (node.type === "folder") { // Filter children first const children = node.children .map((child) => filterNode(child, internalUrls)) .filter((n): n is PageTree.Node => n !== null); // Check if the folder's index page is internal if (node.index && internalUrls.has(node.index.url)) { // If folder has no other children, hide it completely if (children.length === 0) return null; // If folder has children but index is hidden, keep folder but remove index return { ...node, index: undefined, children }; } // If folder becomes empty after filtering children, hide it if (children.length === 0 && !node.index) return null; return { ...node, children }; } return node; }` This code: - Gets all pages marked with `internal: true`    - Creates a `Set` of their URLs    - Recursively filters the page tree:      - Removes pages in the internal URLs set        - Removes folders that become empty after filtering        - Handles folder index pages separately      ## Mark the page as internal Add `internal: true` to any MDX page's frontmatter. This will hide the page is the flag is set to true: `--- title: Internal Team Docs description: Sensitive information for team members only internal: true --- This entire page is hidden when the flag is enabled. - User won't see it in navigation - Direct URL access returns 404` ## Test locally using the Vercel Toolbar When running your project locally, you’ll see the toolbar as a widget floating above the UI. To test your flag locally, open the flags explorer from the toolbar and: - [Add a flags secret](https://vercel.com/docs/feature-flags/flags-explorer/getting-started#adding-a-flags_secret)
  
- [Enable your flag from the flags explorer](https://vercel.com/docs/feature-flags/flags-explorer)
  

Navigate to the page you added the `InternalContent` component to, you should see the banner you added that marks the page as internal to your team.

---

[View full KB sitemap](/kb/sitemap.md)
