---
title: Use feature flags in Fumadocs with the Vercel Toolbar
description: Control documentation visibility with feature flags. Hide inline content, entire pages, and navigation items based on flag state.
url: /kb/guide/use-feature-flags-in-fumadocs-with-the-vercel-toolbar
canonical_url: "https://vercel.com/kb/guide/use-feature-flags-in-fumadocs-with-the-vercel-toolbar"
published: 2025-12-02
last_updated: 2026-07-16
authors: Rich Haines
related:
  - /docs/feature-flags
  - /docs/feature-flags/feature-flags-pattern
  - /docs/vercel-toolbar
  - /docs/feature-flags/flags-explorer/getting-started
  - /docs/feature-flags/flags-explorer
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

[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:

```bash
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:

```javascript
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:

```typescript
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:

```typescript
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:

```typescript
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`:

```typescript
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:

```typescript
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:

```markdown
---
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
---
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.