Build a Modern Documentation Site with Fumadocs and Next.js 15 in 2026

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

Documentation is the public face of every serious developer product. A great docs site converts curious visitors into paying customers, while a slow or confusing one quietly drives them away. In 2026, a new generation of docs frameworks has emerged, and Fumadocs has rapidly become the favorite choice for teams using Next.js. It delivers Mintlify-grade aesthetics, deep MDX support, full-text search, and zero vendor lock-in — all running on top of the App Router you already know.

In this tutorial, we will build a complete documentation site from scratch using Fumadocs and Next.js 15. By the end, you will have a production-ready docs platform with versioning, dark mode, instant search, code group tabs, callouts, custom MDX components, and an OpenAPI explorer.

Prerequisites

Before starting, ensure you have:

  • Node.js 20 or newer installed
  • A working knowledge of React and Next.js App Router
  • Familiarity with TypeScript and MDX
  • A code editor (VS Code with the MDX extension is recommended)
  • A terminal and Git

This tutorial assumes intermediate knowledge of Next.js 15. If you are new to the App Router, review the official Next.js docs first.

What You Will Build

We will build NoqtaDocs, a fictional documentation site for an internal API platform. By the end, the site will have:

  • A polished landing page with a hero and feature cards
  • A sidebar navigation that mirrors the folder structure
  • MDX pages with custom components (callouts, tabs, file trees)
  • Full-text client-side search powered by Orama
  • Dark mode and accessible theming
  • An OpenAPI explorer page
  • Version switching for v1 and v2 of the API
  • Static export ready for deployment to Vercel or Cloudflare

Why Fumadocs in 2026

Three years ago, most teams reached for Mintlify, GitBook, or a hand-rolled Next.js site. Each option came with trade-offs. Mintlify locked you into a SaaS pricing model, GitBook felt sluggish for code-heavy docs, and DIY sites required reinventing search, theming, and navigation every time.

Fumadocs solves all three problems at once. It is a library, not a platform, so you own your code. It ships with first-class App Router support, server components, MDX 3, and full Tailwind CSS v4 compatibility. The default theme is gorgeous out of the box, but every component is customizable.

Compared to Nextra 4, Fumadocs gives you finer control over routing and layouts, and its tree-walking algorithm produces faster builds on large doc sets. Compared to Starlight, it stays in the Next.js ecosystem, which means you can colocate marketing pages, dashboards, and docs in the same monorepo.

Step 1: Project Setup

Let's start by creating a fresh Next.js 15 project. Open your terminal and run the Fumadocs scaffolding command, which sets up everything for you.

npx create-fumadocs-app@latest noqtadocs

The CLI will ask several questions. For this tutorial, choose:

  • Framework: Next.js
  • Content source: Fumadocs MDX
  • Tailwind CSS: Yes
  • Install dependencies: Yes

Once installation finishes, navigate into the project and start the dev server.

cd noqtadocs
npm run dev

Open http://localhost:3000 in your browser. You should see the default Fumadocs landing page with a sidebar already populated by example MDX files.

Folder structure overview

The scaffold produces a clean, predictable structure.

noqtadocs/
├── app/
│   ├── (home)/
│   │   └── page.tsx
│   ├── docs/
│   │   ├── [[...slug]]/
│   │   │   └── page.tsx
│   │   └── layout.tsx
│   ├── api/
│   │   └── search/
│   │       └── route.ts
│   ├── layout.config.tsx
│   └── layout.tsx
├── content/
│   └── docs/
│       ├── index.mdx
│       └── test.mdx
├── lib/
│   └── source.ts
├── source.config.ts
└── package.json

Key files to remember:

  • source.config.ts — declares your content collections and frontmatter schema
  • lib/source.ts — exports the source object used to read your docs
  • app/docs/[[...slug]]/page.tsx — the dynamic route rendering each doc page
  • app/api/search/route.ts — the built-in Orama search endpoint
  • content/docs/ — where your MDX files live

Step 2: Configure Your Content Source

Open source.config.ts. This file tells Fumadocs how to parse your MDX, what frontmatter to expect, and how to generate the searchable index.

import { defineDocs, defineConfig } from "fumadocs-mdx/config";
import { z } from "zod";
 
