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(),
});

Config Options

OptionTypeDefaultDescription
entrystring"docs"Docs source route and folder (e.g. "docs" -> app/docs)
docsPathstringsame as entryPublic docs route prefix in Next.js. Use "" for root-mounted docs
contentDirstringsame as entryPath to content files (TanStack Start, SvelteKit, Astro, Nuxt)
staticExportbooleanfalseSet true for full static builds (see Static export)
themeDocsTheme—Theme preset from a theme factory
nav{ title, url }—Sidebar title and base URL
githubstring | GithubConfig—GitHub repo for "Edit on GitHub" links
themeToggleboolean | ThemeToggleConfigtrueLight/dark mode toggle
breadcrumbboolean | BreadcrumbConfigtrueBreadcrumb navigation
sidebarboolean | SidebarConfigtrueSidebar visibility and style
iconsRecord<string, Component>—Shared icon registry for frontmatter icon fields and built-ins like Prompt
componentsRecord<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
codeBlocksDocsCodeBlocksConfig—Validate fenced MDX code blocks with metadata planning and optional sandbox execution
feedbackboolean | FeedbackConfigfalse for UIHuman page feedback UI; agent feedback endpoints are default-on unless opted out
agentDocsAgentConfig—Compaction, golden evaluations, reusable skills, and optional A2A metadata
reviewboolean | DocsReviewConfigtrueDocs Review CI workflow generation, scoring, and rule severity
readingTimeboolean | ReadingTimeConfigfalseOpt-in estimated read-time label with optional per-page overrides
pageActionsPageActionsConfig—Copy Markdown, Open in LLM buttons
aiAIConfig—RAG-powered AI chat
searchboolean | DocsSearchConfigtrueBuilt-in simple search, Typesense, Algolia, or a custom adapter
cloudDocsCloudConfig—Docs Cloud API key env, deploy, and publish defaults mirrored into docs.json
llmsTxtboolean | LlmsTxtConfigtrueGenerated root and optional section-level llms.txt files with markdown page links
changelogboolean | ChangelogConfigfalseGenerated changelog feed and entry pages from dated MDX entries (Next.js)
mcpboolean | DocsMcpConfigenabledBuilt-in MCP server over stdio, /mcp, and /.well-known/mcp
apiReferenceboolean | ApiReferenceConfigfalseGenerated API reference from framework route conventions or local/remote OpenAPI JSON and YAML
sitemapboolean | DocsSitemapConfigtrueGenerated sitemap.xml, sitemap.md, /docs/sitemap.md, and /.well-known/sitemap.md
robotsboolean | DocsRobotsConfigtrueRuntime/generated robots.txt policy for docs and agent-readable routes
i18nDocsI18nConfig—Query-param locale support and locale switcher
metadataDocsMetadata—SEO metadata template and JSON-LD page inputs
ogOGConfig—Dynamic Open Graph images (see API Reference)

Code block validation

codeBlocks.validate lets the CLI inspect fenced MDX code blocks, build an execution plan from metadata, and optionally run executable snippets with a local or sandbox runner.

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

export default defineDocs({
  entry: "docs",
  codeBlocks: {
    validate: {
      planner: {
        provider: "openai",
        model: "gpt-4.1-mini",
        apiKeyEnv: "OPENAI_API_KEY",
      },
      runner: {
        provider: "vercel-sandbox",
        tokenEnv: "VERCEL_TOKEN",
      },
      envFile: [".env.local", ".env.test", ".env"],
      env: {
        OPENAI_API_KEY: "OPENAI_TEST_API_KEY",
      },
      missingEnv: "skip",
    },
  },
});

Use metadata on the code fence to tell agents and validators how a snippet should be treated:

page.mdx
```ts title="app/api/chat/route.ts" framework="nextjs" packageManager="pnpm" env="OPENAI_API_KEY" runnable
const apiKey = process.env.OPENAI_API_KEY;
```

Small runnable blocks make the check easy to dogfood:

agent-context.json
{
  "framework": "nextjs",
  "packageManager": "pnpm",
  "agentReady": true
}
codeblocks-smoke.js
const metadata = { framework: "nextjs", runnable: true };

if (!metadata.runnable) {
  throw new Error("Expected runnable metadata");
}

console.log("metadata ok");
codeblocks-smoke.sh
test "docs" = "docs"
echo "shell ok"

Run the validator locally:

pnpm exec docs codeblocks validate --plan
pnpm exec docs codeblocks validate

The planner never writes secrets into docs. The env map means the snippet can keep using process.env.OPENAI_API_KEY, while validation injects that runtime variable from OPENAI_TEST_API_KEY. For Vercel Sandbox, VERCEL_TOKEN is enough in most projects; the runner uses .vercel/project.json when present and can auto-discover an accessible Vercel project from the token.

Runner providers are explicit, so a remote sandbox never silently falls back to local execution:

docs.config.ts
codeBlocks: {
  validate: {
    runner: { provider: "vercel-sandbox", tokenEnv: "VERCEL_TOKEN" },
  },
}
docs.config.ts
codeBlocks: {
  validate: {
    runner: { provider: "e2b", tokenEnv: "E2B_API_KEY" },
  },
}
docs.config.ts
codeBlocks: {
  validate: {
    runner: {
      provider: "daytona",
      tokenEnv: "DAYTONA_API_KEY",
      targetEnv: "DAYTONA_TARGET",
      apiUrlEnv: "DAYTONA_API_URL",
    },
  },
}

E2B uses the e2b package and Daytona uses @daytona/sdk. Install the provider SDK in the project that runs the CLI when you choose one of those runners.

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:

docs.config.tsx
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:

docs.config.tsx
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 filePublic 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:

page.mdx
---
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.md and /docs/<slug>.md
  • Successful markdown responses include canonical Link and Content-Location response headers, plus cache validators and locale metadata when available
  • In Next.js, sending an unambiguous Accept: text/markdown request to /docs/<slug> returns the same markdown response; mixed ranges that also advertise text/html, text/*, or */* remain HTML
  • In Next.js, sending a non-empty Signature-Agent header to /docs/<slug> also returns the same markdown response
  • In Next.js, known AI user agents and conservative bot-like agent heuristics also receive markdown from normal docs URLs
  • These negotiated Next.js reads are handled by the existing /api/docs route; withDocs() does not create a separate /api/docs/markdown wrapper
  • Markdown responses start with YAML frontmatter for title, optional description, canonical_url, markdown_url, and last_updated when a page freshness date is known
  • Successful markdown page responses append a ## Sitemap footer that links agents back to the configured markdown sitemap routes
  • Missing markdown pages return actionable markdown with HTTP 404, closest-match suggestions, recovery instructions, discovery links, and sitemap links
  • Very high-confidence missing slugs redirect to the closest .md page
  • Content is shared by default. <Agent> and <Audience only="agent"> add agent-only content; <Human> and <Audience only="human"> add human-only content. Rendered HTML and public search use the human projection, while Markdown, Ask AI, MCP, llms-full.txt, and static agent exports use the agent projection. Compact llms.txt and sitemaps contain discovery/route metadata rather than page bodies.
  • Audience.only must be a static "human" or "agent" literal. docs review reports dynamic expressions and spread props because build-time exports cannot resolve them consistently.
  • When a page folder has agent.md, that file is returned instead of the audience-projected page source
  • Page frontmatter related is rendered as a comma-separated markdown metadata line beside Description when the page uses normal markdown or an audience-projected fallback, so agents can discover adjacent pages without scraping the UI. A sibling agent.md remains the page-content override; include any Related: line manually inside agent.md when 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"
