Home /

docs

Configuration

All configuration lives in a single docs.config.ts file.

Use .tsx when your config contains JSX

If your config includes JSX or React nodes, rename docs.config.ts to docs.config.tsx. This is common when setting things like nav.title, custom icons, or React-based components in the config.

The config file lives at the project root as docs.config.tsx when it contains JSX:

docs.config.tsx
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/theme";

export default defineDocs({
  entry: "docs",
  theme: fumadocs(),
});

The config file lives at the project root as docs.config.tsx when it contains JSX:

docs.config.tsx
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/theme";

export default defineDocs({
  entry: "docs",
  contentDir: "docs",
  theme: fumadocs(),
  nav: {
    title: "My Docs",
    url: "/docs",
  },
});

TanStack Start, SvelteKit, Astro, and Nuxt require contentDir (path to your markdown files) and nav (sidebar title/URL) since routing is handled differently from Next.js.

The config file lives at src/lib/docs.config.ts:

src/lib/docs.config.ts
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/svelte-theme";

export default defineDocs({
  entry: "docs",
  contentDir: "docs",
  theme: fumadocs(),
  nav: {
    title: "My Docs",
    url: "/docs",
  },
});

TanStack Start, SvelteKit, Astro, and Nuxt require contentDir (path to your markdown files) and nav (sidebar title/URL) since routing is handled differently from Next.js.

The config file lives at src/lib/docs.config.ts:

src/lib/docs.config.ts
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/astro-theme";

export default defineDocs({
  entry: "docs",
  contentDir: "docs",
  theme: fumadocs(),
  nav: {
    title: "My Docs",
    url: "/docs",
  },
});

The config file lives at the project root as docs.config.ts:

docs.config.ts
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/nuxt-theme";

export default defineDocs({
  entry: "docs",
  contentDir: "docs",
  theme: fumadocs(),
  nav: {
    title: "My Docs",
    url: "/docs",
  },
});

Config Options

OptionTypeDefaultDescription
entrystring"docs"The docs URL path prefix (e.g. "docs"/docs)
contentDirstringsame as entryPath to content files (TanStack Start, SvelteKit, Astro, Nuxt)
staticExportbooleanfalseSet true for full static builds (see Static export)
themeDocsThemeTheme preset from a theme factory
nav{ title, url }Sidebar title and base URL
githubstring | GithubConfigGitHub repo for "Edit on GitHub" links
themeToggleboolean | ThemeToggleConfigtrueLight/dark mode toggle
breadcrumbboolean | BreadcrumbConfigtrueBreadcrumb navigation
sidebarboolean | SidebarConfigtrueSidebar visibility and style
iconsRecord<string, Component>Icon registry for frontmatter icon fields
componentsRecord<string, Component>Custom MDX components and built-in overrides like HoverLink
onCopyClick(data: CodeBlockCopyData) => voidCallback when the user clicks the copy button on a code block
feedbackboolean | FeedbackConfigfalseEnd-of-page feedback prompt and callback
pageActionsPageActionsConfigCopy Markdown, Open in LLM buttons
aiAIConfigRAG-powered AI chat
apiReferenceboolean | ApiReferenceConfigfalseGenerated API reference from framework route conventions or a hosted OpenAPI JSON
i18nDocsI18nConfigQuery-param locale support and locale switcher
metadataDocsMetadataSEO metadata template
ogOGConfigDynamic Open Graph images (see API Reference)

The default MDX map already includes built-ins like Callout, Tabs, and HoverLink. Use components when you want to add your own components or override any of those defaults. If you only want to change the default props of a built-in like HoverLink, use theme.ui.components instead. See Components for examples and a live HoverLink demo.

Static export

For fully static builds (e.g. Cloudflare Pages, static hosting with no server), set staticExport: true in your config. This:

docs.config.ts
export default defineDocs({
  entry: "docs",
  staticExport: true,  // Hides search & AI; use with static hosting
  theme: fumadocs(),
  // ...
});