export const docs = defineDocs({
  dir: "content/docs",
  docs: {
    schema: z.object({
      title: z.string(),
      description: z.string().optional(),
      icon: z.string().optional(),
      version: z.enum(["v1", "v2"]).default("v2"),
    }),
  },
});
 
export default defineConfig({
  mdxOptions: {
    rehypeCodeOptions: {
      themes: {
        light: "github-light",
        dark: "github-dark",
      },
    },
  },
});

We added a custom version field to the frontmatter schema. We will use it later to power version switching. The rehypeCodeOptions block enables Shiki syntax highlighting with both light and dark themes, which automatically adapts to the user's color scheme.

Restart the dev server so the schema change takes effect.

npm run dev

Step 3: Build the Sidebar Navigation

Fumadocs derives navigation directly from your folder structure. To group docs into sections, create a meta.json file in each folder. Replace the contents of content/docs/ with the following structure.

content/docs/
├── index.mdx
├── meta.json
├── getting-started/
│   ├── meta.json
│   ├── installation.mdx
│   └── quickstart.mdx
├── guides/
│   ├── meta.json
│   ├── authentication.mdx
│   └── webhooks.mdx
└── reference/
    ├── meta.json
    └── openapi.mdx

Here is the root meta.json:

{
  "title": "NoqtaDocs",
  "pages": [
    "index",
    "---Getting Started---",
    "getting-started",
    "---Guides---",
    "guides",
    "---Reference---",
    "reference"
  ]
}

The ---Title--- syntax creates section dividers in the sidebar. The order of the array controls the visual order. This explicit control is what makes Fumadocs feel curated rather than alphabetically chaotic.

Inside each subfolder, add a meta.json that lists the pages.

{
  "title": "Getting Started",
  "pages": ["installation", "quickstart"]
}

Step 4: Write Your First Doc Pages

Let's add real content. Open content/docs/getting-started/installation.mdx and paste the following.

---
title: Installation
description: Install the Noqta SDK in your project
icon: Download
---
 
The Noqta SDK is published on npm and supports Node.js 20 and newer.
 
## Install with your package manager
 
import { Tabs, Tab } from "fumadocs-ui/components/tabs";
 
<Tabs items={["npm", "pnpm", "bun"]}>
  <Tab value="npm">```bash npm install @noqta/sdk ```</Tab>
  <Tab value="pnpm">```bash pnpm add @noqta/sdk ```</Tab>
  <Tab value="bun">```bash bun add @noqta/sdk ```</Tab>
</Tabs>
 
## Verify the installation
 
Create a small test script and run it.
 
```typescript
import { NoqtaClient } from "@noqta/sdk";
 
const client = new NoqtaClient({ apiKey: process.env.NOQTA_API_KEY });
const status = await client.health.check();
console.log(status);

If everything is set up correctly, you will see a JSON response confirming the connection.


The `Tabs` component is one of dozens shipped with `fumadocs-ui`. Each tab can host any MDX content, including more code blocks, tables, or images.

## Step 5: Add Custom Callouts and File Trees

Fumadocs ships with a rich set of MDX components. The two most useful are `Callout` and `Files`. Add a new section to your installation page.

```mdx
import { Callout } from "fumadocs-ui/components/callout";
import { Files, Folder, File } from "fumadocs-ui/components/files";

<Callout type="info">
  The SDK ships with TypeScript types out of the box. No extra installs needed.
</Callout>

<Callout type="warn">
  Never commit your API key to git. Use environment variables instead.
</Callout>

## Project structure

After installation, your project should look like this.

<Files>
  <Folder name="src" defaultOpen>
    <File name="index.ts" />
    <Folder name="lib">
      <File name="noqta.ts" />
    </Folder>
  </Folder>
  <File name=".env.local" />
  <File name="package.json" />
</Files>

Save the file and refresh your browser. The callouts render with colored borders and the file tree is interactive — folders can be expanded and collapsed.

Step 6: Customize the Layout

Open app/layout.config.tsx. This file controls the global layout: the top nav, the sidebar header, and the GitHub link.

import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
 