curl "https://docs.example.com/docs/installation" -H "User-Agent: ClaudeBot/1.0"

In Next.js, browsers still receive HTML from /docs/installation; agents, scripts, and crawlers that send an unambiguous Accept: text/markdown, send Signature-Agent, identify as a known AI user agent, or match a conservative bot-like agent heuristic 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 can negotiate by Accept, or use the .md URL or API format route directly.

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.

Content-Location identifies the selected .md representation. Content-Language is included when the page locale is known. Every successful response has an ETag. Last-Modified is only included when the adapter knows the exact source timestamp; a date-only last_updated value is not promoted to midnight. Matching If-None-Match or If-Modified-Since requests receive HTTP 304 when the corresponding validator is available.

The markdown body starts with YAML frontmatter. Agents can read canonical_url, markdown_url, title, description, and last_updated before parsing the page body.

Negotiated markdown responses include cache-safe Vary headers: Accept for Accept: text/markdown, Accept, Signature-Agent for Signature-Agent, User-Agent for known AI user agents, and User-Agent, Sec-Fetch-Mode for heuristic agent detection. The shared handler and non-Next request handlers honor q values when an Accept header lists both HTML and markdown. Next.js canonical-page negotiation is implemented as a static rewrite, so it intentionally does not attempt arbitrary numeric q comparisons: any text/html, text/*, or */* range keeps the canonical URL on HTML. Use /docs/installation.md or /api/docs?format=markdown&path=installation when the Markdown representation must be exact.

That means a CDN can cache the HTML and markdown versions separately even though the URL is the same:

| apiCatalog | boolean | true | When false, suppresses Allow: /.well-known/api-catalog from the generated robots.txt policy and removes that route from discovery links. Has no effect on hashed Agent Skills routes. |

/docs/installation + normal browser headers      -> cached HTML
/docs/installation + Accept: text/markdown       -> cached markdown
/docs/installation + Signature-Agent: <agent-id> -> cached markdown
/docs/installation + User-Agent: ClaudeBot/1.0   -> cached markdown

If a markdown request misses, the recovery response is still markdown and returns HTTP 404 unless a very high-confidence match redirects to the corrected .md page. The body lists closest matches, recovery steps, the agent discovery spec, search endpoint, and sitemap routes so agents can recover instead of stopping at a plain Not Found.

page.mdx
---
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:

docs.config.ts
export default defineDocs({
  entry: "docs",
  sitemap: {
    enabled: true,
    baseUrl: "https://docs.example.com",
  },
});

Default routes:

  • /sitemap.xml
  • /sitemap.md
  • /docs/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:

terminal
pnpm exec docs sitemap generate

The 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:

docs.config.ts
export default defineDocs({
  sitemap: {
    routePrefix: "/docs-map",
  },
});

That generates /docs-map/sitemap.xml, /docs-map/sitemap.md, and /docs-map/.well-known/sitemap.md. The default /docs/sitemap.md alias is emitted only when routePrefix is empty. 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:

docs.config.ts
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.

Two allow rules are conditional: Allow: /.well-known/api-catalog is omitted when llmsTxt.apiCatalog: false or staticExport: true, and Allow: /.well-known/agent-card.json is included only when agent.a2a is configured. Generate the file with:

terminal
pnpm exec docs robots generate

Server-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:

terminal
pnpm exec docs robots generate --append
pnpm exec docs robots generate --path public/robots.txt --append
pnpm exec docs robots generate --force

Use path when the file lives somewhere custom:

docs.config.ts
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" in next.config, the /api/docs route 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 staticExport is true. Omit or don’t deploy the docs API route so no server is required.
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.

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.

docs.config.ts
export default defineDocs({
  entry: "docs",
  theme: fumadocs(),
  search: true,
});

The same route supports two content projections. GET /api/docs?query=<query> remains the human default and searches shared plus human-only content. Use the exact lowercase GET /api/docs?query=<query>&audience=agent form to search shared plus agent-only content. audience=human, an omitted value, and invalid values all stay on the human projection.

Audience is not access control

The query parameter selects a public content projection; it does not authenticate or authorize the caller. Never place secrets or private data in audience-specific docs content.

Explain retrieval decisions

Use the exact lowercase explain=true opt-in when an agent needs to understand why a result was selected:

HTTP
GET /api/docs?query=deploy&audience=agent&response=structured&explain=true

Each result then includes a bounded explanation with:

  • matched query terms and their title, section, description, content, or URL fields
  • the final provenance scope plus framework, version, package, and tag filter decisions
  • unresolved or unverifiable ambiguity, or the filter that resolved it
  • the one-based rank, ranking strategy, and concrete ranking reasons

The default and explain=false omit this field, including from legacy array responses. For Typesense, Algolia, MCP, and custom adapters, the explanation identifies provider-owned ordering without claiming access to private provider ranking internals.

The built-in MCP tool exposes the same behavior:

search_docs arguments
{
  "query": "deploy",
  "audience": "agent",
  "explain": true
}

Use an object when you want to switch providers or tune indexing behavior:

docs.config.ts
export default defineDocs({
  entry: "docs",
  theme: fumadocs(),
  search: {
    provider: "simple",
    maxResults: 8,
    chunking: {
      strategy: "section",
    },
  },
});

Typesense

docs.config.ts
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:

terminal
pnpm dlx @farming-labs/docs search sync --typesense

Algolia

docs.config.ts
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:

terminal
pnpm dlx @farming-labs/docs search sync --algolia
docs.config.ts
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