Use this when you deploy to a static host and don’t run a server. Search and AI require a server; with staticExport: true they are hidden so the site works without one.

API Reference

Use apiReference to generate an API reference from either your framework route handlers or a hosted OpenAPI JSON document.

Use route scanning when your API lives in the same app. Use specUrl when your backend is hosted elsewhere and already exposes an openapi.json.

Current support

apiReference is supported in Next.js, TanStack Start, SvelteKit, Astro, and Nuxt.

Routing differs by framework

In Next.js, enabling apiReference in docs.config is enough when you use withDocs(). The API reference route is generated automatically.

In TanStack Start, SvelteKit, Astro, and Nuxt, docs.config controls scanning, remote spec rendering, theming, routeRoot, and exclude, but you still need to add the framework route handler that serves /{path}.

Hosted API? Use specUrl

If your docs app and backend are deployed separately, set apiReference.specUrl to a hosted openapi.json. This keeps the API reference in your docs UI without requiring local route files for the actual backend.

docs.config.ts
export default defineDocs({
  entry: "docs",
  apiReference: {
    enabled: true,
    path: "api-reference",
    routeRoot: "api",
    exclude: ["/api/internal/health", "internal/debug"],
  },
  theme: fumadocs(),
});

If your backend lives somewhere else, point the API reference at a hosted openapi.json instead of scanning local route files:

docs.config.ts
export default defineDocs({
  entry: "docs",
  apiReference: {
    enabled: true,
    path: "api-reference",
    specUrl: "https://petstore3.swagger.io/api/v3/openapi.json",
  },
  theme: fumadocs(),
});
PropertyTypeDefaultDescription
enabledbooleantrue inside the objectEnables generated API reference pages
pathstring"api-reference"URL path where the generated reference lives
specUrlstringAbsolute URL to a hosted OpenAPI JSON document. When set, local route scanning is skipped
routeRootstring"api"Filesystem route root to scan. Bare values like "api" resolve inside app/ or src/app/; full values like "app/internal-api" are supported too
excludestring[][]Routes to omit from the generated reference. Accepts URL-style paths like "/api/hello" or route-root-relative entries like "hello" / "hello/route.ts"

When specUrl is set, routeRoot and exclude are ignored because the reference is rendered from the remote spec.

That does not change the framework routing requirements:

If you use output: "export" in Next.js, the generated API reference route is skipped automatically because it needs a server route handler.

Route conventions by framework:

Opt-in route wiring

Next.js

TanStack Start

Create src/routes/api-reference.index.ts:

src/routes/api-reference.index.ts
import { createFileRoute } from "@tanstack/react-router";
import { createTanstackApiReference } from "@farming-labs/tanstack-start/api-reference";
import docsConfig from "../../docs.config";

const handler = createTanstackApiReference(docsConfig);

export const Route = createFileRoute("/api-reference/")({
  server: {
    handlers: {
      GET: handler,
    },
  },
});

Create src/routes/api-reference.$.ts with the same handler and createFileRoute("/api-reference/$").

SvelteKit

Create src/routes/api-reference/+server.ts:

src/routes/api-reference/+server.ts
import { createSvelteApiReference } from "@farming-labs/svelte/api-reference";
import config from "$lib/docs.config";

export const GET = createSvelteApiReference(config);

Create src/routes/api-reference/[...slug]/+server.ts with the same GET export.

Astro

Create src/pages/api-reference/index.ts:

src/pages/api-reference/index.ts
import { createAstroApiReference } from "@farming-labs/astro/api-reference";
import config from "../../lib/docs.config";

export const GET = createAstroApiReference(config);

Create src/pages/api-reference/[...slug].ts with the same GET export.

Nuxt

Create server/routes/api-reference/index.ts:

server/routes/api-reference/index.ts
import { defineApiReferenceHandler } from "@farming-labs/nuxt/api-reference";
import config from "~/docs.config";

export default defineApiReferenceHandler(config);

Create server/routes/api-reference/[...slug].ts with the same default export.

