Documentation App
Fumadocs-powered documentation site with MDX, search, and dynamic navigation
The docs app runs on Fumadocs, a modern Next.js documentation framework built for performance and developer experience. It serves the documentation you're reading right now at http://localhost:9000
in development.
Quick Start
Run pnpm --filter docs dev
to start on port 9000
MDX Components
Rich components for Cards, Steps, Callouts, and more
File Structure
How docs are organized and rendered
Search
Built-in search powered by Fumadocs
Development
Hot Reload
Changes to .mdx
files appear instantly in the browser. No need to restart the dev server.
Documentation Structure
Fumadocs uses a file-based routing system. Each .mdx
file in apps/docs/docs/
becomes a page:
Route Mapping:
index.mdx
→/docs
getting-started.mdx
→/docs/getting-started
apps/marketing.mdx
→/docs/apps/marketing
auth/setup.mdx
→/docs/auth/setup
Root Layout
The root layout (apps/docs/app/layout.tsx
) sets up Fumadocs theming and fonts:
import "./global.css";
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import type { ReactNode } from "react";
import { RootProvider } from "fumadocs-ui/provider";
const metadataBase =
process.env.NODE_ENV === "production"
? new URL("https://docs.starter.app")
: new URL("http://localhost:3002");
const geistSans = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
display: "swap",
});
const geistMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-geist-mono",
display: "swap",
});
export const metadata: Metadata = {
metadataBase,
title: {
default: "StarterApp Docs",
template: "%s | StarterApp Docs",
},
description: "Documentation for StarterApp, covering architecture, workflows, and reference guides.",
icons: { icon: "/favicon.svg", shortcut: "/favicon.ico" },
openGraph: {
images: { url: "/opengraph-image", width: 1200, height: 630, alt: "StarterApp Docs" },
},
twitter: {
card: "summary_large_image",
images: ["/opengraph-image"],
},
};
export const viewport: Viewport = {
themeColor: "#05070d",
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${geistSans.variable} ${geistMono.variable} min-h-screen antialiased font-sans`}>
<RootProvider
theme={{
attribute: "class",
defaultTheme: "dark",
enableSystem: false,
value: { dark: "dark" },
}}
>
{children}
</RootProvider>
</body>
</html>
);
}
Pro Tip
RootProvider
from Fumadocs handles theme management. The default theme is dark mode.
Docs Layout
The docs-specific layout (apps/docs/app/docs/layout.tsx
) renders the sidebar and navigation:
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import type { ReactNode } from "react";
import { baseOptions } from "@/lib/layout.shared";
import { source } from "@/lib/source";
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout tree={source.pageTree} {...baseOptions()}>
{children}
</DocsLayout>
);
}
Pro Tip
source.pageTree
is generated from the file structure in docs/
. Fumadocs automatically creates the sidebar from this tree.
Dynamic Doc Pages
Fumadocs uses a catch-all route ([[...slug]]
) to render documentation pages:
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { docsOg } from "@/lib/og";
import { getMDXComponents } from "@/mdx-components";
import { source } from "@/lib/source";
import { Cards, Card } from "fumadocs-ui/components/card";
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page";
const components = getMDXComponents();
export function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata({ params }: { params: Promise<{ slug?: string[] }> }): Promise<Metadata> {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) return {};
const title = page.data.title ?? (slug?.length ? slug[slug.length - 1] : "Docs");
const description = page.data.description;
return docsOg.withImage(slug ?? [], {
title,
description,
openGraph: { title, description, type: "article" },
twitter: { card: "summary_large_image", title, description },
});
}
export default async function DocPage({ params }: { params: Promise<{ slug?: string[] }> }) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
const MDX = page.data.body;
const title = page.data.title ?? "Docs";
const description = page.data.description;
return (
<DocsPage toc={page.data.toc}>
<DocsTitle>{title}</DocsTitle>
{description ? <DocsDescription>{description}</DocsDescription> : null}
<DocsBody className="prose prose-invert max-w-none">
<MDX components={components} />
</DocsBody>
</DocsPage>
);
}
Static Generation
generateStaticParams()
pre-renders all docs at build time. This makes the site fast and SEO-friendly.
MDX Components
The docs app uses custom MDX components defined in apps/docs/mdx-components.tsx
:
import defaultMdxComponents from "fumadocs-ui/mdx";
import { Callout } from "fumadocs-ui/components/callout";
import { Card, Cards } from "fumadocs-ui/components/card";
import { Tabs, Tab, TabsContent, TabsList, TabsTrigger } from "fumadocs-ui/components/tabs";
import { Step, Steps } from "fumadocs-ui/components/steps";
import { Accordions, Accordion } from "fumadocs-ui/components/accordion";
import { Banner } from "fumadocs-ui/components/banner";
import { InlineTOC } from "fumadocs-ui/components/inline-toc";
import { ImageZoom } from "fumadocs-ui/components/image-zoom";
import { TypeTable } from "fumadocs-ui/components/type-table";
import { Pre, CodeBlock } from "fumadocs-ui/components/codeblock";
import { Sparkles, OctagonAlert, ShieldCheck } from "lucide-react";
const Tip = ({ title = "Pro Tip", ...props }) => (
<Callout
{...props}
title={title}
type="success"
icon={<ShieldCheck className="h-6 w-6" />}
className="border-emerald-500/40 bg-emerald-500/10 py-4 text-emerald-100"
/>
);
const Warning = ({ title = "Heads up", ...props }) => (
<Callout
{...props}
title={title}
type="warning"
icon={<OctagonAlert className="h-6 w-6" />}
className="border-amber-400/40 bg-amber-400/10 py-4 text-amber-50"
/>
);
export function getMDXComponents() {
return {
...defaultMdxComponents,
Callout,
Tip,
Warning,
Cards,
Card,
Tabs,
Tab,
TabsList,
TabsTrigger,
TabsContent,
Steps,
Step,
Accordions,
Accordion,
Banner,
InlineTOC,
ImageZoom,
TypeTable,
CodeBlock,
Pre,
pre: Pre,
img: ImageZoom,
};
}
MDX Usage Examples
Cards
<Cards>
<Card title="Quick Start" href="#development">
Get started in under 5 minutes
</Card>
<Card title="Architecture" href="#architecture">
Learn how the system works
</Card>
</Cards>
Steps
<Steps>
<Step>
**Install dependencies** - From the monorepo root run pnpm install
</Step>
<Step>
**Start the server** - Execute pnpm dev to start development
</Step>
</Steps>
Callouts
<Tip title="Pro Tip">
This is a helpful tip for readers.
</Tip>
<Warning title="Important">
This requires careful attention.
</Warning>
Pro Tip
This is a helpful tip for readers.
Important
This requires careful attention.
Accordions
<Accordions>
<Accordion title="What is Fumadocs?">
Fumadocs is a modern documentation framework built on Next.js.
</Accordion>
<Accordion title="Why use MDX?">
MDX combines Markdown with React components for rich, interactive docs.
</Accordion>
</Accordions>
Code Blocks with Highlighting
```tsx title="example.tsx" {5-7}
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
```
The {5-7}
syntax highlights lines 5-7. The title="example.tsx"
adds a file name header.
Frontmatter
Every .mdx
file should include frontmatter:
---
title: Page Title
description: Brief description for SEO and cards
---
Page content goes here...
Field | Required | Description |
---|---|---|
title | Yes | Page title shown in navigation and heading |
description | Yes | Brief summary for SEO and navigation cards |
icon | No | Icon name from lucide-react |
Search Functionality
Fumadocs includes built-in search powered by FlexSearch:
- Search indexes all
.mdx
content automatically - Search is accessible via the search bar in the docs layout
- No configuration needed—works out of the box
The search API route is at apps/docs/app/api/search/route.ts
:
import { source } from "@/lib/source";
import { createSearchAPI } from "fumadocs-core/search/server";
export const { GET } = createSearchAPI("advanced", {
indexes: source.getPages().map((page) => ({
title: page.data.title,
description: page.data.description,
url: page.url,
id: page.url,
structuredData: page.data.structuredData,
})),
});
Pro Tip
Search results include page titles, descriptions, and structured content. Users can press Cmd+K
(Mac) or Ctrl+K
(Windows) to open search.
OG Image Generation
Fumadocs automatically generates Open Graph images for social sharing:
import { source } from "@/lib/source";
import { createMetadataImage } from "fumadocs-core/server";
export const GET = createMetadataImage({
imageRoute: "/docs/og",
source,
});
export function generateStaticParams() {
return source.generateParams();
}
Each doc page gets a unique OG image at /docs/og/[...slug]
.
Commands
# Development
pnpm --filter docs dev # Start dev server (port 9000)
pnpm --filter docs build # Production build
pnpm --filter docs typecheck # Type checking
pnpm --filter docs lint # Linting
Deployment
Fumadocs generates static output for deployment to Vercel, Netlify, or any static host:
Build the docs
pnpm --filter docs build
Deploy to Vercel
- Connect your GitHub repository to Vercel
- Set Build Command:
pnpm --filter docs build
- Set Output Directory:
apps/docs/.next
- Deploy
Common Issues
Writing Best Practices
Start with Value
Open paragraphs communicate value immediately. Capture attention before diving into details.
Code First
Show code examples before explanations. Developers scan for patterns, then read details.
Use MDX Components
Cards, Steps, and Callouts guide readers to critical information visually.
Keep It Understated
Technical Guide tone: precise, direct, no hype. Developers trust clarity over enthusiasm.
Advanced Configuration
Custom Source Configuration
source.config.ts
controls how Fumadocs reads and organizes docs:
import { defineConfig } from "fumadocs-mdx/config";
export default defineConfig({
mdxOptions: {
remarkPlugins: [],
rehypePlugins: [],
},
});
Custom Layout Options
lib/layout.shared.ts
configures the docs layout:
import type { DocsLayoutProps } from "fumadocs-ui/layouts/docs";
export function baseOptions(): Partial<DocsLayoutProps> {
return {
nav: {
title: "StarterApp Docs",
},
links: [
{ text: "Marketing", url: "http://localhost:3000" },
{ text: "Dashboard", url: "http://localhost:3001" },
],
};
}
Contributing to Docs
Identify gaps
Find areas where current documentation causes confusion or is missing entirely.
Write clearly
Follow the Understated Technical Guide tone: precise, direct, no hype.
Add examples
Real code examples help readers understand concepts faster than explanations.
Test locally
Run pnpm --filter docs dev
and verify your changes render correctly.
Submit PR
Open a pull request with your changes. Reviews typically complete within 2 days.