docs.config.ts
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: false disables search entirely
  • chunking.strategy defaults to "section" and can be changed to "page" for one-document-per-page indexing
  • custom adapters receive the resolved value as query.audience and context.audience, and context.documents uses the matching content projection
  • custom adapters should apply query.audience while filtering and ranking provider results
  • Typesense and Algolia can sync documents automatically on the first search request when adminApiKey is present
  • hosted index sync remains human-projected; requesting audience=agent does not replace the public index with agent-only text
  • Use pnpm dlx @farming-labs/docs search sync --typesense or --algolia when you want manual indexing as a CLI step
  • provider: "mcp" can target a relative route like /mcp or /.well-known/mcp or an absolute remote MCP endpoint
  • same-origin MCP search_docs routes receive the resolved audience; remote endpoints and custom tool names must set forwardAudience: true after supporting that argument; human-projection requests fall back to local search until then
  • when MCP search points at the same site's relative MCP route, the MCP search_docs tool falls back to built-in simple search to avoid recursive loops
  • If you use a custom Next.js docs API route, import createDocsAPI from @farming-labs/next/api and pass the whole config: createDocsAPI(docsConfig)
  • Search requires the docs API route; with staticExport: true the 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.

docs.config.ts
export default defineDocs({
  entry: "docs",
  mcp: {
    route: "/api/docs/mcp",
  },
  theme: fumadocs(),
});

Opt out explicitly:

docs.config.ts
export default defineDocs({
  entry: "docs",
  mcp: {
    enabled: false,
  },
  theme: fumadocs(),
});

Streamable HTTP MCP is public by default. Add security.authenticate only when the endpoint needs access control. To support OAuth-aware MCP clients, configure protectedResource alongside the callback:

docs.config.ts
export default defineDocs({
  mcp: {
    security: {
      protectedResource: {
        authorizationServers: ["https://auth.example.com"],
        scopesSupported: ["docs:read"],
        requiredScopes: ["docs:read"],
      },
      async authenticate({ request, resource }) {
        const user = await authenticateRequest(request, { audience: resource });
        return user ? { id: user.id, scopes: user.scopes } : null;
      },
    },
  },
});

authorizationServers must be non-empty and contain HTTPS issuer URLs without query or fragment (loopback HTTP is accepted for development). scopesSupported is discovery metadata, while requiredScopes is enforced against the principal returned by authenticate. A null result receives a 401 Bearer challenge that links to public RFC 9728 metadata; a principal missing a required scope receives 403 insufficient_scope. If the callback returns a Web Response, that response is preserved and must include its own challenge when needed. The framework does not issue or validate tokens; the callback must validate signature, issuer, expiry, the exact callback resource audience, and derive scopes from the verified token. The callback request retains the real incoming URL and query string.

The default metadata routes are /.well-known/oauth-protected-resource/mcp, /.well-known/oauth-protected-resource/.well-known/mcp, and /.well-known/oauth-protected-resource/api/docs/mcp. The last path follows a custom mcp.route. Protected-resource discovery is inactive without authenticate, and stdio is unaffected. Use HTTPS for hosted MCP, authorization-server, and authentication-documentation URLs. Treat each MCP alias as a distinct OAuth resource identifier and validate token audiences against the exact endpoint used by the client. On Next.js, do not combine protected MCP with basePath; host the protected routes at the origin root or an edge proxy. Inline MCP objects are detected automatically. For imported, spread, or shorthand MCP configuration, import docsConfig in next.config.ts and call withDocs(nextConfig, docsConfig) so routing and the basePath check use the live object.

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_docs, list_pages, list_tasks, read_task, get_navigation, search_docs, read_page, get_code_examples, get_config_schema, get_context

Useful built-in tool calls:

  • list_docs returns page summaries grouped by docs section, with optional section filtering
  • list_tasks filters actionable contract summaries by query/framework/version/package; read_task returns the full normalized contract as structured output plus a JSON text fallback
  • list_docs and list_pages expose only hasContract, task, outcome, and appliesTo in their contract summaries
  • get_code_examples returns fenced code blocks plus parsed metadata such as title, framework, packageManager, and runnable
  • get_config_schema returns structured docs.config.ts option metadata for agents editing config safely
  • get_context returns deterministic section-level context with source anchors and an explicit conservative UTF-8 byte ceiling for the complete assembled response

Framework notes:

  • Next.js: withDocs() auto-generates the default /api/docs/mcp route and public /mcp plus /.well-known/mcp rewrites

When the Farm application lives inside a monorepo or needs to compile documentation from a shared location outside its own directory, pass contentRoots to withDocs:

farm.config.ts
import { withDocs } from "@farming-labs/farmjs";

export default withDocs({}, {
  contentRoots: ["../shared-docs"],
});

contentRoots accepts an array of paths relative to the Farm application root. Each resolved path is added to the dev server's server.fs.allow list so the MDX plugin can compile content from repository-owned or shared documentation directories.

  • TanStack Start: the current init scaffold adds one src/routes/$.ts public forwarder for /api/docs/mcp, /mcp, /.well-known/mcp, llms.txt, agent discovery, and .md routes
  • SvelteKit: the current init scaffold adds one src/hooks.server.ts public forwarder for /api/docs/mcp, /mcp, /.well-known/mcp, llms.txt, agent discovery, and .md routes
  • Astro: the current init scaffold adds one src/middleware.ts public forwarder for /api/docs/mcp, /mcp, /.well-known/mcp, llms.txt, agent discovery, and .md routes
  • Nuxt: the current init scaffold adds one server/middleware/docs-public.ts public forwarder for /api/docs/mcp, /mcp, /.well-known/mcp, llms.txt, agent discovery, and .md routes
  • Custom routes: set mcp.route in docs.config and 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.

Docs Review CI

Docs Review is enabled by default. During next dev or next build, withDocs() creates .github/workflows/docs-review.yml when the workflow does not already exist. The generated workflow is intentionally tiny: it calls the hosted reusable workflow at farming-labs/docs/.github/workflows/docs-review-reusable.yml@main and passes the docs config path, package manager, check name, and working directory as inputs. In monorepos, the file is written at the repository root and the job runs from the docs app directory.

docs.config.ts
export default defineDocs({
  entry: "docs",
  review: true,
  theme: fumadocs(),
});