export const baseOptions: BaseLayoutProps = {
  nav: {
    title: (
      <span className="font-semibold">
        NoqtaDocs
      </span>
    ),
  },
  links: [
    { text: "Documentation", url: "/docs" },
    { text: "Blog", url: "/blog" },
    { text: "Changelog", url: "/changelog" },
  ],
  githubUrl: "https://github.com/your-org/noqtadocs",
};

The top nav now displays your branding and external links. Because everything is server-rendered, these change instantly with no client-side flash.

The scaffold already includes Orama-powered search at app/api/search/route.ts. Open it and confirm it looks like this.

import { source } from "@/lib/source";
import { createFromSource } from "fumadocs-core/search/server";
 
export const { GET } = createFromSource(source);

That is all the code needed. Fumadocs walks your content tree, extracts headings, and builds an Orama index in memory. The search results include rich highlighting of matched terms.

To open the search dialog, press Cmd+K or Ctrl+K. Search results are grouped by page and show the matching heading, which makes deep linking to specific sections trivial.

For larger doc sets with more than a thousand pages, switch to the Algolia or Trieve adapters. The same createFromSource API is used, with a different backend.

Step 8: Add a Beautiful Landing Page

The default landing page is a placeholder. Replace app/(home)/page.tsx with a hero, feature grid, and call to action.

import Link from "next/link";
 
export default function HomePage() {
  return (
    <main className="flex flex-1 flex-col">
      <section className="container flex flex-col items-center gap-6 py-24 text-center">
        <h1 className="text-5xl font-bold tracking-tight">
          The fastest way to ship docs.
        </h1>
        <p className="max-w-2xl text-lg text-fd-muted-foreground">
          NoqtaDocs is a documentation platform built on Fumadocs and Next.js 15.
          Zero vendor lock-in, gorgeous defaults, instant search.
        </p>
        <div className="flex gap-4">
          <Link
            href="/docs"
            className="rounded-md bg-fd-primary px-6 py-3 font-medium text-fd-primary-foreground"
          >
            Read the docs
          </Link>
          <Link
            href="https://github.com/your-org/noqtadocs"
            className="rounded-md border px-6 py-3 font-medium"
          >
            View on GitHub
          </Link>
        </div>
      </section>
    </main>
  );
}

Notice the design tokens prefixed with fd-. Fumadocs ships with a complete CSS variable system that adapts to light and dark mode automatically. Use these tokens instead of hardcoding colors.

Step 9: Implement Version Switching

API documentation often needs to support multiple versions. We added a version field to the frontmatter earlier. Now we will use it to filter pages.

Create a new file lib/source.ts if it does not already exist.

import { docs } from "@/.source";
import { loader } from "fumadocs-core/source";
 
export const source = loader({
  baseUrl: "/docs",
  source: docs.toFumadocsSource(),
});
 
export function getVersionPages(version: "v1" | "v2") {
  return source.getPages().filter((page) => page.data.version === version);
}

Add a version switcher to the sidebar by creating app/docs/layout.tsx.

import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { source } from "@/lib/source";
import { baseOptions } from "@/app/layout.config";
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <DocsLayout
      tree={source.pageTree}
      sidebar={{
        banner: (
          <select className="w-full rounded-md border bg-fd-card p-2">
            <option value="v2">API v2 (latest)</option>
            <option value="v1">API v1 (legacy)</option>
          </select>
        ),
      }}
      {...baseOptions}
    >
      {children}
    </DocsLayout>
  );
}

For a fully interactive switcher, extract the dropdown into a client component and use useRouter to navigate to the correct version subtree. Production sites typically segment versions into folders like content/docs/v1/ and content/docs/v2/.

Step 10: Add an OpenAPI Explorer

Fumadocs includes an official OpenAPI integration that turns your API spec into interactive documentation. Install it.

npm install fumadocs-openapi

Generate MDX files from an OpenAPI spec. Create scripts/generate-openapi.ts.

import { generateFiles } from "fumadocs-openapi";
 
void generateFiles({
  input: ["./openapi.json"],
  output: "./content/docs/reference",
  per: "operation",
});

Run the script.

npx tsx scripts/generate-openapi.ts