Internationalization

Use i18n to enable locale-aware docs with a language selector and query-param routing like /docs?lang=en and /docs?lang=fr.

docs.config.ts
export default defineDocs({
  entry: "docs",
  theme: fumadocs(),
  i18n: {
    locales: ["en", "fr"],
    defaultLocale: "en",
  },
});

When i18n is enabled:

For content structure, place localized docs inside locale folders. For example:

internationalization.txt
docs/
  en/
    page.md
    installation/page.md
  fr/
    page.md
    installation/page.md

The generated examples and CLI scaffold use this folder structure across Next.js, TanStack Start, SvelteKit, Astro, and Nuxt.

Theme Toggle

Controls the light/dark mode switcher. Works on Next.js, TanStack Start, SvelteKit, Astro, and Nuxt.

// Show toggle (default)
themeToggle: true,

// Hide toggle, use system preference
themeToggle: false,

// Hide toggle, force dark mode
themeToggle: { enabled: false, default: "dark" },

// Show toggle with system option
themeToggle: { mode: "light-dark-system" },
PropertyTypeDefaultDescription
enabledbooleantrueShow/hide the theme toggle
default"light" | "dark" | "system""system"Forced theme when toggle is hidden
mode"light-dark" | "light-dark-system""light-dark"Toggle mode

GitHub / Edit on GitHub

Enables "Edit on GitHub" links on each docs page footer. The link appears automatically when both url and directory are set.

// Simple — just the repo URL
github: "https://github.com/my-org/my-docs",

// Monorepo — docs live in a subdirectory
github: {
  url: "https://github.com/my-org/my-monorepo",
  branch: "main",
  directory: "apps/docs/docs",
},
PropertyTypeDefaultDescription
urlstringRepository URL
branchstring"main"Branch name
directorystringSubdirectory for the content files

Controls the breadcrumb navigation above page content.

// Show breadcrumb (default)
breadcrumb: true,

// Hide breadcrumb
breadcrumb: false,
breadcrumb: { enabled: false },

Theme Configuration

Pass options to your theme preset to customize colors, typography, layout, and more:

docs.config.tsx
import { pixelBorder } from "@farming-labs/theme/pixel-border";

theme: pixelBorder({
  ui: {
    colors: { primary: "oklch(0.72 0.19 149)" },
    radius: "0px",
    layout: {
      toc: { enabled: true, depth: 3 },
    },
    sidebar: { style: "floating" },
    typography: {
      font: {
        h1: { size: "2.25rem", weight: 700 },
        body: { size: "0.975rem", lineHeight: "1.8" },
      },
    },
  },
}),
docs.config.tsx
import { pixelBorder } from "@farming-labs/theme/pixel-border";

theme: pixelBorder({
  ui: {
    colors: { primary: "oklch(0.72 0.19 149)" },
    radius: "0px",
    layout: {
      toc: { enabled: true, depth: 3 },
    },
    sidebar: { style: "floating" },
    typography: {
      font: {
        h1: { size: "2.25rem", weight: 700 },
        body: { size: "0.975rem", lineHeight: "1.8" },
      },
    },
  },
}),
src/lib/docs.config.ts
import { darksharp } from "@farming-labs/svelte-theme";

theme: darksharp({
  ui: {
    colors: { primary: "oklch(0.72 0.19 149)" },
    layout: {
      toc: { enabled: true, depth: 3 },
    },
    typography: {
      font: {
        h1: { size: "2.25rem", weight: 700 },
        body: { size: "0.975rem", lineHeight: "1.8" },
      },
    },
  },
}),
src/lib/docs.config.ts
import { darksharp } from "@farming-labs/astro-theme";

theme: darksharp({
  ui: {
    colors: { primary: "oklch(0.72 0.19 149)" },
    layout: {
      toc: { enabled: true, depth: 3 },
    },
    typography: {
      font: {
        h1: { size: "2.25rem", weight: 700 },
        body: { size: "0.975rem", lineHeight: "1.8" },
      },
    },
  },
}),
docs.config.ts
import { darksharp } from "@farming-labs/nuxt-theme";

