Important
Stay updated on React2Shell

Use feature flags in Fumadocs with the Vercel Toolbar

Control documentation visibility with feature flags. Hide inline content, entire pages, and navigation items based on flag state.

6 min read
Last updated December 2, 2025

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 and 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 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

Before you begin, make sure you have:

  • A Next.js project using Fumadocs
  • A Vercel account
  • Basic knowledge of Next.js and MDX

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:

next.config.js
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);

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

app/layout.ts
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>
);
}

Create a new file called flags.ts at your project root:

flags.ts
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

The Vercel Toolbar discovers available flags through an API route. Create the endpoint with the following code:

app/.well-known/vercel/flags/route.ts
import { getProviderData, createFlagsDiscoveryEndpoint } from 'flags/next';
import * as flags from '../../../../flags';
export const GET = createFlagsDiscoveryEndpoint(() => getProviderData(flags));

You can hide parts of your documentation from certain users with a feature flag. Create a component called InternalContent:

app/components/internal-content.tsx
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:

mdx-components.tsx
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:

docs-home.mdx
---
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...

Open your source.config.ts file and extend the Fumadocs schema to add a new property to the MDX pages frontmatter fields:

source.config.ts
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.

Check the flag in your page component and return a 404 if the flag is disabled:

app/(docs)/[[...slug]]/page.tsx
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,
};
}

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:

app/(docs)/layout.tsx
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

Add internal: true to any MDX page's frontmatter. This will hide the page is the flag is set to true:

internal-team-docs.mdx
---
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

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:

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.

Was this helpful?

supported.
Use feature flags in Fumadocs with the Vercel Toolbar | Knowledge Base