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:
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:
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) andnav(sidebar title/URL) since routing is handled differently from Next.js.
The config file lives at 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) andnav(sidebar title/URL) since routing is handled differently from Next.js.
The config file lives at 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:
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
| Option | Type | Default | Description |
|---|---|---|---|
entry | string | "docs" | Docs source route and folder (e.g. "docs" -> app/docs) |
docsPath | string | same as entry | Public docs route prefix in Next.js. Use "" for root-mounted docs |
contentDir | string | same as entry | Path to content files (TanStack Start, SvelteKit, Astro, Nuxt) |
staticExport | boolean | false | Set true for full static builds (see Static export) |
theme | DocsTheme | — | Theme preset from a theme factory |
nav | { title, url } | — | Sidebar title and base URL |
github | string | GithubConfig | — | GitHub repo for "Edit on GitHub" links |
themeToggle | boolean | ThemeToggleConfig | true | Light/dark mode toggle |
breadcrumb | boolean | BreadcrumbConfig | true | Breadcrumb navigation |
sidebar | boolean | SidebarConfig | true | Sidebar visibility and style |
icons | Record<string, Component> | — | Shared icon registry for frontmatter icon fields and built-ins like Prompt |
components | Record<string, Component> | — | Custom MDX components and built-in overrides like HoverLink and Prompt |
onCopyClick | (data: CodeBlockCopyData) => void | — | Callback when the user clicks the copy button on a code block |
feedback | boolean | FeedbackConfig | false for UI | Human page feedback UI; agent feedback endpoints are default-on unless opted out |
agent | DocsAgentConfig | — | Defaults for docs agent compact |
readingTime | boolean | ReadingTimeConfig | false | Opt-in estimated read-time label with optional per-page overrides |
pageActions | PageActionsConfig | — | Copy Markdown, Open in LLM buttons |
ai | AIConfig | — | RAG-powered AI chat |
search | boolean | DocsSearchConfig | true | Built-in simple search, Typesense, Algolia, or a custom adapter |
llmsTxt | boolean | LlmsTxtConfig | true | Generated root and optional section-level llms.txt files with markdown page links |
changelog | boolean | ChangelogConfig | false | Generated changelog feed and entry pages from dated MDX entries (Next.js) |
mcp | boolean | DocsMcpConfig | enabled | Built-in MCP server over stdio, /mcp, and /.well-known/mcp |
apiReference | boolean | ApiReferenceConfig | false | Generated API reference from framework route conventions or a hosted OpenAPI JSON |
sitemap | boolean | DocsSitemapConfig | true | Generated sitemap.xml, sitemap.md, and /.well-known/sitemap.md |
robots | boolean | DocsRobotsConfig | true | Runtime/generated robots.txt policy for docs and agent-readable routes |
i18n | DocsI18nConfig | — | Query-param locale support and locale switcher |
metadata | DocsMetadata | — | SEO metadata template and JSON-LD page inputs |
og | OGConfig | — | Dynamic Open Graph images (see API Reference) |
Public docs path
In Next.js, entry controls where the docs live in your app directory. docsPath controls the
public URL where readers see those pages.
The default keeps both values aligned:
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/theme";
export default defineDocs({
entry: "docs",
theme: fumadocs(),
});With the default config, app/docs/quickstart/page.mdx is available at /docs/quickstart.
Set docsPath: "" when the whole deployment is docs-only and should publish pages from the site
root:
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/theme";
export default defineDocs({
entry: "docs",
docsPath: "",
theme: fumadocs(),
});The files still live under app/docs, but readers see root URLs:
| Source file | Public URL |
|---|---|
app/docs/page.mdx | / |
app/docs/quickstart/page.mdx | /quickstart |
app/docs/reference/auth/page.mdx | /reference/auth |
This is useful for a docs subdomain. Attach docs.example.com to the docs deployment, then set
docsPath: "" so the subdomain serves clean URLs like https://docs.example.com/quickstart
instead of https://docs.example.com/docs/quickstart.
withDocs({}) reads this from docs.config.tsx, so you do not need to add custom rewrites to
next.config.ts.
Use root docs only for docs-first deployments
docsPath: "" makes the docs own / and other root-level page paths on that deployment. If the
same Next.js app also serves a marketing site at /, keep the default /docs path or use a
separate docs deployment for your subdomain.
For sidebar folder parents that have their own landing page, use
sidebar.folderIndexBehavior: "toggle" if you want parent rows to only open and close their
children instead of navigating. Use sidebar.folderIndexBehaviorOverrides when only selected
sections should behave that way. A folder landing page can also override both with frontmatter:
---
sidebar:
folderIndexBehavior: "toggle"
---If you want to remove the parent page from the sidebar entirely while keeping its child pages,
set sidebar.folderIndexBehavior: "hidden" on that folder landing page. In a flat sidebar this
renders as a plain section label with child links underneath, without a parent navigation target.
Manual visits to the hidden parent route redirect to its first visible child.
Machine-readable markdown routes
No separate config flag is required for machine-readable page markdown.
- The shared docs API supports
GET /api/docs?format=markdown&path=<slug> - Next.js
withDocs()and the generated TanStack Start, SvelteKit, Astro, and Nuxt forwarding layer serve/docs.mdand/docs/<slug>.md - Successful markdown responses include a
Link: <canonical-page-url>; rel="canonical"response header so agents can read the markdown mirror but cite the normal docs page - In Next.js, sending
Accept: text/markdownto/docs/<slug>returns the same markdown response - In Next.js, sending a non-empty
Signature-Agentheader to/docs/<slug>also returns the same markdown response - These negotiated Next.js reads are handled by the existing
/api/docsroute;withDocs()does not create a separate/api/docs/markdownwrapper - Embedded
<Agent>...</Agent>blocks stay hidden in the normal UI but are included in the.mdfallback and MCPread_page - When a page folder has
agent.md, that file is returned instead of the normal page markdown - Page frontmatter
relatedis rendered as a comma-separated markdown metadata line besideDescriptionwhen the page uses normal markdown or embedded<Agent>fallback, so agents can discover adjacent pages without scraping the UI. A siblingagent.mdremains a full override; include anyRelated:line manually insideagent.mdwhen needed.
Markdown via content negotiation
You can keep the canonical docs URL and ask for markdown with an HTTP header:
curl "https://docs.example.com/docs/installation" -H "Accept: text/markdown"
curl "https://docs.example.com/docs/installation" -H "Signature-Agent: https://chatgpt.com"In Next.js, browsers still receive HTML from /docs/installation; agents, scripts, and crawlers
that send Accept: text/markdown or Signature-Agent receive the same machine-readable output
as /docs/installation.md. The request is resolved by the shared /api/docs handler, so custom
apps should keep one docs API route instead of adding a second markdown-only wrapper. Other
adapters should use the .md URL or the API format route.
Successful markdown responses also include a canonical Link response header. The body is still
the full markdown document; the header simply tells agents that /docs/installation is the
source-of-truth URL for /docs/installation.md or a negotiated markdown read.
Negotiated markdown responses include cache-safe Vary headers: Accept for
Accept: text/markdown, and Accept, Signature-Agent for Signature-Agent.
That means a CDN can cache the HTML and markdown versions separately even though the URL is the same:
/docs/installation + normal browser headers -> cached HTML
/docs/installation + Accept: text/markdown -> cached markdown
/docs/installation + Signature-Agent: <agent-id> -> cached markdownIf a markdown request misses, the 404 response is still markdown. It points agents at the agent
discovery spec, search endpoint, and sitemap routes so they can recover instead of stopping at a
plain Not Found.
---
title: "Installation"
description: "Install the framework"
related:
- /docs/configuration
- /docs/customization/agent-primitive
---See Agent Primitive for the page-level authoring model.
Structured data
Every docs page automatically includes Schema.org JSON-LD using TechArticle,
BreadcrumbList, canonical URL, and dateModified. This is a hidden
application/ld+json script, not a visible component and not a separate route.
The page title and description come from frontmatter. The canonical URL uses
sitemap.baseUrl, llmsTxt.baseUrl, robots.baseUrl, or ai.docsUrl when one is configured.
The agent discovery JSON advertises this as capabilities.structuredData so agents know the HTML
page contains structured metadata.
The JSON-LD script is escaped before insertion so page titles and descriptions cannot break out of
the application/ld+json script tag. For filesystem-backed pages, dateModified comes from the
page source file. For preloaded Astro, SvelteKit, Nuxt, and TanStack Start builds, run
docs sitemap generate and include /.farming-labs/sitemap-manifest.json in _preloadedContent
so dateModified can reuse stable sitemap lastmod values. If no stable freshness date is
available, the runtime omits dateModified instead of using the current request time.
Sitemaps
Sitemaps are enabled by default. Add sitemap only when you want to customize the base URL, route
prefix, manifest path, or output format details:
export default defineDocs({
entry: "docs",
sitemap: {
enabled: true,
baseUrl: "https://docs.example.com",
},
});Default routes:
/sitemap.xml/sitemap.md/.well-known/sitemap.md/api/docs?format=sitemap-xml/api/docs?format=sitemap-md
For static export, run the generator before your framework build:
pnpm exec docs sitemap generateThe command writes .farming-labs/sitemap-manifest.json plus public sitemap files. It uses each
page source file's last git commit date for lastmod, with filesystem modified time as a fallback.
Preloaded adapters can also read the generated manifest for stable JSON-LD dateModified values.
Use routePrefix to move all sitemap routes together:
export default defineDocs({
sitemap: {
routePrefix: "/docs-map",
},
});That generates /docs-map/sitemap.xml, /docs-map/sitemap.md, and
/docs-map/.well-known/sitemap.md. See Sitemaps for static export,
manifest, lastmod, and output examples.
Robots.txt
Use robots when you want docs robots generate to produce an agent-friendly crawl policy:
export default defineDocs({
robots: {
enabled: true,
baseUrl: "https://docs.example.com",
ai: "allow",
},
});The generated policy explicitly allows the docs entry, .md routes, llms.txt, sitemap routes,
AGENTS.md, skill.md, MCP aliases, and agent discovery routes. It also includes common AI crawler user agents
such as GPTBot, ChatGPT-User, ClaudeBot, CCBot, and Google-Extended.
Generate the file with:
pnpm exec docs robots generateServer-rendered apps also serve /robots.txt through the shared docs handler by default when no
static file already owns that route. The generator is still the right tool for static export or when
you want to commit a policy file.
If a project already owns public/robots.txt, the command leaves it alone. Use --append to add or
update the generated block inside the existing file, or --force when you intentionally want to
replace the file:
pnpm exec docs robots generate --append
pnpm exec docs robots generate --path public/robots.txt --append
pnpm exec docs robots generate --forceUse path when the file lives somewhere custom:
export default defineDocs({
robots: {
path: "public/robots.txt",
baseUrl: "https://docs.example.com",
},
});docs doctor --agent checks the resolved robots.txt path or runtime route and flags policies that
block agent-readable docs routes or common AI crawlers.
Static export
For fully static builds (e.g. Cloudflare Pages, static hosting with no server), set staticExport: true in your config. This:
- Next.js: With
output: "export"innext.config, the/api/docsroute is not generated. The layout hides Cmd+K search and AI chat. - TanStack Start / SvelteKit / Astro / Nuxt: The layout hides the search trigger and floating AI when
staticExportis true. Omit or don’t deploy the docs API route so no server is required.
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.
Search
Search is enabled by default. If you do nothing, the docs API uses the built-in simple adapter with section-based chunking so heading matches feel more precise than page-level search.
export default defineDocs({
entry: "docs",
theme: fumadocs(),
search: true,
});Use an object when you want to switch providers or tune indexing behavior:
Simple search
export default defineDocs({
entry: "docs",
theme: fumadocs(),
search: {
provider: "simple",
maxResults: 8,
chunking: {
strategy: "section",
},
},
});Typesense
export default defineDocs({
entry: "docs",
theme: fumadocs(),
search: {
provider: "typesense",
baseUrl: process.env.TYPESENSE_URL!,
collection: "docs",
apiKey: process.env.TYPESENSE_SEARCH_API_KEY!,
adminApiKey: process.env.TYPESENSE_ADMIN_API_KEY,
mode: "hybrid",
embeddings: {
provider: "ollama",
model: "embeddinggemma",
},
},
});Use mode: "hybrid" together with embeddings when you want keyword + semantic retrieval. The
initial built-in embeddings provider is Ollama, so you can keep that stack local or self-hosted.
If you want to pre-sync the index instead of letting the first request do it lazily:
pnpm dlx @farming-labs/docs search sync --typesenseAlgolia
export default defineDocs({
entry: "docs",
theme: fumadocs(),
search: {
provider: "algolia",
appId: process.env.ALGOLIA_APP_ID!,
indexName: "docs",
searchApiKey: process.env.ALGOLIA_SEARCH_API_KEY!,
adminApiKey: process.env.ALGOLIA_ADMIN_API_KEY,
},
});You can also pre-sync Algolia outside request handling:
pnpm dlx @farming-labs/docs search sync --algoliaMCP-backed search
export default defineDocs({
entry: "docs",
theme: fumadocs(),
search: {
provider: "mcp",
endpoint: "/mcp",
},
mcp: {
enabled: true,
},
});Use MCP search when you want the docs search UI to query an MCP search_docs tool instead of a
native search backend. This is useful when another docs service or agent-facing MCP layer already
owns retrieval and ranking.
Custom adapters
import { createCustomSearchAdapter, defineDocs } from "@farming-labs/docs";
export default defineDocs({
entry: "docs",
theme: fumadocs(),
search: createCustomSearchAdapter({
name: "my-search",
async search(query, context) {
return context.documents
.filter((doc) =>
`${doc.title} ${doc.section ?? ""} ${doc.content}`.toLowerCase().includes(query.query.toLowerCase()),
)
.slice(0, query.limit ?? 10)
.map((doc) => ({
id: doc.id,
url: doc.url,
content: doc.section ? `${doc.title} — ${doc.section}` : doc.title,
description: doc.description,
type: doc.type,
section: doc.section,
}));
},
}),
});Notes:
search: falsedisables search entirelychunking.strategydefaults to"section"and can be changed to"page"for one-document-per-page indexing- Typesense and Algolia can sync documents automatically on the first search request when
adminApiKeyis present - Use
pnpm dlx @farming-labs/docs search sync --typesenseor--algoliawhen you want manual indexing as a CLI step provider: "mcp"can target a relative route like/mcpor/.well-known/mcpor an absolute remote MCP endpoint- when MCP search points at the same site's relative MCP route, the MCP
search_docstool falls back to built-in simple search to avoid recursive loops - If you use a custom Next.js docs API route, import
createDocsAPIfrom@farming-labs/next/apiand pass the whole config:createDocsAPI(docsConfig) - Search requires the docs API route; with
staticExport: truethe UI is hidden because there is no server route
@farming-labs/theme/api compatibility
The older @farming-labs/theme/api import path still works, but new Next.js apps should use
@farming-labs/next/api. The theme-level path is being kept for compatibility and will be
deprecated.
MCP Server
MCP is enabled by default. Use mcp to customize the built-in MCP server for AI tools and IDE
agents, or set enabled: false to opt out.
export default defineDocs({
entry: "docs",
mcp: {
route: "/api/docs/mcp",
},
theme: fumadocs(),
});Opt out explicitly:
export default defineDocs({
entry: "docs",
mcp: {
enabled: false,
},
theme: fumadocs(),
});Default behavior:
- Public HTTP route:
/mcp - Well-known HTTP route:
/.well-known/mcp - Canonical HTTP route:
/api/docs/mcp - stdio command:
pnpx @farming-labs/docs mcp - Built-in tools:
list_pages,get_navigation,search_docs,read_page
Framework notes:
- Next.js:
withDocs()auto-generates the default/api/docs/mcproute and public/mcpplus/.well-known/mcprewrites - TanStack Start: the current
initscaffold adds onesrc/routes/$.tspublic forwarder for/api/docs/mcp,/mcp,/.well-known/mcp,llms.txt, agent discovery, and.mdroutes - SvelteKit: the current
initscaffold adds onesrc/hooks.server.tspublic forwarder for/api/docs/mcp,/mcp,/.well-known/mcp,llms.txt, agent discovery, and.mdroutes - Astro: the current
initscaffold adds onesrc/middleware.tspublic forwarder for/api/docs/mcp,/mcp,/.well-known/mcp,llms.txt, agent discovery, and.mdroutes - Nuxt: the current
initscaffold adds oneserver/middleware/docs-public.tspublic forwarder for/api/docs/mcp,/mcp,/.well-known/mcp,llms.txt, agent discovery, and.mdroutes - Custom routes: set
mcp.routeindocs.configand update the framework public forwarder so the configured path and the actual endpoint stay aligned
See the full MCP Server guide for the route snippets and stdio usage.
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.
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:
export default defineDocs({
entry: "docs",
apiReference: {
enabled: true,
path: "api-reference",
specUrl: "https://petstore3.swagger.io/api/v3/openapi.json",
},
theme: fumadocs(),
});| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true inside the object | Enables generated API reference pages |
path | string | "api-reference" | URL path where the generated reference lives |
specUrl | string | — | Absolute URL to a hosted OpenAPI JSON document. When set, local route scanning is skipped |
routeRoot | string | "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 |
exclude | string[] | [] | 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:
- Next.js still generates the API reference route automatically with
withDocs() - TanStack Start, SvelteKit, Astro, and Nuxt still need the
/{path}route files because those routes serve the generated API reference page
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:
- Next.js:
app/api/**/route.tsorsrc/app/api/**/route.ts - TanStack Start:
src/routes/api.*.tsand nested route files under the configured route root - SvelteKit:
src/routes/api/**/+server.tsor+server.js - Astro:
src/pages/api/**/*.tsor.js - Nuxt:
server/api/**/*.tsor.js
Opt-in route wiring
Next.js
- Nothing else is required beyond
apiReferenceindocs.configandwithDocs()innext.config.ts.
TanStack Start
Create 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:
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:
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:
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.
Changelog
Use changelog to render a release feed and entry pages from dated MDX files inside your docs
content tree.
Current support
The turn-key generated changelog pages are currently wired in Next.js when you use
withDocs().
export default defineDocs({
entry: "docs",
theme: fumadocs(),
changelog: {
enabled: true,
path: "changelogs",
contentDir: "changelog",
title: "Changelog",
description: "Latest product updates and release notes.",
search: true,
},
});With the default Next.js docs tree, source entries live under
app/docs/changelog/YYYY-MM-DD/page.mdx.
That publishes:
/docs/changelogs/docs/changelogs/YYYY-MM-DD
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true inside the object | Enables generated changelog pages |
path | string | "changelog" | URL path where the changelog listing lives inside the docs layout |
contentDir | string | "changelog" | Source directory for dated entry folders like app/docs/changelog/2026-03-04/page.mdx |
title | string | "Changelog" | Listing page title |
description | string | — | Listing page description and metadata |
search | boolean | true | Show the built-in changelog search field |
actionsComponent | ReactNode | Component | — | Custom action content rendered in the changelog rail |
Useful entry frontmatter:
titledescriptionimageauthorsversiontagspinneddraft
If you pass actionsComponent, use docs.config.tsx so JSX is valid.
When you use withDocs(), the changelog route files are generated automatically. There is no
separate __changelog.generated.tsx file to maintain.
Internationalization
Use i18n to enable locale-aware docs with a language selector and query-param routing like /docs?lang=en and /docs?lang=fr.
export default defineDocs({
entry: "docs",
theme: fumadocs(),
i18n: {
locales: ["en", "fr"],
defaultLocale: "en",
},
});When i18n is enabled:
- The docs UI renders a locale selector in the sidebar footer
- Links and search navigation preserve the active
langquery parameter - Each locale can have its own content tree and sidebar structure
- The CLI can scaffold locale folders like
docs/enanddocs/frfor existing projects
For content structure, place localized docs inside locale folders. For example:
docs/
en/
page.md
installation/page.md
fr/
page.md
installation/page.mdThe 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" },| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Show/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",
},| Property | Type | Default | Description |
|---|---|---|---|
url | string | — | Repository URL |
branch | string | "main" | Branch name |
directory | string | — | Subdirectory for the content files |
Breadcrumb
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:
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" },
},
},
},
}),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" },
},
},
},
}),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" },
},
},
},
}),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" },
},
},
},
}),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" },
},
},
},
}),Navigation
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:
import { Rocket, BookOpen } from "lucide-react";
icons: {
rocket: <Rocket size={16} />,
book: <BookOpen size={16} />,
},---
icon: "rocket"
---import { Rocket, BookOpen } from "lucide-react";
icons: {
rocket: <Rocket size={16} />,
book: <BookOpen size={16} />,
},---
icon: "rocket"
---SvelteKit uses a built-in icon map. Add icon to your page frontmatter:
---
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:
---
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:
---
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.
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:
- Global callback — In a client-side script (e.g. in your layout), set
window.__fdOnCopyClick__to your function. It will be called withCodeBlockCopyDataafter each copy. - Custom event — Listen for the
fd:code-block-copyevent ondocumentorwindow;event.detailis 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.
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.
Feedback is not prompt context
Page feedback is emitted as untrusted user data. The framework does not add comments from this
endpoint to Ask AI prompts, docs search context, or llms.txt. If your onFeedback callback
forwards feedback to an LLM, issue triager, or support workflow, label it as user-provided text
and keep your own validation, length limits, and rate limiting in place.
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.
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);
},
},
});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 });
}Agent Compaction
Use agent.compact to set defaults for docs agent compact, which generates sibling agent.md
files from resolved docs pages.
export default defineDocs({
entry: "docs",
agent: {
compact: {
apiKeyEnv: "TOKEN_COMPANY_API_KEY",
model: "bear-1.2",
aggressiveness: 0.3,
protectJson: true,
},
},
});If you want to reference the key directly from docs.config.tsx, this is supported too:
export default defineDocs({
entry: "docs",
agent: {
compact: {
apiKey: process.env.TOKEN_COMPANY_API_KEY,
},
},
});Supported fields:
apiKeyapiKeyEnvbaseUrlmodelaggressivenessmaxOutputTokensminOutputTokensprotectJson
What changes after compaction
docs agent compact creates missing agent.md files and overwrites existing ones. The written
agent.md becomes the machine-readable source for .md routes,
GET /api/docs?format=markdown&path=<slug>, and MCP read_page(). The human docs UI still
renders the normal page.
Handwritten agent.md files are also valid. They count as explicit machine context in
docs doctor --agent, but their freshness is reported as unknown because there is no generated
provenance block to compare against the page source.
The CLI loads .env and .env.local before resolving the key, so TOKEN_COMPANY_API_KEY works
out of the box. Use positional page args by default:
pnpm exec docs agent compact installation configuration
pnpm exec docs agent compact installation --dry-run
pnpm exec docs agent compact --all
pnpm exec docs agent compact --changedPer-page compaction budgets live in frontmatter:
---
title: "Installation"
description: "Install the framework"
agent:
tokenBudget: 777
---Notes:
agent.tokenBudgetoverrides globalagent.compact.maxOutputTokensdefaults and CLI--max-output-tokensfor that one page- if a sibling
agent.mdalready exists,docs agent compactcompacts that file - if no
agent.mdexists, the command compacts the generated machine-readable page output and then writes a new siblingagent.md docs agent compact --changedonly processes docs pages changed in the current git working tree, including staged, unstaged, and untracked docs changesdocs agent compact --stale --include-missingwill also create missingagent.mdfiles for pages that defineagent.tokenBudget- if inherited
minOutputTokenswould be greater than the page budget, the CLI clamps it down to that page budget before calling the compression API
See CLI for the command surface and Token Efficiency for
when to use compaction instead of writing agent.md by hand.
Agent Feedback Endpoints
Machine-readable feedback routes are enabled by default for coding agents and docs-aware automation.
The default submit route is a no-op 202 response until you add a callback. This is separate from
the built-in page footer UI, which remains opt-in.
Use feedback.agent only when you want to customize the callback, routes, schema, or explicitly opt
out.
Agents should discover the configured routes first with GET /.well-known/agent.json.
GET /.well-known/agent is the fallback public alias, and GET /api/docs/agent/spec is the
canonical framework route. Generated projects wire all three to the same JSON. The spec includes
site identity, locale config, capability flags, the search endpoint, markdown route pattern,
Accept: text/markdown contract, Signature-Agent support, API, public, and section-level
llms.txt routes, sitemap routes, the robots.txt route, generated AGENTS.md metadata, root
skill.md metadata, Skills CLI install metadata, MCP public/well-known endpoints and enabled tools,
and the active agent feedback schema and submit endpoints. Use docs robots generate when a static export or committed policy
file should own the advertised route.
export default defineDocs({
entry: "docs",
feedback: {
agent: {
async onFeedback(data) {
console.log(data.context?.source, data.payload);
},
},
},
});Default behavior:
GET /.well-known/agent.jsonis the preferred public agent discovery URLGET /.well-known/agentis the public fallback discovery URLGET /api/docs/agent/specreturns the canonical framework discovery documentGET /AGENTS.mdserves a rootAGENTS.mdorAGENT.mdfile when present, and otherwise returns a generated fallbackGET /.well-known/AGENTS.mdandGET /api/docs?format=agentsreturn the same agent instructionsGET /skill.mdserves a rootskill.mdfile when present, and otherwise returns a generated fallbackGET /.well-known/skill.mdandGET /api/docs?format=skillreturn the same skill document- the discovery JSON advertises
agentSpecDefault,agentSpecFallback,agents.file,agents.route,skills.file,skills.route, the root and sectionllms.txtroutes,robots.route, and sitemap routes when enabled GET /api/docs/agent/feedback/schemareturns the feedback schemaPOST /api/docs/agent/feedbackaccepts{ context?, payload }; withoutonFeedbackit returns{ ok: true, handled: false }- non-Next server adapters also expose the same contract through
GET /api/docs?feedback=agent&schema=1andPOST /api/docs?feedback=agent - the shared
/api/docshandler is still the source of truth - in Next.js,
withDocs()adds the public rewrites automatically feedback.agentalone does not enable the human footer UI- set
feedback: falseorfeedback: { agent: false }to opt out of the agent feedback routes
Safe by default
Agent feedback submissions are validated data passed to feedback.agent.onFeedback; they are not
inserted into the Ask AI system prompt or retrieved as documentation context. Treat the payload as
untrusted if your callback stores it, opens issues, posts to chat, or sends it to another model.
Default request shape:
{
"context": {
"page": "/docs/installation",
"url": "https://docs.example.com/docs/installation.md",
"slug": "installation",
"locale": "en",
"source": "md-route"
},
"payload": {
"task": "install docs in an existing Next.js app",
"understanding": "partial",
"outcome": "implemented",
"confidence": 0.78,
"neededCodeReading": true,
"missingContext": ["how the markdown route is resolved"],
"docIssues": ["command example was unclear"],
"suggestedImprovement": "Add one sentence about the rewrite behavior."
}
}Customize the public route or the payload schema when needed:
export default defineDocs({
entry: "docs",
feedback: {
agent: {
route: "/internal/docs/agent-feedback",
schemaRoute: "/internal/docs/agent-feedback/schema",
schema: {
type: "object",
additionalProperties: false,
properties: {
task: { type: "string" },
outcome: { type: "string" },
confidence: { type: "number", minimum: 0, maximum: 1 }
},
required: ["task", "outcome"]
}
}
}
});Quick test:
curl "http://127.0.0.1:3000/api/docs/agent/feedback/schema"
curl -X POST "http://127.0.0.1:3000/api/docs/agent/feedback" \
-H "content-type: application/json" \
-d '{"payload":{"task":"demo","outcome":"implemented"}}'Reading Time
Use readingTime when you want an estimated label like 5 min read on docs pages.
export default defineDocs({
entry: "docs",
readingTime: {
enabled: true,
wordsPerMinute: 220,
},
});Default behavior:
- the feature is opt-in
- the estimate uses
220words per minute unless you override it - fenced code blocks, inline code, links, images, and raw HTML are stripped before counting words
- changelog pages do not render the reading-time label
- page frontmatter wins when
readingTimeis present, even if the global config is disabled
Per-page overrides live in frontmatter:
---
title: "Installation"
description: "Get up and running"
readingTime: false
---You can also opt a page in explicitly when the global config is off:
---
title: "API Guide"
readingTime: true
---And you can pin an exact number:
---
title: "Long Guide"
readingTime: 12
---That gives you a simple override model:
- global
readingTime.enabled: true-> pages get a computed label by default - page
readingTime: false-> hide the label for that page - page
readingTime: true-> force a computed label for that page - page
readingTime: 12-> force an exact12 min readlabel for that page
Placement with page actions
Reading time follows the same title-area slot as page actions. With
pageActions.position: "above-title", it renders directly under the action row. With
pageActions.position: "below-title", it stays grouped in the below-title metadata area,
including when page actions are disabled but the slot is still configured.
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.
If you use the built-in Prompt component, you can also add promptUrlTemplate to a provider so the same provider name and icon can open prompt text directly:
pageActions: {
openDocs: {
enabled: true,
providers: [
{
name: "Cursor",
urlTemplate: "https://cursor.com/link/prompt?text=Read+this+documentation:+{url}",
promptUrlTemplate: "https://cursor.com/link/prompt?text={prompt}",
},
],
},
},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:
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):
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):
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):
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
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",
},
});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",
},
});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",
},
});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",
},
});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",
},
});How is this guide?