theme: darksharp({
  ui: {
    colors: { primary: "oklch(0.72 0.19 149)" },
    layout: {
      toc: { enabled: true, depth: 3 },
    },
    typography: {
      font: {
        h1: { size: "2.25rem", weight: 700 },
        body: { size: "0.975rem", lineHeight: "1.8" },
      },
    },
  },
}),

See Colors, Typography, and Sidebar for full details.

The nav option controls the sidebar header:

nav: {
  title: "My Docs",
  url: "/docs",
},

In Next.js and TanStack Start, title can also be a React element:

nav: {
  title: (
    <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
      <Rocket size={14} />
      <span>My Docs</span>
    </div>
  ),
  url: "/docs",
},

Icons

Register icons in the config and reference them in frontmatter:

docs.config.tsx
import { Rocket, BookOpen } from "lucide-react";

icons: {
  rocket: <Rocket size={16} />,
  book: <BookOpen size={16} />,
},
page.mdx
---
icon: "rocket"
---
docs.config.tsx
import { Rocket, BookOpen } from "lucide-react";

icons: {
  rocket: <Rocket size={16} />,
  book: <BookOpen size={16} />,
},
docs/page.mdx
---
icon: "rocket"
---

SvelteKit uses a built-in icon map. Add icon to your page frontmatter:

page.md
---
icon: "rocket"
---

Built-in icons: book, terminal, rocket, settings, shield, puzzle, zap, database, key, mail, file, folder, link, lightbulb, code, users, globe, lock.

Astro uses a built-in icon map. Add icon to your page frontmatter:

page.md
---
icon: "rocket"
---

Built-in icons: book, terminal, rocket, settings, shield, puzzle, zap, database, key, mail, file, folder, link, lightbulb, code, users, globe, lock.

Nuxt uses a built-in icon map. Add icon to your page frontmatter:

page.md
---
icon: "rocket"
---

Built-in icons: book, terminal, rocket, settings, shield, puzzle, zap, database, key, mail, file, folder, link, lightbulb, code, users, globe, lock.

Code block copy callback

Run a callback when the user clicks the copy button on a code block (in addition to the default copy-to-clipboard). Useful for analytics or logging.

Next.js / TanStack Start: Set onCopyClick in defineDocs(); it is passed through to the MDX components.

docs.config.ts
import type { CodeBlockCopyData } from "@farming-labs/docs";

export default defineDocs({
  entry: "docs",
  theme: fumadocs(),
  onCopyClick(data: CodeBlockCopyData) {
    // data: { title?, content, url, language? }
    console.log("Code copied", data.title, data.language, data.url);
  },
});

SvelteKit, Astro, Nuxt: Config is serialized at build time so you cannot pass a function. Use one of these:

  1. Global callback — In a client-side script (e.g. in your layout), set window.__fdOnCopyClick__ to your function. It will be called with CodeBlockCopyData after each copy.
  2. Custom event — Listen for the fd:code-block-copy event on document or window; event.detail is the same { title?, content, url, language? } object.

The callback receives title (if the code block has a title), content (raw code), url (current page URL), and language (syntax hint) when available.

Page Feedback

Show a built-in feedback prompt at the end of each docs page. The callback receives whether the user clicked the positive or negative button, plus the current page path, slug, title, URL, and locale when available.

docs.config.ts
import type { DocsFeedbackData } from "@farming-labs/docs";

export default defineDocs({
  entry: "docs",
  theme: fumadocs(),
  feedback: {
    enabled: true,
    onFeedback(data: DocsFeedbackData) {
      console.log("Feedback", data.value, data.slug, data.url);
    },
  },
});

If you just want the UI without a callback, use feedback: true.

Next.js / TanStack Start / SvelteKit / Nuxt: feedback.onFeedback is called from the built-in page footer UI with no extra client bridge file.