The generated wrapper only triggers on docs-related PR changes such as docs.config.*, app/docs/**, src/app/docs/**, content/docs/**, and the workflow files themselves. By default it runs in warn mode, posts GitHub annotations, uses docs-review as the GitHub check name, and keeps CI green.

Use block mode when the team wants docs PRs to fail below the configured score threshold or when error-level findings exist:

docs.config.ts
export default defineDocs({
  entry: "docs",
  review: {
    ci: {
      name: "agent-docs-review",
      mode: "block",
    },
    score: { threshold: 90 },
    rules: {
      brokenLinks: "error",
      frontmatter: "error",
      codeFenceMetadata: "warn",
      agentContext: "warn",
      commandHealth: "warn",
      relatedCoverage: "suggestion",
      configConfidence: "warn",
      agentSurfaceDrift: "error",
      goldenTasks: "warn",
      agentSkills: "warn",
    },
  },
  theme: fumadocs(),
});

agentContext is corpus-aware: it detects exact duplicates, repeated boilerplate, generic Agent blocks, missing prerequisites/results/recovery, and ambiguous framework or version guidance. commandHealth inspects commands without running them or making network requests. It resolves package scripts from an enclosing workspace and recognizes constrained HTTP probes, safe shell built-ins, and supported Agent Skills or Claude MCP setup forms; unknown or compound commands stay unverified. configConfidence reports when docs.config could only be read through partial static parsing, while agentSurfaceDrift compares resolved configuration, discovery metadata, structured contracts, MCP tools, routes, and the public config schema. goldenTasks reports both failed evaluations and an unmeasured task suite. agentSkills reports oversized skill instructions, broken or deep references, missing compatibility requirements, and scripts without dependency or validation guidance.

Set review: false to opt out of review and workflow generation.

API Reference

Use apiReference to generate an API reference from either your framework route handlers or a local/remote OpenAPI JSON or YAML document.

Use route scanning when your API lives in the same app and no contract exists. Use specUrl for an existing project-relative file, absolute URL, or request-relative URL.

Current support

apiReference is supported in Next.js, TanStack Start, Farm.js, 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.

Farm.js serves the route through withDocs(). 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}.

Native renderer by default

The built-in "farming-labs" renderer is the default on every framework. It renders the shared normalized operation model with searchable navigation, version switching, copyable cURL examples, schemas, parameters, responses, and accessible light/dark controls. Set renderer: "scalar" on any framework or renderer: "fumadocs" on Next.js to keep either bundled alternate renderer. Next.js can also use fumadocsRenderer() to pass typed playground and runtime options to the Fumadocs renderer.

Configure the Fumadocs playground on Next.js without mixing renderer behavior into theme:

docs.config.ts
import { defineDocs } from "@farming-labs/docs";
import { fumadocsRenderer } from "@farming-labs/next/fumadocs-renderer";
import { fumadocs } from "@farming-labs/theme";
import { authProviders } from "./app/api-reference/auth-providers";

export default defineDocs({
  entry: "docs",
  theme: fumadocs(),
  apiReference: {
    enabled: true,
    specUrl: "./openapi.yaml",
    renderer: fumadocsRenderer({
      playground: {
        enabled: true,
        authProviders,
      },
      oauthRedirectUrl: "/api/docs/oauth",
      proxyUrl: "/api/docs/proxy",
      showResponseSchema: true,
      schemaUI: { showExample: true },
    }),
  },
});

fumadocsRenderer() accepts the upstream Fumadocs createOpenAPIPage() options plus proxyUrl and disableCache from createOpenAPI(). authProviders, field transforms, and custom renderers run in the browser; keep them in client-safe modules and never include secrets. A configured proxy or OAuth redirect URL must point to a route implemented by the application.

Existing contract? Use specUrl

Set apiReference.specUrl to a checked-in file such as ./openapi.yaml, a hosted JSON/YAML URL, or a request-relative URL. This keeps the API reference in your docs UI without requiring framework route scanning.

Validated and bounded

Contract sources must use Swagger 2.0 or OpenAPI 3.0.x, 3.1.x, or 3.2.x. Farming Labs rejects malformed operations and duplicate operationId values before rendering. Local and remote $ref documents are bundled once per build, with at most 32 documents and 5 MiB per document. Remote requests time out after 10 seconds.

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 you already keep an OpenAPI contract in the repository, point the API reference at it instead of scanning framework route files:

docs.config.ts
export default defineDocs({
  entry: "docs",
  apiReference: {
    enabled: true,
    path: "api-reference",
    specUrl: "./openapi.yaml",
  },
  theme: fumadocs(),
});

The same option accepts a hosted document:

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(),
});

For multiple contract versions, use stable version keys and choose one explicit default. Each version is rendered at /{path}/{version} and served from the shared schema endpoint with ?format=openapi&version={version}:

docs.config.ts
export default defineDocs({
  entry: "docs",
  apiReference: {
    enabled: true,
    path: "api-reference",
    versions: {
      v1: { specUrl: "./openapi/v1.yaml", label: "Version 1" },
      v2: {
        specUrl: "./openapi/v2.yaml",
        label: "Version 2",
        overlays: ["./openapi/public.overlay.yaml"],
      },
    },
    defaultVersion: "v2",
  },
  theme: fumadocs(),
});
PropertyTypeDefaultDescription
enabledbooleantrue inside the objectEnables generated API reference pages
renderer"farming-labs" | "scalar" | "fumadocs""farming-labs"UI renderer. Scalar works on every framework; Fumadocs is available on Next.js
pathstring"api-reference"URL path where the generated reference lives
specUrlstring—Project-relative file, file: URL, absolute HTTP(S) URL, or request-relative URL for an OpenAPI JSON/YAML document. When set, framework route scanning is skipped
overlaysreadonly string[][]Ordered OpenAPI Overlay 1.0/1.1 sources applied to the single specUrl before $ref resolution
versionsRecord<string, ApiReferenceVersionConfig>—Named sources with stable URL-safe identifiers, labels, and optional per-version overlays
defaultVersionstring—Required key from versions; selects the unversioned default route and schema response
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 configured document. Project-relative paths must remain inside the project root; use an explicit file: URL when a trusted local contract intentionally lives elsewhere.

versions cannot be combined with the single-source specUrl or top-level overlays. Version identifiers may contain letters, numbers, dots, underscores, and hyphens. Farming Labs publishes every version in agent discovery, llms.txt, and the RFC 9727 API catalog while keeping the unversioned API reference and schema endpoint as aliases for defaultVersion.

Overlay files may be local or remote JSON/YAML documents using OpenAPI Overlay 1.0.x or 1.1.x. They are applied in array order and action order before references are bundled. Targets use RFC 9535 JSONPath. Invalid documents, invalid selectors, unmatched targets, incompatible merge values, and a final result that is no longer a valid OpenAPI contract produce an unavailable-contract diagnostic instead of silently rendering a partial result. The same timeout, 5 MiB size limit, and project-root containment rules used for contracts also apply to overlays.

Relative and absolute $ref values may target local or remote JSON/YAML documents. Farming Labs validates JSON Pointer fragments, rewrites external references into a self-contained bundle, and caches each referenced document for that build. Project-relative entry documents cannot use a reference to escape the project root. The synchronous builder supports local references only; use the async builder or a framework route for a local document that references HTTP(S). Named anchors and $dynamicRef resolution are not supported yet.

That does not change the framework routing requirements:

  • Next.js still generates the API reference route automatically with withDocs()
  • Farm.js still serves the API reference route through 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.ts or src/app/api/**/route.ts
  • TanStack Start: src/routes/api.*.ts and nested route files under the configured route root
  • Farm.js: app/api/**/route.ts or src/app/api/**/route.ts
  • SvelteKit: src/routes/api/**/+server.ts or +server.js
  • Astro: src/pages/api/**/*.ts or .js
  • Nuxt: server/api/**/*.ts or .js