Each API operation becomes its own MDX page, complete with parameter tables, response samples, and a live "Try it" panel. The generated pages live alongside your hand-written docs and inherit the same theme.

Step 11: Theme and Polish

Open app/global.css and customize the design tokens. Fumadocs uses HSL CSS variables that you can override per theme.

@layer base {
  :root {
    --color-fd-primary: 250 95% 55%;
    --color-fd-primary-foreground: 0 0% 100%;
    --color-fd-background: 0 0% 100%;
  }
 
  .dark {
    --color-fd-primary: 250 95% 65%;
    --color-fd-background: 240 10% 4%;
  }
}

The brand color now flows through buttons, links, active sidebar items, and search highlights. Changes hot-reload instantly thanks to Tailwind v4.

To add your logo to the top nav, drop an SVG in public/logo.svg and reference it in layout.config.tsx.

import Image from "next/image";
 
nav: {
  title: (
    <span className="flex items-center gap-2">
      <Image src="/logo.svg" alt="" width={24} height={24} />
      <span className="font-semibold">NoqtaDocs</span>
    </span>
  ),
},

Step 12: Build and Deploy

Build the production bundle.

npm run build

Fumadocs uses Next.js incremental static regeneration by default. Every page is statically generated at build time, and the search index is shipped as a single JSON file fetched on demand.

For Vercel deployment, push to GitHub and import the repo. No additional configuration is needed.

For Cloudflare Pages, add a wrangler.toml and use the @cloudflare/next-on-pages adapter. Build size is typically under 5 MB even for 500-page sites, well within Cloudflare's free tier.

Testing Your Implementation

Run through this checklist before sharing your site.

  • Open /docs and click every sidebar item. All should render without errors.
  • Press Cmd+K and search for terms across multiple pages. Results should highlight matches.
  • Toggle dark mode using the theme switcher. Verify code blocks adapt correctly.
  • Resize the window down to mobile width. The sidebar should collapse into a drawer.
  • Inspect the page source. Each doc should ship with proper meta tags and OpenGraph images.
  • Run Lighthouse. A correctly configured Fumadocs site scores 95 or higher on all metrics.

Troubleshooting

Sidebar does not show new pages. Restart the dev server. Fumadocs caches the page tree at startup and rebuilds it whenever source.config.ts or meta.json changes.

MDX components render as raw HTML. Make sure you imported the component at the top of the MDX file. Unlike the Pages Router, App Router MDX files do not auto-import.

Search returns no results. Check app/api/search/route.ts is exporting GET correctly. Open the browser network tab and look for a 200 response from /api/search.

Build fails with type errors on frontmatter. Your Zod schema in source.config.ts does not match the frontmatter in your MDX files. Add the missing field or mark it optional.

Next Steps

You now have a production-grade documentation site. Here are some ideas for extending it.

  • Add a /blog section using the same MDX pipeline
  • Integrate analytics with PostHog or Plausible to track popular pages
  • Auto-generate changelog pages from your GitHub releases
  • Add an AI-powered docs assistant using the Vercel AI SDK and your search index
  • Create a public API explorer that runs requests directly from the browser

For more advanced patterns, see our related tutorials on Next.js 15 server actions, the AI SDK, and TanStack Query.

Conclusion

Fumadocs strikes a rare balance between developer ergonomics and design polish. It gives you the same beautiful aesthetics as paid SaaS docs platforms while keeping every line of code in your repository. By combining server components, MDX 3, Tailwind v4, and Orama search, it produces a docs site that is fast to build, fast to load, and easy to maintain.

If you are starting a new project in 2026, Fumadocs is the safest bet for documentation. The community is vibrant, the API is stable, and the upgrade path is friendly. Ship it, share it, and watch your developer experience improve overnight.


Want to read more tutorials? Check out our latest tutorial on Building REST APIs with Hono and Bun: A Modern Alternative to Express.

Discuss Your Project with Us

We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.

Let's find the best solutions for your needs.

Related Articles

Build a Full-Stack App with Appwrite Cloud and Next.js 15

Learn how to build a complete full-stack application using Appwrite Cloud as your backend-as-a-service and Next.js 15 App Router. Covers authentication, databases, file storage, and real-time features.

30 min read·