Astro: feedback: true still enables the built-in UI with no extra setup. For optional client-side analytics hooks, the same payload is also emitted through window.__fdOnFeedback__ and the fd:feedback custom event.

How this docs site uses it

This website enables feedback in website/docs.config.tsx and posts the callback payload to website/app/api/feedback/route.ts, where it is inserted into Prisma with the model defined in website/prisma/schema.prisma.

website/docs.config.tsx
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/theme";
import { submitDocsFeedback } from "@/lib/submit-docs-feedback";

export default defineDocs({
  entry: "docs",
  theme: fumadocs(),
  feedback: {
    enabled: true,
    onFeedback(data) {
      void submitDocsFeedback(data);
    },
  },
});
website/app/api/feedback/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function POST(request: Request) {
  const body = await request.json();

  await prisma.docsFeedback.create({
    data: {
      value: body.value === "positive" ? "POSITIVE" : "NEGATIVE",
      url: body.url,
      pathname: body.pathname ?? body.path,
      entry: body.entry,
      slug: body.slug || null,
      title: body.title || null,
      description: body.description || null,
      locale: body.locale || null,
    },
  });

  return NextResponse.json({ ok: true }, { status: 201 });
}

Page Actions

Enable "Copy as Markdown" and "Open in LLM" buttons:

pageActions: {
  alignment: "right",
  position: "below-title",
  copyMarkdown: { enabled: true },
  openDocs: {
    enabled: true,
    providers: [
      {
        name: "ChatGPT",
        urlTemplate: "https://chatgpt.com/?q=Read+this:+{url}",
      },
    ],
  },
},

Use openDocs: true for the built-in ChatGPT + Claude provider list, or pass providers to fully customize it.

See the full Page Actions reference for all options.

Ask AI

Add a RAG-powered AI chat that lets users ask questions about your docs:

ai: {
  enabled: true,
  mode: "floating",
  floatingStyle: "full-modal",
  model: "gpt-4o-mini",
  suggestedQuestions: ["How do I get started?"],
},

The API key is read from process.env.OPENAI_API_KEY automatically.

ai: {
  enabled: true,
  mode: "floating",
  floatingStyle: "full-modal",
  model: "gpt-4o-mini",
  suggestedQuestions: ["How do I get started?"],
},

Pass the API key through src/lib/docs.server.ts:

src/lib/docs.server.ts
import { createDocsServer } from "@farming-labs/tanstack-start/server";
import docsConfig from "../../docs.config";

export const docsServer = createDocsServer({
  ...docsConfig,
  rootDir: process.cwd(),
  ai: { apiKey: process.env.OPENAI_API_KEY, ...docsConfig.ai },
});
ai: {
  enabled: true,
  mode: "floating",
  floatingStyle: "panel",
  model: "gpt-4o-mini",
  suggestedQuestions: ["How do I get started?"],
},

Pass the API key through docs.server.ts (SvelteKit requires server-only env access):

src/lib/docs.server.ts
import { env } from "$env/dynamic/private";

export const { load, GET, POST } = createDocsServer({
  ...config,
  ai: { apiKey: env.OPENAI_API_KEY, ...config.ai },
  _preloadedContent: contentFiles,
});
ai: {
  enabled: true,
  mode: "floating",
  floatingStyle: "panel",
  model: "gpt-4o-mini",
  suggestedQuestions: ["How do I get started?"],
},

Pass the API key through docs.server.ts (Astro requires server-only env access):

src/lib/docs.server.ts
export const { load, GET, POST } = createDocsServer({
  ...config,
  ai: { apiKey: import.meta.env.OPENAI_API_KEY, ...config.ai },
  _preloadedContent: contentFiles,
});
ai: {
  enabled: true,
  mode: "floating",
  floatingStyle: "panel",
  model: "gpt-4o-mini",
  suggestedQuestions: ["How do I get started?"],
},

Pass the API key through the server handler (Nuxt requires server-only env access):