Opt-in route wiring

Next.js

  • Nothing else is required beyond apiReference in docs.config and withDocs() in next.config.ts.

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.

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().

docs.config.ts
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
PropertyTypeDefaultDescription
enabledbooleantrue inside the objectEnables generated changelog pages
pathstring"changelog"URL path where the changelog listing lives inside the docs layout
contentDirstring"changelog"Source directory for dated entry folders like app/docs/changelog/2026-03-04/page.mdx
titlestring"Changelog"Listing page title
descriptionstring—Listing page description and metadata
searchbooleantrueShow the built-in changelog search field
actionsComponentReactNode | Component—Custom action content rendered in the changelog rail

Useful entry frontmatter:

  • title
  • description
  • image
  • authors
  • version
  • tags
  • pinned
  • draft

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.

docs.config.ts
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 lang query parameter
  • Each locale can have its own content tree and sidebar structure
  • The CLI can scaffold locale folders like docs/en and docs/fr for 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.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
When enabled is false, the configured default value ("light" or "dark") is applied as a forced theme and the switcher is hidden entirely. When the toggle is enabled (the default), the same default value is used as the initial theme that visitors can still change. Setting default: "system" skips forced and default theme derivation in both cases, deferring to the operating-system preference.
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
urlstring—Repository URL
branchstring"main"Branch name
directorystring—Subdirectory 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" },
      },
    },
  },
}),

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"
---

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.

Set requireComment: true to disable the submit button until the user types a non-empty comment:

docs.config.ts
feedback: {
  enabled: true,
  requireComment: 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.

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 });
}

Golden agent evaluations

Use agent.evaluations to define user-shaped tasks that docs doctor and docs review can measure.

Golden tasks measure retrieval recall, citation accuracy, version selection, adversarial safety, answer quality, executable examples, and token budget. The default mcp-context surface is local and makes no implicit model, network, or command-execution request. You can explicitly measure the configured search pipeline, the production Ask AI context, an actual answer provider, or runtime-verified examples when those checks are useful.

docs.config.ts
export default defineDocs({
  entry: "docs",
  agent: {
    evaluations: {
      tokenBudget: 4_000,
      topK: 3,
      searchTimeoutMs: 30_000,
      surface: "mcp-context",
      tasks: [
        {
          id: "next-16-install",
          query: "Install the docs framework in a Next.js 16 app",
          filters: {
            framework: "nextjs",
            version: "16",
          },
          expect: {
            scope: {
              framework: "nextjs",
              version: "16",
            },
            relevantSources: ["/docs/installation"],
            requiredCitations: ["/docs/installation"],
            forbiddenSources: ["/docs/legacy-installation"],
            minRecallAtK: 1,
            maxFirstRelevantRank: 1,
            minUsefulByteRatio: 0.7,
            examples: [
              {
                source: "/docs/installation",
                language: "bash",
                packageManager: "pnpm",
                runnable: true,
                verification: "present",
                includes: ["pnpm add @farming-labs/docs"],
              },
            ],
          },
        },
      ],
    },
  },
});

Top-level tokenBudget and topK are defaults; either can be overridden on an individual task. requiredCitations defaults to relevantSources, and allowedSources can name legitimate supporting pages that should not reduce citation precision. Source values may be canonical page or section URLs. filters constrain retrieval, while expect.scope independently asserts that the returned sources selected the right framework, version, or locale. This distinction catches a provider that returns a plausible result for the wrong target instead of hiding that result before evaluation.

Choose the surface globally with agent.evaluations.surface or override it on one task:

  • "mcp-context" is the default built-in context builder and stays local.
  • "configured-search" queries the project's actual search provider and evaluates only its returned order and citations, without local-result supplementation or diagnostic reranking.
  • "ask-ai-context" runs the production Ask AI retrieval and context-assembly path, including its source headers and truncation behavior.

Non-simple configured search providers require allowNetwork: true. Provider errors fail the task instead of silently substituting local search, so the report always identifies the surface and provider it actually measured. searchTimeoutMs bounds each configured-search or Ask AI retrieval attempt and defaults to 30 seconds. Transient timeouts, fetch failures, rate limits, and server errors are retried twice with bounded backoff; other provider errors fail immediately. Each timeout aborts framework-managed requests and is also exposed to custom adapters as context.signal. The evaluator records actual UTF-8 usage for every surface. MCP and configured-search contexts enforce the byte ceiling directly; Ask AI uses its production character limit and fails the budget check if the resulting UTF-8 payload exceeds the configured ceiling.

Adversarial safety assertions

Add expect.safety to a task to assert that the agent's retrieval and answer pipeline resists adversarial inputs. These fields are evaluator-only and must never contain real secrets or private data.

docs.config.ts
export default defineDocs({
  agent: {
    evaluations: {
      tasks: [
        {
          id: "safe-retrieval",
          query: "Configure safe retrieval",
          expect: {
            relevantSources: ["/docs/safe-retrieval"],
            safety: {
              promptInjection: {
                markers: ["SAFETY_INJECTION_CANARY"],
                forbiddenAnswerText: ["ignore the user"],
              },
              poisonedCitations: true,
              authenticatedContent: {
                forbiddenSources: ["/docs/private-control-plane"],
                forbiddenText: ["PRIVATE_AUTH_CANARY"],
              },
              freshness: {
                indexGeneration: "expected-generation-digest",
                sourceDigests: {
                  "/docs/safe-retrieval": "sha256:abc123",
                },
              },
              rejectConflictingFrameworkVersions: true,
              deletedSectionTombstones: true,
              queryVariants: [
                { kind: "rephrased" },
                { kind: "adversarial" },
              ],
            },
          },
        },
      ],
    },
  },
});
FieldTypeDescription
promptInjection.markersstring[]Canary fragments that must be present in retrieved context
promptInjection.forbiddenAnswerTextstring[]Canary fragments the answer must not repeat. Defaults to markers
poisonedCitationsbooleanAssert that retrieval rejects injected citation payloads
authenticatedContent.forbiddenSourcesstring[]Protected page or section URLs that public retrieval must not expose
authenticatedContent.forbiddenTextstring[]Stable canary fragments that must not appear in context or answers
freshness.indexGenerationstringExpected retrieval-index generation digest. Omit to require one consistent generation
freshness.sourceDigestsRecord<string, string>Expected full-document digest keyed by canonical page or section URL
rejectConflictingFrameworkVersionsbooleanAssert that the pipeline rejects sources with conflicting framework version provenance
deletedSectionTombstonesbooleanAssert that deleted-section tombstones are surfaced rather than silently dropped
queryVariants[].kindstringAdditional query variant kind to evaluate, e.g. "rephrased" or "adversarial"

