StarterApp Docs
Applications

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.

Development

Install dependencies

From the monorepo root:

Install dependencies
pnpm install

Start the dev server

Start docs dev server
pnpm --filter docs dev

Docs run on http://localhost:9000

Create or edit documentation

All docs live in apps/docs/docs/:

index.mdx

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:

index.mdx
getting-started.mdx

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:

apps/docs/app/layout.tsx
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:

apps/docs/app/docs/layout.tsx
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:

apps/docs/app/docs/[[...slug]]/page.tsx
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:

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 component
<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 Component Example
<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 and Warning components
<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

Accordion component
<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:

Frontmatter structure
---
title: Page Title
description: Brief description for SEO and cards
---

Page content goes here...
FieldRequiredDescription
titleYesPage title shown in navigation and heading
descriptionYesBrief summary for SEO and navigation cards
iconNoIcon name from lucide-react

Search Functionality

Fumadocs includes built-in search powered by FlexSearch:

  1. Search indexes all .mdx content automatically
  2. Search is accessible via the search bar in the docs layout
  3. No configuration needed—works out of the box

The search API route is at apps/docs/app/api/search/route.ts:

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:

apps/docs/app/docs/og/[[...slug]]/route.tsx
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

Docs app 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

Build docs
pnpm --filter docs build

Deploy to Vercel

  1. Connect your GitHub repository to Vercel
  2. Set Build Command: pnpm --filter docs build
  3. Set Output Directory: apps/docs/.next
  4. 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:

source.config.ts
import { defineConfig } from "fumadocs-mdx/config";

export default defineConfig({
  mdxOptions: {
    remarkPlugins: [],
    rehypePlugins: [],
  },
});

Custom Layout Options

lib/layout.shared.ts configures the docs layout:

lib/layout.shared.ts
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.