server/api/docs.ts
import { defineDocsHandler } from "@farming-labs/nuxt/server";
import config from "../../docs.config";
export default defineDocsHandler({
  ...config,
  ai: { apiKey: process.env.OPENAI_API_KEY, ...config.ai },
}, useStorage);

See the full Ask AI reference for all options including custom providers, labels, system prompts, and more.

Full Example

docs.config.tsx
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/theme";

export default defineDocs({
  entry: "docs",
  theme: fumadocs({
    ui: {
      colors: { primary: "oklch(0.72 0.19 149)" },
    },
  }),
  nav: { title: "My Docs" },
  github: {
    url: "https://github.com/my-org/my-docs",
    directory: "docs",
  },
  themeToggle: { enabled: true },
  breadcrumb: { enabled: true },
  ai: {
    enabled: true,
    model: "gpt-4o-mini",
    suggestedQuestions: ["How do I get started?"],
  },
  og: {
    enabled: true,
    type: "dynamic",
    endpoint: "/api/og",
  }, 
  metadata: {
    titleTemplate: "%s – Docs",
    description: "My documentation site",
  },
});
docs.config.tsx
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/theme";

export default defineDocs({
  entry: "docs",
  contentDir: "docs",
  theme: fumadocs({
    ui: {
      colors: { primary: "oklch(0.72 0.19 149)" },
    },
  }),
  nav: { title: "My Docs", url: "/docs" },
  github: {
    url: "https://github.com/my-org/my-docs",
    directory: "docs",
  },
  themeToggle: { enabled: true },
  breadcrumb: { enabled: true },
  ai: {
    enabled: true,
    model: "gpt-4o-mini",
    suggestedQuestions: ["How do I get started?"],
  },
  metadata: {
    titleTemplate: "%s – Docs",
    description: "My documentation site",
  },
});
src/lib/docs.config.ts
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/svelte-theme";

export default defineDocs({
  entry: "docs",
  contentDir: "docs",
  theme: fumadocs({
    ui: {
      colors: { primary: "oklch(0.72 0.19 149)" },
    },
  }),
  nav: { title: "My Docs", url: "/docs" },
  github: {
    url: "https://github.com/my-org/my-docs",
    directory: "docs",
  },
  themeToggle: { enabled: true },
  breadcrumb: { enabled: true },
  ai: {
    enabled: true,
    model: "gpt-4o-mini",
    suggestedQuestions: ["How do I get started?"],
  },
  metadata: {
    titleTemplate: "%s – Docs",
    description: "My documentation site",
  },
});
src/lib/docs.config.ts
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/astro-theme";

export default defineDocs({
  entry: "docs",
  contentDir: "docs",
  theme: fumadocs({
    ui: {
      colors: { primary: "oklch(0.72 0.19 149)" },
    },
  }),
  nav: { title: "My Docs", url: "/docs" },
  github: {
    url: "https://github.com/my-org/my-docs",
    directory: "docs",
  },
  themeToggle: { enabled: true },
  breadcrumb: { enabled: true },
  ai: {
    enabled: true,
    model: "gpt-4o-mini",
    suggestedQuestions: ["How do I get started?"],
  },
  metadata: {
    titleTemplate: "%s – Docs",
    description: "My documentation site",
  },
});
docs.config.ts
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/nuxt-theme/fumadocs";

export default defineDocs({
  entry: "docs",
  contentDir: "docs",
  theme: fumadocs({
    ui: {
      colors: { primary: "oklch(0.72 0.19 149)" },
    },
  }),
  nav: { title: "My Docs", url: "/docs" },
  github: {
    url: "https://github.com/my-org/my-docs",
    directory: "docs",
  },
  themeToggle: { enabled: true },
  breadcrumb: { enabled: true },
  ai: {
    enabled: true,
    model: "gpt-4o-mini",
    suggestedQuestions: ["How do I get started?"],
  },
  metadata: {
    titleTemplate: "%s – Docs",
    description: "My documentation site",
  },
});