Evaluate actual answers

Answer scoring is opt-in. Add expect.answer to a task and configure either a callback or an HTTP provider. The framework does not choose or call a model when answer is omitted.

docs.config.ts
export default defineDocs({
  agent: {
    evaluations: {
      allowNetwork: true,
      answer: {
        provider: "http",
        endpoint: process.env.AGENT_EVAL_ENDPOINT!,
        headers: process.env.AGENT_EVAL_TOKEN
          ? { authorization: `Bearer ${process.env.AGENT_EVAL_TOKEN}` }
          : undefined,
        timeoutMs: 10_000,
      },
      tasks: [
        {
          id: "install-answer",
          query: "How do I install the framework?",
          expect: {
            relevantSources: ["/docs/installation"],
            answer: {
              includes: ["pnpm add"],
              excludes: ["legacy-package"],
              requiredCitations: ["/docs/installation"],
            },
          },
        },
      ],
    },
  },
});

The HTTP endpoint receives the task id, query, filters, surface, context, and retrieved sources as JSON, but never receives the expected answer, citations, or callback-only abort signal. It returns { text, citations? }. HTTP authentication is optional and explicit through headers; header values are never copied into evaluation reports. The built-in HTTP provider and managed external search requests require allowNetwork: true and use fail-closed provider errors. A provider: "callback" answer instead calls your explicit run(input) function and expects the same answer result shape. That callback is user code, so it may perform only the work you put inside it; allowNetwork does not sandbox or reinterpret it.

Choose example verification strength

Set expect.examples[].verification to the evidence the task must produce:

  • "present" requires a matching retrieved fence.
  • "syntax" requires supported static syntax validation. It is the default for runnable examples.
  • "execute" requires the configured validator to execute the snippet and return PASS. It only runs when allowNetwork: true and codeBlocks.validate are explicitly enabled in "report" mode with a suitable runner.

Examples marked runnable: false default to "present". Unsupported syntax, a skipped runner, or a missing execution opt-in is reported as skipped or failed and never receives passing credit.

An absent, disabled, or empty task suite is deliberately unmeasured, not passing. This keeps a site from receiving usefulness credit merely because no representative tasks were defined. Put runnable, framework, packageManager, and title metadata on expected code fences so example checks can select the intended snippet deterministically.

Agent Compaction

Use agent.compact to set defaults for docs agent compact, which generates sibling agent.md files from resolved docs pages.

docs.config.ts
export default defineDocs({
  entry: "docs",
  agent: {
    compact: {
      apiKeyEnv: "DOCS_CLOUD_API_KEY",
      model: "docs-cloud-compress-v1",
      aggressiveness: 0.3,
      protectJson: true,
    },
  },
});

If you want to reference the key directly from docs.config.tsx, this is supported too:

docs.config.tsx
export default defineDocs({
  entry: "docs",
  agent: {
    compact: {
      apiKey: process.env.DOCS_CLOUD_API_KEY,
    },
  },
});

Supported fields:

  • apiKey

  • apiKeyEnv

  • baseUrl

  • model

  • aggressiveness

  • maxOutputTokens

  • minOutputTokens

  • protectJson

  • reviewManifestPath

reviewManifestPath sets the project-relative path to the JSON manifest used by docs agent compact --review to store hash-bound reviewed exceptions. The default is .farming-labs/agent-compaction-reviews.json. Set it when your project stores the manifest at a custom location:

docs.config.ts
agent: {
  compact: {
    reviewManifestPath: ".farming-labs/agent-compaction-reviews.json",
  },
},

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 root agent.compact config, so apiKeyEnv: "DOCS_CLOUD_API_KEY" works without exporting the key manually. Keep compression keys in agent.compact.apiKey or agent.compact.apiKeyEnv; use positional page args by default:

terminal
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 --changed

Per-page compaction budgets live in frontmatter:

app/docs/installation/page.mdx
---
title: "Installation"
description: "Install the framework"
agent:
  tokenBudget: 777
  task: Install the framework
  outcome: The docs route renders locally.
  appliesTo:
    framework: nextjs
    version: ">=16"
    package: "@farming-labs/next"
  files:
    - package.json
    - next.config.ts
  commands:
    - run: pnpm add @farming-labs/docs @farming-labs/next
  verification:
    - run: pnpm dev
      expect: The docs route returns HTTP 200
---

Notes:

  • agent.tokenBudget overrides global agent.compact.maxOutputTokens defaults and CLI --max-output-tokens for that one page
  • optional structured fields include task, outcome, appliesTo, prerequisites, files, commands, sideEffects, verification, rollback, and failureModes
  • structured fields are emitted into machine-readable Markdown, search/Ask AI context, Schema.org HowTo JSON-LD, and MCP task tools; page-list tools keep only the concise contract summary
  • invalid or unknown fields are ignored at runtime and reported by docs review; misspelled keys get closest-name suggestions such as verfication → verification
  • if a sibling agent.md already exists, docs agent compact compacts that file
  • if no agent.md exists, the command compacts the generated machine-readable page output and then writes a new sibling agent.md
  • generated contract markers are excluded from compaction input, and rendering preserves a handwritten ## Agent Contract, so repeated compaction/read cycles do not duplicate the section
  • docs agent compact --changed only processes docs pages changed in the current git working tree, including staged, unstaged, and untracked docs changes
  • docs agent compact --stale --include-missing will also create missing agent.md files for pages that define agent.tokenBudget
  • if inherited minOutputTokens would 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.

Protect agent-facing page content

Add agent.access to keep a page out of unauthenticated machine-readable surfaces while allowing an authenticated MCP principal with the required scopes and claims to read it:

app/docs/internal-runbook/page.mdx
---
title: "Internal deployment runbook"
agent:
  access:
    visibility: authenticated
    scopes:
      - docs:private
    claims:
      role:
        - admin
        - editor
      plan: enterprise
---

The policy is deny by default. Every scope and claim must match; an expected claim array matches when the principal has any listed value. The principal comes from mcp.security.authenticate, so the example requires a principal such as { id: "agent-1", scopes: ["docs:private"], claims: { role: "editor", plan: "enterprise" } }.

Protected pages are omitted from public llms.txt, search/Ask AI indexes, content-change feeds, Markdown recovery suggestions, MCP navigation, and Static Agent Bundles. Default .md routes return a private, non-cacheable 404 instead of revealing whether the page exists. Invalid authored policies also fail closed and are reported by docs review.

agent.access protects the framework's agent-facing machine surfaces. It does not authenticate your rendered HTML page route; enforce application authorization separately when the human page is also private.

Reusable Agent Skills

Set agent.skills to publish every configured skill through standards discovery, Static Agent Bundles, and MCP. The value can be a string, an array, or { paths }:

docs.config.ts
export default defineDocs({
  agent: {
    skills: {
      paths: [
        "./skills/getting-started/SKILL.md",
        "./skills/product",
        "./skills/team-skills",
      ],
      progressiveDisclosure: {
        maxSkillLines: 500,
        instructionTokenBudget: 5000,
        maxReferenceDepth: 1,
        compatibility: "when-needed",
        checkScripts: true,
      },
    },
  },
});

A path can point to one SKILL.md, a skill directory containing SKILL.md, or a collection directory whose descendants contain skill directories. Paths are relative to the project root and must remain inside its workspace. Publication accepts only non-symlink regular files and directories. It includes the skill document and safe files below references/, scripts/, and assets/; other sibling content is not exposed.

Skills with only SKILL.md are indexed as skill-md. Skills with companion files are packaged as deterministic .tar.gz archives, and the index digest covers the exact indexed artifact bytes. The original files stay directly available for progressive disclosure. The project-root skill.md remains the backwards-compatible site skill and is published alongside configured skills.

docs doctor --agent checks every configured skill, and docs review checks the affected skill when SKILL.md, references/, scripts/, assets/, or the docs config changes. A local reference must stay inside the skill and resolve to content that publication exposes. Reference Markdown can be one hop away by default; move second-level detail into a directly linked focused reference. Token estimates use a dependency-free, code-conscious approximation and can be tuned with instructionTokenBudget.

SurfaceRoute or URI
Agent Skills v0.2 index/.well-known/agent-skills/index.json
Indexed skill archive/.well-known/agent-skills/<name>.tar.gz
Direct skill files/.well-known/agent-skills/<name>/SKILL.md and /<companion-path> below it
Legacy index and files/.well-known/skills/index.json and direct files below /.well-known/skills/<name>/
MCP resourcesdocs://skills/<name>/<path>

MCP returns text-compatible resources as text and binary assets as base64 blob values. Running docs agent export --public emits the same indexes, artifacts, direct companion files, and hashes for static hosts.

Optional A2A Agent Card

Configure agent.a2a only when the advertised URLs implement real A2A interfaces. A docs API, Ask AI route, or MCP endpoint does not satisfy that contract. Prefer ordered supportedInterfaces; the first entry is the preferred interface. Each interface defaults to A2A protocol 1.0 over HTTP+JSON. The core bindings are JSONRPC, GRPC, and HTTP+JSON; identify a custom binding with an absolute URI. The deprecated interfaceUrl shorthand remains available for a single interface, with top-level protocolVersion and protocolBinding applying only to that shorthand. It retains the historical protocol 0.3 default; set protocolVersion: "1.0" explicitly when a shorthand interface implements v1. Core interfaces and all HTTP card/authentication metadata require HTTPS outside loopback development. Custom bindings use a secure binding-appropriate absolute URL such as WSS or MQTTS.

The published /.well-known/agent-card.json uses the strict A2A v1 shape: it does not emit the legacy top-level url, protocolVersion, or preferredTransport fields. Describe only capabilities, media modes, A2A skills, and optional security metadata that the service really implements. An A2A skill has no URL; configured Agent Skill documents and artifacts keep their URLs in the Agent Skills indexes and MCP resources. supportedInterfaces requires explicit A2A skills; automatic projection of published documentation skills remains only for the deprecated shorthand. When agent.a2a is omitted, the Agent Card route is not advertised or generated.

docs.config.ts
agent: {
  skills: "./skills",
  a2a: {
    name: "Product docs agent",
    description: "Answers implementation questions from the product docs.",
    supportedInterfaces: [
      {
        url: "https://agent.example.com/a2a",
        protocolBinding: "HTTP+JSON",
        protocolVersion: "1.0",
      },
      {
        url: "https://agent.example.com/a2a/rpc",
        protocolBinding: "JSONRPC",
        protocolVersion: "1.0",
      },
    ],
    documentationUrl: "https://docs.example.com/docs",
    provider: {
      organization: "Example, Inc.",
      url: "https://example.com",
    },
    version: "1.0.0",
    capabilities: {
      streaming: true,
      pushNotifications: false,
    },
    defaultInputModes: ["text/plain"],
    defaultOutputModes: ["text/plain"],
    skills: [
      {
        id: "answer-docs",
        name: "Answer documentation questions",
        description: "Answers implementation questions using the product docs.",
        tags: ["documentation", "implementation"],
      },
    ],
  },
},

See Agent Primitive for the discovery flow and API Reference for the exact types.

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 the versioned Farming Labs manifest schema and format identifiers, 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.

docs.config.ts
export default defineDocs({
  entry: "docs",
  feedback: {
    agent: {
      async onFeedback(data) {
        console.log(data.context?.source, data.payload);
      },
    },
  },
});

Default behavior:

  • GET and HEAD /.well-known/api-catalog return the RFC 9727 API catalog
  • set llmsTxt.apiCatalog: false to remove that catalog route and its discovery links without disabling the hashed Agent Skills routes
  • GET and HEAD /.well-known/agent-skills/index.json return the modern hashed Agent Skills index
  • GET and HEAD /.well-known/skills/index.json return the legacy compatibility index; indexed artifacts and safe direct files remain available at the routes described above
  • dynamic adapters set the catalog media type and discovery Link headers; static hosts should serve /.well-known/api-catalog as application/linkset+json with the RFC 9727 profile
  • GET /.well-known/agent.json is the preferred public agent discovery URL
  • GET /.well-known/agent is the public fallback discovery URL
  • GET /api/docs/agent/spec returns the canonical framework discovery document
  • the manifest declares the Draft 2020-12 schema at https://docs.farming-labs.dev/schema/agent-manifest.v1.json; dynamic responses advertise it with rel="describedby" while preserving application/json
  • the manifest is a Farming Labs extension; the separate /.well-known/agent-card.json route is only published when agent.a2a configures a real A2A service
  • GET /AGENTS.md serves a root AGENTS.md or AGENT.md file when present, and otherwise returns a generated fallback
  • GET /.well-known/AGENTS.md and GET /api/docs?format=agents return the same agent instructions
  • GET /skill.md serves a root skill.md file when present, and otherwise returns a generated fallback
  • GET /.well-known/skill.md and GET /api/docs?format=skill return the same skill document
  • the discovery JSON advertises agentSpecDefault, agentSpecFallback, apiCatalog, skills.discovery, agents.file, agents.route, skills.file, skills.route, the root and section llms.txt routes, robots.route, and sitemap routes when enabled
  • GET /api/docs/agent/feedback/schema returns the feedback schema
  • POST /api/docs/agent/feedback accepts { context?, payload }; without onFeedback it returns { ok: true, handled: false }
  • non-Next server adapters also expose the same contract through GET /api/docs?feedback=agent&schema=1 and POST /api/docs?feedback=agent
  • the shared /api/docs handler is still the source of truth
  • in Next.js, withDocs() adds the public rewrites automatically
  • feedback.agent alone does not enable the human footer UI
  • set feedback: false or feedback: { 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.

Turn recurring feedback into improvements

Store callback payloads as a JSON array/object or JSON Lines export, then cluster recurring failed tasks locally:

pnpm exec docs agent feedback --input .farming-labs/agent-feedback.jsonl
pnpm exec docs agent feedback --input .farming-labs/agent-feedback.jsonl --write

The report contains evidence clusters, deduplicated missing context and doc issues, draft GitHub issues, draft golden tasks for agent.evaluations.tasks, and a draft documentation PR title/body. Only failed, blocked, partial, unclear, or code-reading-dependent reports participate; a cluster needs two reports by default. Use --min-occurrences to raise that threshold. Without --write, the command only prints JSON.

Feedback text remains untrusted evidence in every draft. Review it before copying suggestions into docs, issues, evaluation config, or model prompts.

Promote feedback into evaluation candidates

Turn the feedback report's draft golden tasks into a sanitized candidate registry:

terminal
pnpm exec docs agent feedback-evals \
  --input .farming-labs/agent-feedback-improvements.json \
  --write

The command redacts common credentials, secret query parameters, private keys, and email addresses; normalizes task budgets and retrieval expectations; and deduplicates by task ID and a stable query/source fingerprint. Candidates remain in state: "candidate" and are never inserted into agent.evaluations.tasks automatically. Review the original evidence and the sanitized task before promoting it.

After running docs doctor --agent --json-output doctor.json, create an explicit baseline and use it as a CI regression gate:

terminal
pnpm exec docs agent feedback-evals \
  --results doctor.json \
  --baseline .farming-labs/agent-evaluation-baseline.json \
  --write-baseline

pnpm exec docs agent feedback-evals \
  --results doctor.json \
  --baseline .farming-labs/agent-evaluation-baseline.json \
  --check

The check catches suite score drops, per-task score drops, pass-to-fail changes, removed baseline tasks, and newly added failing tasks. Use --max-score-drop only for an intentional bounded tolerance, and --allow-new-failures only when newly introduced tasks are expected to start red. Baseline replacement is always explicit through --write-baseline.

Feed that report—or normalized search, Ask AI, MCP, issue, support, and git signals—into the maintenance proposal workflow:

terminal
pnpm exec docs agent propose \
  --input .farming-labs/agent-feedback-improvements.json \
  --input .farming-labs/search-signals.jsonl \
  --write

Generic inputs are JSON arrays/objects or JSON Lines records with source, summary, and optional page, topic, task, severity, and count fields. Supported sources are agent-feedback, ask-ai, git, issue, mcp, search, and support. Use the same topic on related records to cluster evidence from different systems.

The output ranks recurring gaps by represented occurrences, source diversity, and severity, then drafts acceptance criteria, issues, golden tasks, and a PR description. Signal summaries are always rendered as untrusted evidence. The workflow is deliberately proposal-only: it never edits docs, creates issues, publishes branches, or merges changes, and every proposal requires human review.

Default request shape:

POST body
{
  "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:

docs.config.ts
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:

terminal
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"}}'

Last Updated

Use lastUpdated to control the date shown at the bottom of each docs page. The optional label field lets you replace the default "Last updated" prefix with any string — useful for localization or rebranding.

docs.config.ts
lastUpdated: {
  label: "Updated",
  position: "below-title",
},
PropertyTypeDefaultDescription
enabledbooleantrueShow or hide the last-updated date
labelstring"Last updated"Text prefix shown before the formatted date
position"footer" | "below-title""footer"Render beside "Edit on GitHub" or below the page title

Set label to an empty string to display only the formatted date with no prefix text. Dates are formatted against UTC, so the displayed calendar day stays the same across local, serverless, and edge deployment timezones.

Reading Time

Use readingTime when you want an estimated label like 5 min read on docs pages.

docs.config.ts
export default defineDocs({
  entry: "docs",
  readingTime: {
    enabled: true,
    wordsPerMinute: 220,
    format: "short",
  },
});

For code-heavy guides where examples make up a large part of the page, set includeCode: true so fenced and inline code blocks are counted toward the estimate:

docs.config.ts
export default defineDocs({
  entry: "docs",
  readingTime: {
    enabled: true,
    wordsPerMinute: 220,
    includeCode: true,
  },
});

Default behavior:

  • the feature is opt-in

  • an options object does not enable the feature unless it includes enabled: true

  • the estimate uses 220 words per minute unless you override it

  • labels render as 5 min read by default; set format: "short" to render 5 min

  • fenced code blocks, inline code, links, images, and raw HTML are stripped before counting words

  • set includeCode: true in ReadingTimeConfig to count code block and inline code words toward the estimate instead of stripping them

  • changelog pages do not render the reading-time label

  • page frontmatter wins when readingTime is present, even if the global config is disabled

Per-page overrides live in frontmatter:

page.mdx
---
title: "Installation"
description: "Get up and running"
readingTime: false
---

You can also opt a page in explicitly when the global config is off:

page.mdx
---
title: "API Guide"
readingTime: true
---

And you can pin an exact number:

page.mdx
---
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 exact 12 min read label for that page

The global format setting applies to computed labels and exact per-page values. With format: "short", the same readingTime: 12 override renders as 12 min.

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,
    target: "markdown",
    providers: ["chatgpt", "claude", "cursor"],
  },
},

Use openDocs: true for the built-in ChatGPT + Claude provider list, or pass provider strings and target: "markdown" to send tools directly to the public .md route for the page.

For deeper customization, use provider objects. This keeps the built-in preset but opens Cursor through the app deeplink:

pageActions: {
  openDocs: {
    enabled: true,
    target: "markdown",
    providers: [
      "chatgpt",
      "claude",
      { id: "cursor", mode: "app" },
    ],
  },
},

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.

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",
  },
});
Customize
Live preview on these docs
Reset
Customize
Export Code
Preset
Explore more themes
Primary Color
Primary
Colors
Background
Foreground
Muted Text
Border
Card
Muted BG
Ring
Layout
SidebarBordered
TOC StyleDirectional (tree)
TOC DepthH2 + H3
Ask AI
ModeFloating
Floating StyleFull Modal
PositionBottom Right
Features