API Reference
Complete reference for every option in docs.config.ts. All types are exported from @farming-labs/docs.
defineDocs(config)
The entry point for configuring your docs. Returns a typed config object.
import { defineDocs } from "@farming-labs/docs";
export default defineDocs({
entry: "docs",
// ...all options below
});DocsConfig
DocsLocalMcpSearchRuntimeConfig
Runtime shape that describes whether the local MCP server should be treated as the search target. Exported from @farming-labs/docs and @farming-labs/docs/server.
| Property | Type | Description |
|---|---|---|
enabled | boolean | Whether the local MCP server is enabled |
route | string | Canonical HTTP route for the local MCP endpoint (e.g. "/api/docs/mcp") |
tools | { searchDocs?: boolean } | Tool-level toggle; searchDocs controls whether search_docs is exposed |
DocsLocalMcpSearchRuntimeInput
Accepted input shape for resolveLocalDocsMcpSearchConfig. A partial form of DocsLocalMcpSearchRuntimeConfig — all fields are optional. Exported from @farming-labs/docs and @farming-labs/docs/server.
DocsSearchRequestResolutionOptions
Options accepted by the third parameter of resolveSearchRequestConfig. Exported from @farming-labs/docs and @farming-labs/docs/server.
| Property | Type | Description |
|---|---|---|
localMcp | DocsLocalMcpSearchRuntimeInput | null | undefined | When present, resolveSearchRequestConfig calls resolveLocalDocsMcpSearchConfig before the self-referential URL check to detect whether the MCP endpoint resolves to the local server |
resolveLocalDocsMcpSearchConfig
Public function exported from @farming-labs/docs and @farming-labs/docs/server. Given a McpDocsSearchConfig, an optional DocsLocalMcpSearchRuntimeInput, and an optional request URL, returns a modified search config that uses direct simple search when the configured MCP endpoint resolves to the local server. Returns the original config unchanged for external endpoints.
import { resolveLocalDocsMcpSearchConfig } from "@farming-labs/docs";
const resolved = resolveLocalDocsMcpSearchConfig(
search, // McpDocsSearchConfig
mcp, // DocsLocalMcpSearchRuntimeInput | null | undefined
requestUrl, // string | undefined
);resolveSearchRequestConfig now accepts an optional third options: DocsSearchRequestResolutionOptions argument. Pass { localMcp: config.mcp } to enable local-MCP detection in custom server adapters.
Top-level configuration object passed to defineDocs().
| Property | Type | Default | Description |
|---|---|---|---|
entry | string | required | Docs source route and folder (e.g. "docs" -> app/docs) |
docsPath | string | same as entry | Public docs route prefix in Next.js. Use "" to serve docs from / |
contentDir | string | same as entry | Path to content files. TanStack Start, SvelteKit, Astro, and Nuxt use it; Next.js uses app/{entry}/ |
staticExport | boolean | false | Set true for full static builds; hides search and AI (see Configuration) |
theme | DocsTheme | — | Theme preset from a factory (fumadocs(), darksharp(), pixelBorder(), etc.) |
github | string | GithubConfig | — | GitHub repo config for "Edit on GitHub" links |
nav | DocsNav | — | Sidebar header title and URL |
themeToggle | boolean | ThemeToggleConfig | true | Light/dark mode toggle |
breadcrumb | boolean | BreadcrumbConfig | true | Breadcrumb navigation |
sidebar | boolean | SidebarConfig | true | Sidebar visibility and customization |
icons | Record<string, unknown> | — | Shared icon registry for frontmatter icon fields and built-ins like Prompt |
components | Record<string, unknown> | — | Custom MDX component overrides, including built-ins like HoverLink and Prompt |
analytics | boolean | DocsAnalyticsConfig | false | Product/usage event stream for docs UI, search, AI, feedback, agent reads, and MCP usage |
telemetry | boolean | DocsTelemetryConfig | production-only enabled | Farming Labs maintainer telemetry for package adoption and coarse agent-surface usage |
observability | boolean | DocsObservabilityConfig | false | Trace event stream for Ask AI and MCP runs, including spans, timing, status, and errors |
onCopyClick | (data: CodeBlockCopyData) => void | — | Callback when the user clicks the copy button on a code block (runs in addition to copy) |
feedback | boolean | FeedbackConfig | false for UI | Human page feedback UI; agent feedback endpoints are default-on unless opted out |
agent | DocsAgentConfig | — | Compaction, golden evaluations, reusable skill publication, and optional A2A metadata |
review | boolean | DocsReviewConfig | true | Docs Review scoring, rule severity, and GitHub Actions workflow generation |
readingTime | boolean | ReadingTimeConfig | false | Opt-in estimated reading-time label with per-page overrides |
pageActions | PageActionsConfig | — | "Copy Markdown" and "Open in LLM" buttons |
ai | AIConfig | — | RAG-powered AI chat |
search | boolean | DocsSearchConfig | true | Built-in simple search, Typesense, Algolia, or a custom adapter |
mcp | boolean | DocsMcpConfig | enabled | Built-in MCP server over stdio, /mcp, and /.well-known/mcp |
apiReference | boolean | ApiReferenceConfig | false | Generated API reference pages from supported framework route conventions or local/remote OpenAPI JSON and YAML |
sitemap | boolean | DocsSitemapConfig | true | Generated sitemap.xml, sitemap.md, /docs/sitemap.md, and /.well-known/sitemap.md |
robots | boolean | DocsRobotsConfig | true | Runtime/generated robots.txt policy for docs and agent-readable routes |
changelog | boolean | ChangelogConfig | false | Generated changelog feed and entry pages from dated MDX entries |
ordering | "alphabetical" | "numeric" | OrderingItem[] | "alphabetical" | Sidebar page ordering strategy |
metadata | DocsMetadata | — | SEO metadata |
ThemeToggleConfig
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Show or hide the theme switcher |
default | "light" | "dark" | "system" | "system" | When enabled is false, this value is applied as a forced theme (hiding the switcher). When the toggle is enabled, it is used as the initial theme that visitors can still change. "system" skips forced and default derivation in both cases. |
mode | "light-dark" | "light-dark-system" | "light-dark" | Toggle mode presented to the user |
og | OGConfig | — | Open Graph image config |
DocsMarkdownSection, DocsMarkdownSectionRequest, and DocsMarkdownSectionResult
Three interfaces that power section-addressable markdown reads. All are exported from @farming-labs/docs.
DocsMarkdownSection
Represents a single heading-delimited section parsed from a markdown document.
| Property | Type | Description |
|---|---|---|
id | string | Stable kebab-case heading id used as the section query parameter |
heading | string | Raw heading text |
level | number | Heading level (1–6) |
content | string | Full markdown content of the section, including the heading line |
startLine | number | 1-based line number of the heading that opens this section, relative to the Markdown body |
endLine | number | 1-based inclusive line number of the last line in this section |
startLine | number | Zero-based line index where the section starts |
endLine | number | Zero-based line index where the section ends (exclusive) |
DocsMarkdownSectionRequest
Input parameters for a section-addressable markdown request.
| Property | Type | Description |
|---|---|---|
section | string | null | Heading id to select, e.g. "prerequisites". When omitted the full document is returned. |
tokenBudget | number | null | Soft token ceiling; the response is truncated when the estimated token count exceeds this value |
byteBudget | number | null | Soft UTF-8 byte ceiling applied before the token estimate |
DocsMarkdownSectionResult
Result returned by resolveDocsMarkdownSectionRequest.
| Property | Type | Description |
|---|---|---|
found | boolean | true when the requested section was resolved |
requestedSection | string | The section value from the request, when provided |
section | DocsMarkdownSection | The resolved section, present when found is true |
document | string | The content to return — the matched section body or the full document |
truncated | boolean | true when the document was cut to fit the budget |
tokenBudget | number | Applied token budget |
byteBudget | number | Applied byte budget |
estimatedTokens | number | Estimated token count for the returned document |
utf8Bytes | number | UTF-8 byte length of the returned document |
availableSections | DocsMarkdownSection[] | All sections parsed from the full document; useful for agent recovery when found is false |
Related functions
Three functions are exported from @farming-labs/docs for working with these types:
| Function | Description |
|---|---|
collectDocsMarkdownSections(markdown) | Parse a markdown string into an array of DocsMarkdownSection objects, one per heading block |
selectDocsMarkdownSection(sections, id) | Find a section by its stable heading id; returns the matching DocsMarkdownSection or undefined |
resolveDocsMarkdownSectionRequest(markdown, request) | Combine parsing, section lookup, and budget enforcement in one call; returns a DocsMarkdownSectionResult |
The constant DOCS_MARKDOWN_SECTION_MAX_BUDGET = 1_000_000 is the hard upper limit accepted for tokenBudget and byteBudget.
DocsMcpPageSectionIndex
DocsMcpPageSectionIndex is the body-free section discovery metadata returned by the list_page_sections MCP tool. It is exported from @farming-labs/docs as a type alias for DocsMarkdownSectionIndex.
DocsMarkdownSectionIndex
The docs-markdown-sections.v1 JSON payload returned by the ?sections endpoint.
| Property | Type | Description |
|---|---|---|
format | "docs-markdown-sections.v1" | Fixed format identifier |
sectionCount | number | Total number of sections in the document |
sections | DocsMarkdownSectionMetadata[] | Ordered array of section descriptors |
DocsMarkdownSectionMetadata
One entry in the sections array of a DocsMarkdownSectionIndex.
| Property | Type | Description |
|---|---|---|
id | string | Stable kebab-case heading id, usable as the section query parameter |
heading | string | Raw heading label |
level | number | Heading level (1–6) |
parentId | string | null | Id of the nearest ancestor heading, or null for top-level sections |
startLine | number | 1-based line of the heading within the Markdown body |
endLine | number | 1-based inclusive final line of the section |
estimatedTokens | number | Conservative token estimate for the section content |
markdownUrl | string | Absolute URL for fetching this section with ?section={id} |
DocsMarkdownSectionIndexOptions
Options accepted by buildDocsMarkdownSectionIndex.
| Property | Type | Description |
|---|---|---|
markdown | string | Raw Markdown source to index |
baseUrl | string | Base URL used to construct markdownUrl values for each section |
Section-index functions
Two additional functions are exported from @farming-labs/docs:
| Function | Description |
|---|---|
buildDocsMarkdownSectionIndex(options) | Parse a Markdown string and build a DocsMarkdownSectionIndex response object with per-section metadata and absolute markdownUrl values |
isDocsMarkdownSectionIndexRequest(request) | Return true when the incoming Request includes the sections query parameter and should receive a section-index response instead of the full Markdown body |
The constant DOCS_MARKDOWN_SECTION_INDEX_FORMAT = "docs-markdown-sections.v1" is the fixed format string included in every index response and advertised in /.well-known/agent.json.
DocsConfigMap, DocsConfigMapPointer, and config-map constants
The config-map endpoint (GET /api/docs?format=config) returns a DocsConfigMap JSON document
built from the active docs config. All five framework server adapters handle this request
automatically.
Constants
| Constant | Value | Description |
|---|---|---|
DEFAULT_DOCS_CONFIG_FORMAT | "docs-config-map.v1" | Schema version identifier included in the response and in the agent discovery spec |
DEFAULT_DOCS_CONFIG_ROUTE | "/api/docs?format=config" | Canonical route for the config-map endpoint |
DocsConfigMap
The JSON response shape. Contains a format field set to "docs-config-map.v1" and a pointers
array of DocsConfigMapPointer entries. Sensitive config values are redacted before serialization.
DocsConfigMapPointer
| Property | Type | Description |
|---|---|---|
path | string | Dot-path to the config field, e.g. "mcp.tools.readPage" |
kind | string | Coarse value kind at this pointer, e.g. "boolean", "string", "object" |
Response headers
| Header | Value |
|---|---|
Content-Type | application/json; charset=utf-8 |
Cache-Control | public, max-age=0, s-maxage=3600, stale-while-revalidate=86400 |
X-Robots-Tag | noindex |
Agent discovery spec
The agent discovery spec (GET /.well-known/agent.json) now includes:
{
"$schema": "https://docs.farming-labs.dev/schema/agent-manifest.v2.json",
"format": "farming-labs-agent-manifest.v2",
"version": "2",
"profile": "full",
"profiles": {
"full": "/.well-known/agent.json",
"compact": "/.well-known/agent.json?profile=compact"
},
"api": {
"config": "/api/docs?format=config"
},
"config": {
"format": "docs-config-map.v1",
"endpoint": "/api/docs?format=config"
}
}Agents that read the discovery spec can use spec.config.endpoint and spec.config.format to
fetch and identify the config map without hard-coding the route.
Agents with a small first-hop budget can request ?profile=compact. The compact response is
minified and replaces each published skill's companion-file inventory with fileCount; agents can
then follow skills.discovery.index only when they need the expanded catalog.
The root $schema and format identify this as the Farming Labs manifest extension. Dynamic
manifest responses keep Content-Type: application/json and add:
Link: <https://docs.farming-labs.dev/schema/agent-manifest.v2.json>; rel="describedby"; type="application/schema+json"This is intentionally distinct from the opt-in A2A v1 Agent Card at
/.well-known/agent-card.json.
entry and docsPath
entry is the source route and app folder. In a Next.js app with entry: "docs", the framework
expects docs pages under app/docs.
docsPath changes the public route where those pages are exposed. It defaults to the same path as
entry, so app/docs/quickstart/page.mdx is served at /docs/quickstart.
For a docs-only deployment or a docs subdomain, set docsPath: "":
import { defineDocs } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/theme";
export default defineDocs({
entry: "docs",
docsPath: "",
theme: fumadocs(),
});That keeps the files under app/docs and serves them from the public root:
| Source file | Public URL |
|---|---|
app/docs/page.mdx | / |
app/docs/quickstart/page.mdx | /quickstart |
withDocs({}) reads docsPath from docs.config.tsx. You do not need host-specific rewrites in
next.config.ts for a docs-only deployment. Attach the docs deployment to a subdomain like
docs.example.com, then root URLs resolve on that subdomain.
Farm.js adapter — withDocs Vite config
When using the @farming-labs/farmjs adapter, withDocs() accepts an optional vite field on the config object passed to it.
| Option | Type | Description |
|---|---|---|
vite | FarmViteConfigLike | ((config: FarmViteConfigLike) => FarmViteConfigLike) | Optional Vite config object or factory function. The adapter injects its MDX/remark transform plugin ahead of any plugins you supply here. |
Both object and function forms are supported:
import { withDocs } from "@farming-labs/farmjs";
export default withDocs({
// object form
vite: {
plugins: [myPlugin()],
},
});import { withDocs } from "@farming-labs/farmjs";
export default withDocs({
// factory form — receives the resolved config and returns a merged object
vite: (config) => ({ ...config, plugins: [myPlugin()] }),
});In both cases the adapter-owned MDX transform is prepended before any plugins you provide, so MDX processing always runs first.
Root docs own root routes
Use docsPath: "" when the deployment is dedicated to docs. If the same app needs a marketing
homepage at /, keep docsPath unset so docs stay under /docs.
components is merged into the default MDX component map, so you can both add your own components and replace built-ins such as Callout, Tabs, HoverLink, or Prompt. For built-in defaults like theme.ui.components.HoverLink and theme.ui.components.Prompt, see Creating themes. For usage examples and a live demo, see Components.
telemetry and DocsTelemetryConfig
Farming Labs maintainer telemetry for production adoption and coarse agent-optimized feature usage.
It is separate from analytics: telemetry is for framework/package insight, while analytics is for
project-owned product events.
Telemetry can be disabled with either:
DOCS_TELEMETRY=false
# or
DOCS_TELEMETRY_DISABLED=1or:
telemetry: false,| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | production-only enabled | Enable or disable maintainer telemetry |
siteOrigin | string | request/deployment origin | Stable public origin to include in telemetry events |
endpoint | string | Farming Labs endpoint | Override the ingestion endpoint for self-hosting or tests |
Set DOCS_TELEMETRY_INGEST_KEY in both the docs runtime and self-hosted ingestion app when the
endpoint should require a shared x-docs-telemetry-key header.
Telemetry may include an anonymous project identity hash, package/framework versions, deployment
provider signals, site origin, enabled docs features, and coarse agent-surface events such as
llms, markdown, mcp, or skill. It does not collect visitor identities, page views, device
fingerprints, search queries, Ask AI questions, feedback comments, copied content, docs source
content, cookies, or per-user sessions.
Local development origins are ignored even when telemetry is explicitly enabled. Requests from
localhost, *.localhost, IPv4 loopback, and IPv6 loopback or wildcard binding addresses are not
sent or stored.
analytics and DocsAnalyticsConfig
Built-in product and usage event stream. Use it for page views, search activity, Ask AI request counts, feedback, page actions, machine-readable markdown reads, agent feedback routes, and MCP usage. See Analytics for the full guide and event list.
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true when object is passed | Enable or disable the analytics stream |
console | boolean | "log" | "info" | "debug" | true for analytics: true | Log analytics events to the console |
includeInputs | boolean | false | Include raw queries, AI questions, feedback comments, copied text |
onEvent | (event) => void | Promise<void> | — | Callback fired for every analytics event |
analytics: {
onEvent(event) {
console.info(event.type, event.properties);
},
}Inputs stay out unless requested
Search queries, AI questions, feedback comments, and copied content are omitted by default. Set
includeInputs: true only when your project is allowed to store that text.
observability and DocsObservabilityConfig
Built-in trace event stream for agent runtime debugging. Use it for Ask AI and MCP spans, timing, status, previews, errors, retries, and final handoffs. See Observability for the full guide and trace event list.
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true when object is passed | Enable or disable the observability stream |
console | boolean | "log" | "info" | "debug" | true for observability: true | Log trace events to the console |
includeInputs | boolean | false | Include raw input fields on custom observability events |
onEvent | (event) => void | Promise<void> | — | Callback fired for every observability trace event |
observability: {
console: "debug",
onEvent(event) {
console.info(event.type, event.traceId, event.durationMs);
},
}Separate from analytics
analytics.onEvent receives usage events like page_view, api_ai_request, and mcp_tool.
observability.onEvent receives trace events like run.start, model.call, and tool.result.
Ordering
Controls how pages are sorted in the sidebar. Three strategies are available:
"alphabetical" (default)
Pages are sorted alphabetically by folder name. No extra configuration needed.
ordering: "alphabetical","numeric"
Pages are sorted by the order field in frontmatter. Lower numbers appear first. Pages without order are sorted alphabetically after ordered pages.
ordering: "numeric",Then in your page frontmatter:
---
title: "Installation"
order: 1
---Slug-based (OrderingItem[])
Pass an array of { slug, children? } objects for explicit control over page order, including nested pages.
ordering: [
{ slug: "installation" },
{ slug: "cli" },
{ slug: "configuration" },
{
slug: "themes",
children: [
{ slug: "default" },
{ slug: "darksharp" },
{ slug: "pixel-border" },
{ slug: "creating-themes" },
],
},
{
slug: "customization",
children: [
{ slug: "colors" },
{ slug: "typography" },
{ slug: "sidebar" },
{ slug: "components" },
{ slug: "ai-chat" },
{ slug: "page-actions" },
],
},
{ slug: "reference" },
],OrderingItem
| Property | Type | Description |
|---|---|---|
slug | string | Folder name at this level (not the full path) |
children | OrderingItem[] | Ordering for child pages within this folder |
Pages not listed in the array appear alphabetically after the listed ones.
DocsNav
Sidebar header configuration.
| Property | Type | Default | Description |
|---|---|---|---|
title | string | ReactNode | "Docs" | Sidebar header title. React elements supported in Next.js and TanStack Start |
url | string | "/{entry}" | URL the title links to |
nav: {
title: "My Docs",
url: "/docs",
},LlmsTxtConfig
Generated llms.txt files for agent-readable docs indexes. The root files are enabled by default;
section files are opt-in and only add route handling plus discovery metadata.
If a native static file already exists at the same public route, such as public/llms.txt or
SvelteKit static/llms.txt, that file takes precedence and the generated output remains the
fallback.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable or disable llms.txt generation |
baseUrl | string | request origin at runtime | Base URL used for absolute links |
siteTitle | string | nav.title or "Documentation" | Heading shown in generated files |
siteDescription | string | undefined | Description shown below the heading |
maxChars | LlmsTxtMaxCharsConfig | { mode: "warn", chars: 50000 } | Budget for compact llms.txt files |
sections | LlmsTxtSectionConfig[] | [] | Optional section-level llms.txt files |
export default defineDocs({
llmsTxt: {
baseUrl: "https://docs.example.com",
maxChars: {
mode: "warn",
chars: 50_000,
},
sections: [
{
title: "API",
description: "Endpoint, SDK, and integration reference pages.",
match: "/docs/api/**",
maxChars: {
mode: "warn",
chars: 25_000,
},
},
],
},
});Default routes:
/llms.txt/llms-full.txt/.well-known/llms.txt/.well-known/llms-full.txt
For sections, the route is derived from the first match value. For example, /docs/api/**
creates /docs/api/llms.txt and /docs/api/llms-full.txt. Root /llms.txt lists configured
sections first and keeps matching pages inside those section files.
LlmsTxtMaxCharsConfig
| Property | Type | Default | Description |
|---|---|---|---|
mode | "warn" | "error" | "off" | "warn" | Warn, fail the response, or disable budget checks |
chars | number | 50000 | Maximum recommended character count |
LlmsTxtSectionConfig
| Property | Type | Description |
|---|---|---|
title | string | Section title shown in root and section files |
description | string | Optional description shown next to the section link |
match | string | string[] | Public URL matcher, such as /docs/api/** |
maxChars | LlmsTxtMaxCharsConfig | Optional section-specific budget |
DocsMcpConfig
Built-in MCP server configuration for AI clients, IDE agents, and local MCP tooling.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable the MCP server. MCP is on by default; set false to opt out. |
route | string | "/api/docs/mcp" | Streamable HTTP route used by the MCP endpoint |
name | string | nav.title or @farming-labs/docs | MCP server name reported to clients |
version | string | "0.0.0" | Version string reported to clients |
tools | DocsMcpToolsConfig | all enabled | Fine-grained tool toggles |
security | DocsMcpSecurityConfig | see below | HTTP Origin validation, opt-in authentication, and request limits |
export default defineDocs({
entry: "docs",
mcp: {
route: "/api/docs/mcp",
name: "My Docs MCP",
tools: {
listDocs: true,
listPages: true,
listTasks: true,
readTask: true,
getNavigation: true,
searchDocs: true,
readPage: true,
getCodeExamples: true,
getConfigSchema: true,
getContext: true,
},
},
});DocsMcpSecurityConfig
Authentication is opt-in. Omitting authenticate keeps HTTP MCP public, while Origin validation
and the request-size limit still apply. The stdio transport is unaffected.
| Property | Type | Default | Description |
|---|---|---|---|
allowedOrigins | "same-origin" | string[] | callback | "same-origin" | Policy for a supplied HTTP Origin header; requests without Origin are accepted |
authenticate | (context) => principal | null | Response | public | Optional Web Request callback; adding it makes authentication required |
protectedResource | DocsMcpProtectedResourceConfig | — | Opt-in RFC 9728 metadata, Bearer challenges, and endpoint-wide scope requirements; active only with authenticate |
maxBodyBytes | number | 1048576 | Maximum Streamable HTTP POST body size |
cors | boolean | DocsMcpCorsConfig | true | Exact-Origin browser CORS for accepted Origins; set false to suppress headers |
export default defineDocs({
mcp: {
security: {
protectedResource: {
authorizationServers: ["https://auth.example.com"],
scopesSupported: ["docs:read"],
requiredScopes: ["docs:read"],
},
authenticate: async ({ request, pathname, resource }) => {
const user = await authenticateRequest(request, { audience: resource });
return user
? { id: user.id, scopes: user.scopes, claims: { pathname, resource } }
: null;
},
},
},
});POST bodies are capped before allowedOrigins or authenticate callbacks run. Browser preflight
uses the OPTIONS handler and does not run authentication.
DocsMcpProtectedResourceConfig
RFC 9728 protected-resource discovery for OAuth-aware HTTP MCP clients. This object only becomes
active when authenticate is also configured. Omitting both keeps HTTP MCP public by default.
It does not issue or validate tokens: authenticate remains responsible for signature, issuer,
expiry, the exact callback resource audience, and token-derived scope validation. The callback
request retains the real incoming URL and query string.
| Property | Type | Required | Description |
|---|---|---|---|
authorizationServers | readonly string[] | yes | Non-empty list of HTTPS OAuth issuer URLs without query or fragment; loopback HTTP is accepted for development |
scopesSupported | readonly string[] | no | Scopes advertised as scopes_supported |
requiredScopes | readonly string[] | no | Scopes every principal returned by authenticate must include |
resourceName | string | no | Human-readable protected-resource name; defaults to the resolved MCP server name |
resourceDocumentation | string | no | Absolute HTTP(S) URL with human-readable authentication instructions |
The metadata routes are public and support GET and HEAD:
/.well-known/oauth-protected-resource/mcpfor/mcp/.well-known/oauth-protected-resource/.well-known/mcpfor/.well-known/mcp/.well-known/oauth-protected-resource/api/docs/mcpfor the default canonical route; a custommcp.routeproduces the matching path instead
When authenticate returns null, protected-resource mode returns 401 with a Bearer
WWW-Authenticate challenge containing the metadata URL and required scopes. A valid principal
missing a required scope receives 403 with error="insufficient_scope". Returning a Web
Response bypasses framework-generated challenges and preserves the custom response. Use HTTPS for
hosted MCP and authorization-server URLs; plain HTTP is appropriate only for loopback development.
Each alias is a distinct OAuth resource identifier, so token audience validation must accept the
exact endpoint used by the client.
For Next.js, protected MCP is incompatible with basePath; host it at the origin root or terminate
the MCP and RFC 9728 routes at an edge proxy. Inline MCP objects are detected automatically. Pass
the live docs config to withDocs(nextConfig, docsConfig) when MCP configuration is imported,
spread, or shorthand.
DocsMcpCorsConfig
| Property | Type | Default | Description |
|---|---|---|---|
allowedHeaders | string[] | MCP standard headers | Additional browser request headers allowed during preflight |
exposedHeaders | string[] | MCP response headers | Additional response headers exposed to browser JavaScript |
allowCredentials | boolean | false | Emit credentialed CORS for the validated exact Origin; wildcard credentials are never emitted |
maxAgeSeconds | number | 600 | Browser preflight cache lifetime |
The default allowedHeaders set includes Authorization, Content-Type, Last-Event-ID, MCP-Method, MCP-Name, MCP-Protocol-Version, and MCP-Session-Id. Use cors.allowedHeaders to add application-specific headers on top of these defaults. |
DocsMcpToolsConfig
| Property | Type | Description |
|---|---|---|
listDocs | boolean | Expose the list_docs tool |
listPages | boolean | Expose the list_pages tool |
listPageSections | boolean | Expose the list_page_sections tool that returns body-free section discovery metadata for a page (default: true) |
listTasks | boolean | Expose the list_tasks tool for concise actionable-task discovery |
readTask | boolean | Expose the read_task tool for full structured contracts |
getNavigation | boolean | Expose the get_navigation tool |
searchFacets | boolean | Expose the list_search_facets tool for body-free framework, version, package, and tag discovery (default: true) |
listContentChanges | boolean | Expose the list_content_changes tool, which returns body-free document change metadata with polling support via DOCS_MCP_CONTENT_CHANGES_CURRENT_URI and DOCS_MCP_CONTENT_CHANGES_URI_TEMPLATE resources (default: false) |
hydrateContentChanges | boolean | Expose hydrate_content_changes for budget-aware section bodies and deletion tombstones after polling list_content_changes. Accepts a tokenBudget parameter (default 5000; min 4; max 32000) (default: true) |
searchDocs | boolean | Expose the search_docs tool |
readPage | boolean | Expose the read_page tool |
getCodeExamples | boolean | Expose the get_code_examples tool |
getConfigSchema | boolean | Expose the get_config_schema tool |
getContext | boolean | Expose deterministic get_context retrieval with a conservative UTF-8 byte ceiling |
Default MCP surface:
- 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
list_pages and list_docs keep their agent value concise: hasContract, task, outcome, and
appliesTo only. Use list_tasks to filter actionable pages by query/framework/version/package,
then read_task for the full normalized contract (also available as Markdown through read_page).
Framework notes:
- Next.js:
withDocs()auto-generates the default/api/docs/mcproute and public/mcpplus/.well-known/mcprewrites
FarmDocsAdapterOptions
Options accepted by the second argument of withDocs(farmConfig, options) in @farming-labs/farmjs.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable the docs runtime |
configPath | string | auto-detected | Path to docs.config.ts, relative to the Farm application directory |
config | Partial<DocsConfig> | — | Inline docs config overrides |
codeBlockThemes | FarmDocsCodeBlockThemes | — | Shiki themes used to compile Markdown and MDX code blocks |
buildCache | boolean | true | Reuse unchanged production Markdown/MDX compilation and highlighting. Applies to build commands outside watch mode. |
contentRoots | string[] | — | Additional documentation roots outside the Farm application directory. Each path is resolved relative to the application root and added to the dev server's server.fs.allow list so the MDX plugin can compile content from shared or repository-owned locations. |
Production compilation is cached under <Vite cacheDir>/farming-docs/mdx-v1.json, normally
node_modules/.vite/farming-docs/mdx-v1.json. Preserve that directory in CI to reuse work
between builds. Source, module path, highlighting themes, mode, Node version, and compiler
implementation changes invalidate the relevant entries. Missing or unusable caches fall
back to compilation. Set buildCache: false to diagnose builds without cache reuse;
the lower-level docsMdx() plugin accepts the same option.
import { withDocs } from "@farming-labs/farmjs";
export default withDocs({}, {
contentRoots: ["../shared-docs"],
});- TanStack Start / SvelteKit / Astro / Nuxt: use the generated public forwarder for MCP aliases, or add an equivalent framework hook/middleware that forwards to the built-in
MCPhandler - Custom routes: set
mcp.routeindocs.configand update the framework public forwarder so the configured path and the actual endpoint stay aligned
See MCP Server for the route snippets and examples.
createDocsAuthoringMcpServer(options)
Server-only factory for the separate protected authoring MCP. It is not registered by
createDocsMcpServer and is never added to public framework routes.
| Option | Type | Default | Description |
|---|---|---|---|
rootDir | string | required | Git project root |
contentDir | string | entry | Only directory authoring tools may change |
entry | string | "docs" | Docs route/content fallback |
branchPrefix | string | "docs/" | Required prefix for created and published branches |
doctorCommand | command argv tuple | — | Shell-free command executed by authoring_run_doctor |
allowPublish | boolean | false | Register commit/push/draft-PR publication |
baseBranch | string | repository default | Draft PR base branch |
Use runDocsAuthoringMcpStdio() or docs mcp author for local stdio. If a team wraps the factory
in HTTP, the transport must require an authoring-specific OAuth scope. Public docs MCP remains
read-only.
DocsAgentConfig
| Property | Type | Description |
|---|---|---|
okf | boolean | DocsOkfConfig | Publish OKF v0.2 trust metadata in Markdown, search, MCP, doctor, and Static Agent Bundles |
compact | DocsAgentCompactConfig | Defaults for docs agent compact |
evaluations | boolean | DocsAgentEvaluationsConfig | Offline-by-default golden tasks used by docs doctor and docs review |
skills | DocsAgentSkillsInput | Project or workspace skills published by the runtime, Static Agent Bundle, and MCP |
a2a | DocsAgentA2AConfig | Opt-in Agent Card metadata for a real A2A interface |
agent.okf and DocsOkfConfig
OKF metadata makes provenance and freshness explicit for machine readers. agent.okf: true derives
conservative defaults; an object can provide corpus defaults and a staleness policy.
agent: {
okf: {
generatedBy: "software:docs-build",
staleAfterDays: 90,
verified: [{ by: "human:docs-team", at: "2026-08-01" }],
},
},| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true inside the object | Enable OKF projection |
route | string | "/.well-known/okf.json" | Static knowledge-bundle route |
generatedBy | string | "software:@farming-labs/docs" | Generator actor for derived records |
staleAfterDays | number | — | Derive stale_after from the best page timestamp |
sources | DocsOkfSource[] | derived per page | Default source provenance |
generated | { by, at } | derived | Explicit generation record |
verified | { by, at }[] | [] | Verification records; a human: actor produces human-reviewed trust |
status | "draft" | "stable" | "deprecated" | "stable" | Knowledge lifecycle status |
stale_after | string | derived when configured | Explicit expiry date |
Pages can override these fields with okf frontmatter. docs agent export --public writes the
JSON bundle, Markdown frontmatter carries the resolved trust object, local search results expose a
trust field, MCP adds get_trust_metadata, and docs doctor --agent reports expired pages.
agent.contentChanges and DocsAgentContentChangesConfig
Body-free runtime document synchronization feed. Enabled by default on server-rendered adapters; always disabled (false) in static bundles produced by docs agent export.
| Property | Type | Default | Description |
|---|---|---|---|
contentChanges | boolean | DocsAgentContentChangesConfig | true | Enable or configure the content-change feed. Set false to opt out. |
contentChanges.enabled | boolean | true | Whether runtime adapters serve response=changes. |
contentChanges.maxSnapshots | number | 8 | Maximum number of content snapshots retained for delta computation. Capped at 64. |
contentChanges.loadSnapshot | (key: string) => Promise<DocsContentSnapshot | null> | — | Optional callback to load a previously saved snapshot from durable storage. |
contentChanges.saveSnapshot | (key: string, snapshot: DocsContentSnapshot) => Promise<void> | — | Optional callback to persist a snapshot for future delta requests. |
Content-change feed endpoint
All five framework adapters handle GET /api/docs?audience=agent&response=changes. The response carries Content-Type: application/json, an ETag derived from the indexGeneration digest, and standard Cache-Control headers. Conditional GET with If-None-Match returns 304 when the index has not changed.
The response format is docs-content-changes.v1:
| Field | Type | Description |
|---|---|---|
format | "docs-content-changes.v1" | Fixed format identifier |
indexGeneration | string | SHA-256 digest of the current content index |
mode | "snapshot" | "delta" | "reset" | Feed mode selected for this response |
resetRequired | boolean | true when the caller must discard its local state and re-index |
documentCount | number | Total number of documents in the current index |
added | Array<{ canonicalUrl, digest, content? }> | Documents added since the since generation |
modified | Array<{ canonicalUrl, digest, content? }> | Documents whose digest changed since the since generation |
removed | Array<{ canonicalUrl }> | Documents removed since the since generation |
Query parameters:
| Parameter | Description |
|---|---|
audience | Must be agent to select the content-changes response |
response | Must be changes |
since | Optional indexGeneration value from a previous response; omit for a full snapshot |
The agent discovery manifest gains capabilities.contentChanges: boolean and a top-level contentChanges block describing the endpoint, supported modes, query parameters, and HTTP validators (etag: true, bodyFree: true).
DocsAgentSkillsInput and DocsAgentSkillsConfig
type DocsAgentSkillsInput =
| string
| readonly string[]
| {
paths: string | readonly string[];
progressiveDisclosure?: {
maxSkillLines?: number;
instructionTokenBudget?: number;
maxReferenceDepth?: number;
compatibility?: "when-needed" | "always" | "off";
checkScripts?: boolean;
};
};Each path is relative to the project root. It may name a SKILL.md, its containing skill
directory, or a collection directory whose descendants contain SKILL.md files. A path may reach
another project inside the same workspace, but it cannot escape the workspace. Only non-symlink
regular files and directories are accepted. Published content is limited to SKILL.md and regular
files below references/, scripts/, and assets/.
The project-root skill.md remains the site-level compatibility skill. It is always published
alongside agent.skills, using a generated fallback when that root file is absent. Duplicate skill
names are rejected.
docs doctor --agent analyzes every configured skill. docs review runs the same analysis when a
configured skill, companion file, or docs config changes. Defaults follow the Agent Skills
progressive-disclosure guidance: at most 500 lines, an approximately 5,000-token instruction body,
and one local Markdown reference hop. The checks also report broken, escaping, or unpublished
references; environment-dependent skills without compatibility; and bundled scripts without
dependency and validation guidance. Override the thresholds with progressiveDisclosure.
Discovery preserves both client generations:
/.well-known/agent-skills/index.jsonis the Agent Skills v0.2 index- a skill containing only
SKILL.mduses/.well-known/agent-skills/<name>/SKILL.mdas its indexedskill-mdartifact - a skill with companions uses
/.well-known/agent-skills/<name>.tar.gzas a deterministic indexed archive; the SHA-256 digest covers the exact archive bytes - each safe file is also served at
/.well-known/agent-skills/<name>/<path>with its own hash /.well-known/skills/index.json,/.well-known/skills/<name>/skill.md, and legacy companion paths support older clients- Static Agent Bundles emit the same indexes, artifacts, direct files, and hashes
- MCP registers every file at
docs://skills/<name>/<path>; text-compatible files usetext, and binary files use a base64blob
DocsAgentA2AConfig
The generated document follows the A2A v1 Agent Card wire shape.
| Property | Type | Required | Description |
|---|---|---|---|
name | string | yes | Agent display name |
description | string | yes | Agent purpose |
supportedInterfaces | readonly DocsAgentA2AInterfaceConfig[] | yes* | Ordered implemented interfaces; the first is preferred |
interfaceUrl | string | deprecated alternative | Single-interface shorthand; prefer supportedInterfaces |
documentationUrl | string | no | Absolute HTTPS documentation URL; HTTP is limited to loopback development |
provider | { organization: string; url: string } | no | Provider identity and absolute HTTPS URL outside loopback |
version | string | no | Agent version; defaults to "1.0.0" |
iconUrl | string | no | Absolute HTTPS icon URL; HTTP is limited to loopback development |
capabilities | DocsAgentA2ACapabilities | no | Implemented streaming, push notification, extension, and extended-card capabilities |
defaultInputModes | readonly string[] | no | Agent-wide input media types; defaults to ["text/plain"] |
defaultOutputModes | readonly string[] | no | Agent-wide output media types; defaults to ["text/plain"] |
skills | readonly DocsAgentA2ASkill[] | yes with supportedInterfaces | Explicit capabilities implemented by the A2A service; optional only for the deprecated shorthand |
securitySchemes | Readonly<Record<string, DocsAgentA2ASecurityScheme>> | no | Named API key, HTTP auth, OAuth 2, OpenID Connect, or mutual TLS schemes |
securityRequirements | readonly DocsAgentA2ASecurityRequirement[] | no | Agent-wide alternatives over named security schemes |
protocolVersion | string | deprecated shorthand only | Protocol for interfaceUrl; retains the historical "0.3" default |
protocolBinding | string | deprecated shorthand only | Binding for interfaceUrl; defaults to "HTTP+JSON" |
supportedInterfaces must contain at least one { url, protocolBinding?, protocolVersion?, tenant? } entry. Each entry defaults to A2A protocol 1.0 over HTTP+JSON. The core bindings are
JSONRPC, GRPC, and HTTP+JSON; a custom binding is an absolute URI. Ordering is meaningful: the
first interface is the client's preferred starting point. The deprecated interfaceUrl form
cannot be combined with supportedInterfaces; it retains protocol 0.3 as its default so existing
services are not silently misadvertised. Set its protocolVersion explicitly for other versions.
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.
DocsAgentA2ASkill requires id, name, description, and non-empty tags; it can also set
examples, capability-specific inputModes and outputModes, and securityRequirements. It
does not have a URL in A2A v1. URLs for configured Agent Skill documents and artifacts remain in
/.well-known/agent-skills/index.json, its legacy compatibility index, and MCP resources. The
preferred supportedInterfaces form requires explicit A2A skills. Published documentation skills
are projected only when a2a.skills is omitted from the deprecated shorthand, preserving its
previous behavior.
Security schemes use the A2A v1 discriminated wrappers, and each requirement maps a configured scheme name to its scope list. For example:
securitySchemes: {
bearer: {
httpAuthSecurityScheme: {
scheme: "bearer",
bearerFormat: "JWT",
},
},
},
securityRequirements: [
{
schemes: {
bearer: { list: ["docs.read"] },
},
},
],Use exactly one wrapper per named scheme. Requirement objects are alternatives; multiple schemes
inside one object are required together. capabilities.extendedAgentCard: true requires at least
one declared security scheme and a non-empty applicable security requirement.
When a2a is set, /.well-known/agent-card.json uses the strict v1 shape with
supportedInterfaces; it does not emit legacy top-level url, protocolVersion,
preferredTransport, or per-skill url fields. Capabilities default to non-streaming with no push
notifications, while security metadata is omitted unless configured. When a2a is omitted, the
route is not published. Do not point an advertised interface at a docs, Ask AI, or MCP route; those
transports do not implement A2A.
DocsAgentEvaluationsConfig
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable configured golden-task evaluation |
tokenBudget | number | 4000 | Default hard UTF-8 context-byte ceiling |
topK | number | 5 | Default ranked retrieval depth |
surface | "mcp-context" | "configured-search" | "ask-ai-context" | "mcp-context" | Retrieval/context surface measured by the suite |
allowNetwork | boolean | false | Allow framework-managed external search, HTTP answers, and runtime example execution |
searchTimeoutMs | number | 30000 | Per-attempt timeout for configured-search and Ask AI retrieval; transient failures are retried twice |
answer | DocsAgentEvaluationAnswerProvider | — | Opt-in callback or HTTP provider for actual-answer evaluation |
tasks | DocsAgentGoldenTask[] | [] | Representative tasks; an empty list is reported as unmeasured |
Each task requires id, query, and expect.relevantSources. It can set filters.framework,
filters.version, filters.locale, per-task surface, and per-task tokenBudget or topK.
Expectations can include allowedSources, forbiddenSources, requiredCitations, minRecallAtK,
maxFirstRelevantRank, minUsefulByteRatio, scope, answer, and code-fence matches through
examples. filters narrow retrieval; expect.scope asserts the framework, version, or locale of
the returned sources without narrowing them first.
DocsAgentEvaluationAnswerProvider is either { provider: "callback", run } or
{ provider: "http", endpoint, headers?, timeoutMs? }. The callback receives the task id, query,
filters, surface, context, retrieved sources, and an abort signal, but never receives the task
expectations. The serializable HTTP request omits that callback-only signal. Both
providers return { text, citations? }. HTTP auth is opt-in through headers, and header values
never appear in reports. Managed external search and HTTP requests require allowNetwork: true,
while callbacks remain explicit user code.
DocsAgentGoldenSafetyExpectation
Optional adversarial safety assertions on a golden task. All fields are evaluator-only and must never contain real credentials or private data. Exported as DocsAgentGoldenSafetyExpectation from @farming-labs/docs.
| Property | Type | Description |
|---|---|---|
promptInjection | DocsAgentGoldenPromptInjectionExpectation | Assert prompt-injection resistance using canary markers |
poisonedCitations | boolean | Assert that retrieval rejects injected citation payloads |
authenticatedContent | DocsAgentGoldenAuthenticatedContentExpectation | Assert that authenticated content does not leak into public retrieval |
freshness | DocsAgentGoldenFreshnessExpectation | Assert retrieval-index generation and per-source content digests |
rejectConflictingFrameworkVersions | boolean | Assert that sources with conflicting framework version provenance are rejected |
deletedSectionTombstones | boolean | Assert that deleted-section tombstones are surfaced |
queryVariants | DocsAgentGoldenQueryVariant[] | Additional query variant kinds to evaluate alongside the primary query |
DocsAgentGoldenPromptInjectionExpectation has markers: string[] (canary fragments that must appear in retrieved context) and an optional forbiddenAnswerText?: string[] (fragments the answer must not repeat; defaults to markers).
DocsAgentGoldenAuthenticatedContentExpectation has optional forbiddenSources?: string[] (protected URLs that public retrieval must not expose) and forbiddenText?: string[] (canary fragments that must not appear in context or answers).
DocsAgentGoldenFreshnessExpectation has optional indexGeneration?: string (expected retrieval-index generation digest) and sourceDigests?: Record<string, string> (expected full-document digest keyed by canonical page or section URL).
DocsAgentGoldenQueryVariant has kind: DocsAgentGoldenQueryVariantKind (a string such as "rephrased" or "adversarial").
All five types are exported from @farming-labs/docs and @farming-labs/docs/server.
DocsAgentGoldenExpectedExample.verification accepts "present", "syntax", or "execute".
Runnable examples default to syntax verification and explicitly non-runnable examples default to
presence. Execution is never inferred: it requires allowNetwork: true,
verification: "execute", and an enabled codeBlocks.validate configuration in "report" mode;
only an actual PASS receives credit.
Evaluation is offline by default on mcp-context, with no implicit model or network call. The
configured-search and ask-ai-context surfaces exercise the real configured retrieval/context
paths, and provider failures or timeouts fail instead of falling back to a different surface.
Configured-search preserves provider ranking and does not supplement it with local hits. See
Golden agent evaluations for a complete example.
DocsReviewConfig
Docs Review configuration for changed docs files, score thresholds, and GitHub Actions behavior.
Omitted review config is enabled by default. Set review: false to opt out of the review command
and generated workflow. The generated CI file is a small repository-root wrapper around the hosted
reusable workflow at farming-labs/docs/.github/workflows/docs-review-reusable.yml@main.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable Docs Review |
ci | boolean | DocsReviewCiConfig | enabled | GitHub Actions behavior and workflow generation |
score | DocsReviewScoreConfig | { threshold: 80 } | Healthy score threshold and severity weights |
rules | DocsReviewRulesConfig | default severities | Per-rule severity overrides |
export default defineDocs({
entry: "docs",
review: {
ci: {
name: "agent-docs-review",
mode: "warn",
annotations: true,
comment: true,
},
score: {
threshold: 85,
},
},
});DocsReviewCiConfig
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable review CI workflow generation |
name | string | "docs-review" | GitHub Actions job/check name for the generated workflow |
mode | "off" | "warn" | "block" | "warn" | Report only, warn without failing, or block unhealthy PRs |
annotations | boolean | true | Emit GitHub workflow annotations |
comment | boolean | true | Reserved for PR comments from the official action/bot |
DocsReviewRulesConfig
| Property | Default | Description |
|---|---|---|
brokenLinks | "error" | Check internal docs links |
frontmatter | "error" | Check title and description frontmatter |
duplicateSlugs | "error" | Check duplicate docs routes |
invalidMdx | "error" | Check markdown/MDX read and frontmatter parsing |
configExamples | "warn" | Check config examples when possible |
codeFenceMetadata | "warn" | Check code fences for metadata such as title |
runnableMetadata | "warn" | Check runnable snippets for package manager metadata |
agentContext | "warn" | Validate structured contracts and detect duplicate, generic, incomplete, or ambiguous agent context |
commandHealth | "warn" | Statically validate package managers, scripts, working directories, and docs CLI commands |
relatedCoverage | "suggestion" | Check valid related-page coverage for actionable docs |
configConfidence | "warn" | Report partial static fallback when the config module cannot be evaluated |
agentSurfaceDrift | "error" | Detect discovery, resolved config, contract, route, MCP tool, and schema drift |
goldenTasks | "warn" | Report failed or unmeasured configured golden tasks |
agentSkills | "warn" | Check configured skills for progressive disclosure, reference integrity, compatibility, and script guidance |
Rule severities accept "off", "suggestion", "warn", or "error".
DocsRobotsConfig
Runtime and generated robots.txt policy for docs and agent-readable routes. Server-rendered apps
serve /robots.txt through the shared docs handler when no static file already owns that path; the
CLI writes or appends a static file for static export and committed policies.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable or disable the robots policy |
path | string | public/robots.txt, or static/robots.txt for SvelteKit | File path used by docs robots generate |
baseUrl | string | sitemap.baseUrl or llmsTxt.baseUrl in the CLI | Public site URL used for the Sitemap: line |
ai | boolean | "allow" | "disallow" | "allow" | Explicitly allow or disallow common AI crawler user agents |
aiUserAgents | string | string[] | built-in common AI crawler list | Extra AI user-agent names to include beside the defaults |
extraRules | DocsRobotsRule[] | [] | Extra robots rule blocks appended after the generated agent policy |
export default defineDocs({
robots: {
enabled: true,
baseUrl: "https://docs.example.com",
path: "public/robots.txt",
ai: "allow",
},
});Generated policy covers the docs entry, .md routes, llms.txt, sitemap routes, AGENTS.md, skill.md, MCP
aliases, agent discovery routes, and common AI crawler user agents. Existing files are preserved by
default; use docs robots generate --append to add or update the managed block, or --force to
replace the file.
The agent discovery JSON advertises this surface as robots.enabled, robots.route, and
robots.defaultRoute, so agents can find the policy without guessing.
DocsRobotsRule
| Property | Type | Description |
|---|---|---|
userAgent | string | string[] | User-agent name or names for this rule block |
allow | string | string[] | Routes to allow |
disallow | string | string[] | Routes to disallow |
crawlDelay | number | Optional crawl-delay value emitted as-is |
See CLI for docs robots generate and Configuration for the
high-level setup guide.
DocsSearchConfig
Search is enabled by default. If you do not configure anything, the docs API uses the built-in simple adapter with section-based chunking.
export default defineDocs({
entry: "docs",
search: true,
});GET /api/docs?query=<query> searches the human projection by default. The exact lowercase
audience=agent query parameter selects the agent projection:
GET /api/docs?query=installation&audience=agentThe human projection contains shared plus human-only content; the agent projection contains shared
plus agent-only content. audience=human, a missing value, and invalid values all resolve to human.
This is content projection, not authentication or authorization.
Built-in providers:
simple— zero-config docs search with section chunkingtypesense— external Typesense collection with optional hybrid modealgolia— external Algolia indexmcp— Streamable HTTP MCP endpoint that exposes asearch_docstoolcustom— your own adapter implementation
SimpleDocsSearchConfig
| Property | Type | Default | Description |
|---|---|---|---|
provider | "simple" | implied | Use the built-in search adapter |
enabled | boolean | true | Toggle search on or off |
maxResults | number | 10 | Maximum results to return |
chunking | DocsSearchChunkingConfig | { strategy: "section" } | Controls whether indexing happens per page or per section |
search: {
provider: "simple",
maxResults: 8,
chunking: {
strategy: "section",
},
},TypesenseDocsSearchConfig
| Property | Type | Default | Description |
|---|---|---|---|
provider | "typesense" | — | Use Typesense as the search backend |
baseUrl | string | — | Typesense API base URL |
collection | string | — | Collection name used for docs documents |
apiKey | string | — | Search API key |
adminApiKey | string | — | Optional admin key used for on-demand indexing |
maxResults | number | 10 | Maximum results to return |
syncOnSearch | boolean | true when adminApiKey is present | Re-index docs on the first search request |
queryBy | string[] | built-in fields | Custom Typesense query_by fields |
mode | "keyword" | "hybrid" | "keyword" | Keyword search only, or keyword + embeddings |
embeddings | DocsSearchEmbeddingsConfig | — | Semantic embeddings config for hybrid search |
chunking | DocsSearchChunkingConfig | { strategy: "section" } | Chunking strategy used before indexing/search |
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",
},
},pnpm dlx @farming-labs/docs search sync --typesenseAlgoliaDocsSearchConfig
| Property | Type | Default | Description |
|---|---|---|---|
provider | "algolia" | — | Use Algolia as the search backend |
appId | string | — | Algolia application id |
indexName | string | — | Algolia index name |
searchApiKey | string | — | Search API key |
adminApiKey | string | — | Optional admin key used for on-demand indexing |
maxResults | number | 10 | Maximum results to return |
syncOnSearch | boolean | true when adminApiKey is present | Re-index docs on the first search request |
chunking | DocsSearchChunkingConfig | { strategy: "section" } | Chunking strategy used before indexing/search |
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,
},pnpm dlx @farming-labs/docs search sync --algoliaMcpDocsSearchConfig
| Property | Type | Default | Description |
|---|---|---|---|
provider | "mcp" | — | Use an MCP endpoint as the search backend |
endpoint | string | — | Streamable HTTP MCP endpoint |
headers | Record<string, string> | — | Extra headers sent to initialize and tool calls |
toolName | string | "search_docs" | MCP tool name used for search |
protocolVersion | string | "2025-11-25" | MCP protocol version header |
maxResults | number | 10 | Maximum results to request |
search: {
provider: "mcp",
endpoint: "/mcp",
},Relative endpoints are resolved against the current docs API request URL. That makes local setups like the Next example easy to test without hardcoding a host.
CustomDocsSearchConfig
| Property | Type | Description |
|---|---|---|
provider | "custom" | Use your own adapter |
adapter | DocsSearchAdapter | DocsSearchAdapterFactory | Search runtime implementation |
maxResults | number | Maximum results to return |
chunking | DocsSearchChunkingConfig | Chunking strategy used before your adapter runs |
import { createCustomSearchAdapter, defineDocs } from "@farming-labs/docs";
export default defineDocs({
entry: "docs",
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,
}));
},
}),
});DocsSearchAdapter
Custom adapters receive normalized source pages and chunked search documents so you can focus on
retrieval and ranking instead of rebuilding the docs scan pipeline. DocsSearchQuery.audience and
DocsSearchAdapterContext.audience are both "human" | "agent" and contain the resolved request
projection; context.documents is projected to match. Hosted index synchronization always uses
the human projection. Custom adapters should apply query.audience while filtering and ranking
provider results.
import type { DocsSearchAdapter } from "@farming-labs/docs";
export const adapter: DocsSearchAdapter = {
name: "my-search",
async search(query, context) {
return context.documents.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,
}));
},
};DocsSearchChunkingConfig
| Property | Type | Default | Description |
|---|---|---|---|
strategy | "page" | "section" | "section" | Chunk by whole page or split by headings |
SearchSyncOptions — additional sync fields
These options are available on the search sync CLI command and on the SearchSyncOptions interface:
| Property | Type | Description |
|---|---|---|
syncNamespace | string | Optional namespace that scopes the synced corpus. Maximum 1024 characters. Set via --sync-namespace flag or the DOCS_SEARCH_SYNC_NAMESPACE environment variable. A value exceeding 1024 chars is rejected with error code invalid-search-sync-namespace. |
siteUrl | string | Canonical public URL embedded in indexed documents during sync. Set via --site-url flag. |
DocsSearchSourcePage — additional fields
| Property | Type | Description |
|---|---|---|
canonicalUrl | string | Optional canonical public URL override for this page. Relative values are resolved against the request base URL. |
agentLastModified | string | ISO-8601 modification time for an explicit agent-source override file (e.g. agent.md), tracked separately from the page's own lastModified. |
Retrieval provenance types and exports
The following are exported from @farming-labs/docs and @farming-labs/docs/server:
| Export | Kind | Description |
|---|---|---|
buildDocsRetrievalDigestProjection | function | Builds a projection object containing a SHA-256 digest (sha256:<hex>) of normalized page content, suitable for change-detection in search indexes. |
enrichDocsSearchDocumentsWithProvenance | function | Attaches retrieval provenance metadata (scope, canonical URL, digest) to an array of search documents. |
resolveDocsRetrievalLastModified | function | Resolves the canonical last-modified date for a page, preferring the lastmod frontmatter value over the filesystem mtime. |
DocsRetrievalSourceProvenance | type | Normalized provenance metadata attached to a retrieved source, including canonicalUrl, lastModified, digest, and scope fields. |
DocsRetrievalSourceScope | type | Applicability metadata for a retrieved source: audience, locale, framework, version, versionGroups, and package. |
EnrichDocsSearchDocumentsWithProvenanceOptions | type | Options accepted by enrichDocsSearchDocumentsWithProvenance. |
HTTP cache integrity helpers
The following are exported from @farming-labs/docs:
| Export | Kind | Description |
|---|---|---|
createDocsCacheableResponse | function | Build a cacheable Response for an agent-surface request. Computes a strong ETag ("<64-hex-char sha256>"), an RFC 9530 Content-Digest: sha-256=:…: header, and an optional Last-Modified header. Returns an empty-body response for HEAD requests and 304 Not Modified when If-None-Match matches. |
formatDocsContentDigest | function | Format a SHA-256 hex digest as an RFC 9530 Content-Digest field value (sha-256=:…:). |
requestHasFreshDocsDate | function | Return true when the request's If-Modified-Since header indicates the client already has a fresh copy. |
requestMatchesDocsEtag | function | Return true when the request's If-None-Match header matches the supplied strong ETag. |
resolveDocsHttpDate | function | Parse a string, Date, or null into an RFC 7231 HTTP-date string, returning null for invalid or absent values. |
sha256DocsContent | function | Hash exact bytes (string or Uint8Array) with portable SHA-256; returns the lowercase hex digest. Works in Node, edge, and browser runtimes. |
digestDocsContent | function | Hash a string or Uint8Array and return the result in sha256:<hex> provenance format. |
DocsCacheableResponseOptions | type | Options accepted by createDocsCacheableResponse: request, content, status?, headers?, sha256? (pre-computed hex digest), and lastModified? (string | Date | null). |
ETag format change: Agent-surface responses now use a strong ETag ("<64-hex-char sha256>") instead of the previous weak form (W/"…"). Clients that cache or compare ETags must update their If-None-Match logic accordingly. Conditional GET and HEAD requests with a matching strong ETag receive 304 Not Modified with the Content-Digest header preserved.
lastModified on discovery responses: DocsStandardsDiscoveryResponseOptions and CreateDocsStandardsResponseOptions both accept a new optional lastModified?: string | Date | null field. When supplied, the response includes a Last-Modified header and supports If-Modified-Since conditional requests.
DocsSearchFacetValue
One selectable search-scope value and the number of matching source pages.
| Property | Type | Description |
|---|---|---|
value | string | Normalized filter value |
count | number | Number of source pages that carry this value |
DocsSearchFacet
Bounded values for one search-scope field.
| Property | Type | Description |
|---|---|---|
valueCount | number | Number of distinct values before the response limit is applied |
truncated | boolean | Whether additional values were omitted from this response |
values | DocsSearchFacetValue[] | Ordered list of values and counts |
total | number | Total number of distinct values. Alias of valueCount for pagination clients |
hasMore | boolean | Whether another page of values can be requested using nextCursor |
nextCursor | string | undefined | Opaque cursor for the next page. Omitted when this is the final page |
DocsSearchFacetsResponse
Cheap, body-free discovery response for the search scopes available in an index. Returned by GET /api/docs?audience=agent&response=facets with Cache-Control: public, max-age=60.
Two optional query parameters refine the ?response=facets request:
| Parameter | Type | Description |
|---|---|---|
facet | framework | version | package | tags | Pin the response to a single field and enable cursor-based pagination for that field. Requires response=facets. |
limit | integer 1–100 | Maximum number of facet values to return per page. Defaults to the built-in cap when omitted. |
cursor | string | Opaque continuation token returned as nextCursor in a previous response. Accepted alongside response=facets (and the existing response=structured) when facet is set. |
When facet is present, only the named field is computed and returned. Check hasMore in the response to determine whether another page is available, then pass the returned nextCursor value as cursor in the next request to retrieve it.
| Property | Type | Description |
|---|---|---|
format | "docs-search-facets.v1" | Fixed format identifier |
filters | DocsSearchFilters | Active filters applied to the facet computation |
matchedPageCount | number | Number of source pages matched after filters |
facets | Record<string, DocsSearchFacet> | Facet values keyed by field name (framework, version, package, tags) |
BuildDocsSearchFacetsOptions
Options accepted by buildDocsSearchFacets. Exported from @farming-labs/docs.
| Property | Type | Description |
|---|---|---|
pages | DocsSearchSourcePage[] | Source pages to compute facets over |
search | DocsSearchConfig | Active search config |
audience | "human" | "agent" | Content projection used when filtering pages |
locale | string | Active locale |
baseUrl | string | Public base URL for provenance |
filters | DocsSearchFilters | Optional filters to apply before computing facets |
indexGeneration | string | Digest of the current index for cache freshness |
scope | object | Optional audience/locale scope |
facet | DocsSearchFilterField | Optional single field to compute and paginate (framework, version, package, or tags) |
limit | number | Maximum number of facet values to return (1–100) |
cursor | string | Opaque continuation cursor from a previous response |
| Property | Type | Description |
| -------- | ---- | ----------- |
pages | DocsSearchSourcePage[] | Source pages to compute facets over |
search | DocsSearchConfig | Active search config |
audience | "human" | "agent" | Content projection used when filtering pages |
locale | string | Active locale |
baseUrl | string | Public base URL for provenance |
filters | DocsSearchFilters | Optional filters to apply before computing facets |
indexGeneration | string | Digest of the current index for cache freshness |
scope | object | Optional audience/locale scope |
buildDocsSearchFacets
Builds a DocsSearchFacetsResponse from source pages without fetching page content. Exported from @farming-labs/docs.
import { buildDocsSearchFacets } from "@farming-labs/docs";
const response = await buildDocsSearchFacets(options);
// response.format === "docs-search-facets.v1"DocsSearchEmbeddingsConfig
| Property | Type | Default | Description |
|---|---|---|---|
provider | "ollama" | — | Embeddings provider for hybrid Typesense mode |
model | string | — | Embedding model id |
baseUrl | string | "http://127.0.0.1:11434" | Ollama base URL |
Notes:
search: falsedisables search entirely- Search requires the docs API route; static export hides the UI because there is no server route
GET /api/docs?query=...remains human by default; only exactaudience=agentopts into agent content- On Next.js, generated routes from
withDocs()passdocsConfigdirectly intocreateDocsAPI - MCP-backed search works with relative endpoints like
/mcpor/.well-known/mcpand absolute remote endpoints likehttps://docs.example.com/mcp - same-origin MCP
search_docsroutes receive the resolvedaudience; setforwardAudience: trueonly when a remote endpoint or custom tool supports the same argument; human-projection requests fall back to local search until then - If MCP-backed search points at the same relative MCP route, the built-in
search_docstool falls back to simple search internally to avoid recursive loops - On custom/manual Next routes, import
createDocsAPIfrom@farming-labs/next/apiand pass the whole config:createDocsAPI(docsConfig)
@farming-labs/theme/api still works for now
@farming-labs/theme/api remains supported as a compatibility import path today, but prefer
@farming-labs/next/api in Next.js apps. The theme-level path will be deprecated.
ApiReferenceConfig
Generates API reference pages from framework route conventions or a local/remote OpenAPI JSON or YAML document.
Use route scanning when the API lives in the same project 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.
Renderer dependencies are bundled
The native "farming-labs" renderer has no extra renderer dependency and is the default on every
framework. You do not need to install fumadocs-openapi, fumadocs-ui, @scalar/core, or
related packages separately. Set renderer: "scalar" on any framework or
renderer: "fumadocs" on Next.js to use the bundled alternatives. Use the Next.js
fumadocsRenderer() helper when the Fumadocs renderer needs playground or runtime options.
Important
apiReference in docs.config controls scanning, theming, routeRoot, and exclude.
In Next.js, withDocs() also generates the route automatically.
Farm.js serves the route through withDocs(). In TanStack Start, SvelteKit,
Astro, and Nuxt, you must additionally add the framework route handler for /{path}.
The native renderer uses the same normalized operation model as Markdown, Ask AI, search, and MCP. It includes operation search and grouping, stable operation permalinks, version switching, parameters, request and response schemas, authentication metadata, copyable cURL examples, and responsive light/dark UI. Each operation also includes a Try it tab that executes requests in the browser and renders status, timing, response headers, and formatted response bodies.
The request console starts from the operation's first server, parameters, media type, and schema examples. Readers can change the server, path/query/header parameters, request body, browser credential mode, and supported bearer, basic, or API-key authentication values before sending. Authentication values stay in memory and are never persisted by Farming Labs. Cookie parameters remain browser-managed; choose Include cookies when the target API relies on them. POST, PUT, PATCH, and DELETE operations require an explicit confirmation before Send request is enabled.
Browser security still applies
Cross-origin APIs must allow the docs origin through CORS. A relative server URL resolves against the current docs origin, so replace it with an absolute API URL when a remotely loaded contract uses a server path relative to its own host. The console supports JSON and raw request bodies; specialized multipart file controls are not generated yet.
The interactive console is part of the native "farming-labs" renderer. Selecting "scalar" or
"fumadocs" preserves that renderer's own request tooling and presentation.
OpenAPI document mode
specUrl accepts project-relative JSON/YAML files such as ./openapi.yaml, hosted JSON/YAML
URLs, and request-relative URLs such as /api/openapi.json.
Contract validation and source limits
Sources must use Swagger 2.0 or OpenAPI 3.0.x, 3.1.x, or 3.2.x. Invalid operations and duplicate
operationId values are rejected before rendering. Each local or remote document is limited to
5 MiB, a build can load at most 32 documents, and remote requests time out after 10 seconds.
Agent schema discovery
When apiReference is enabled, the shared docs API also serves
GET /api/docs?format=openapi. The agent discovery spec advertises that OpenAPI schema URL, and
generated llms.txt / AGENTS.md / skill.md output points agents there before they scrape API reference pages.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true inside the object | Enable generated API reference pages |
renderer | ApiReferenceRendererInput | "farming-labs" | Renderer name or { name, options } object. Scalar works on every framework; Fumadocs and its typed fumadocsRenderer() helper are available on Next.js |
path | string | "api-reference" | URL path where the generated API reference lives |
specUrl | string | — | 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 |
overlays | readonly string[] | [] | Ordered OpenAPI Overlay 1.0/1.1 JSON/YAML sources for the single specUrl |
versions | Readonly<Record<string, ApiReferenceVersionConfig>> | — | Named OpenAPI sources with stable routes, labels, and optional per-version overlays |
defaultVersion | string | — | Required key from versions; selects the default unversioned response |
routeRoot | string | "api" | Filesystem route root to scan. Bare values like "api" resolve inside app/ or src/app/; full values like "app/internal-api" and "src/app/v2/api" are supported |
exclude | string[] | [] | Route entries to omit from the generated reference. Accepts URL-style paths like "/api/hello" or route-root-relative values like "hello" / "hello/route.ts" |
mcp | boolean | DocsOpenApiMcpConfig | false | Deny-by-default projection of selected OpenAPI operations into MCP tools |
apiReference: {
enabled: true,
path: "api-reference",
routeRoot: "api",
exclude: ["/api/internal/health", "internal/debug"],
},apiReference: {
enabled: true,
path: "api-reference",
specUrl: "./openapi.yaml",
renderer: "farming-labs",
},Pin an alternate renderer when you want its existing presentation:
apiReference: {
enabled: true,
specUrl: "./openapi.yaml",
renderer: "scalar", // or "fumadocs" in Next.js
},Pass typed Fumadocs playground and runtime options on Next.js:
import { fumadocsRenderer } from "@farming-labs/next/fumadocs-renderer";
import { authProviders } from "./app/api-reference/auth-providers";
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 },
}),
},The helper accepts Fumadocs createOpenAPIPage() options together with the proxyUrl and
disableCache server options. Browser-side providers and callbacks must not contain secrets. Proxy
and OAuth URLs must point to application-owned routes.
apiReference: {
enabled: true,
path: "api-reference",
specUrl: "https://petstore3.swagger.io/api/v3/openapi.json",
},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",
},When specUrl is set, routeRoot and exclude are ignored and the API reference is rendered
from the configured document instead. Project-relative sources must remain inside the project
root; use an explicit file: URL for a trusted local contract outside it.
versions is mutually exclusive with specUrl and top-level overlays. Every version has a
stable /{path}/{version} UI route and GET /api/docs?format=openapi&version={version} schema
identity. The unversioned routes use defaultVersion. Discovery, llms.txt, and the API catalog
publish all version identities.
Overlay sources support local files, file: URLs, absolute HTTP(S) URLs, and request-relative
URLs. Farming Labs supports OpenAPI Overlay 1.0.x and 1.1.x, applies overlays and actions in their
declared order, and evaluates targets with RFC 9535 JSONPath before $ref bundling. Invalid overlay
documents, invalid or unmatched targets, incompatible updates, and invalid final OpenAPI contracts
are reported as source diagnostics. Overlay sources use the same timeout, size, and local
containment protections as OpenAPI sources.
$ref supports internal and external URI references with JSON Pointer fragments. External local
or remote JSON/YAML documents are fetched once per build and rewritten into the returned OpenAPI
document, so downstream renderers and projections receive a self-contained contract. References
from project-relative entry documents stay inside the project root. The synchronous builder cannot
load HTTP(S) references; use buildApiReferenceOpenApiDocumentAsync() or a framework route for
mixed local/remote contracts. Named anchors and $dynamicRef resolution are not supported yet.
Normalized OpenAPI operation model
Server integrations can import buildNormalizedOpenApiModel and its NormalizedOpenApi* types
from @farming-labs/docs/server. The additive API produces stable operation IDs and slugs, method
and path selectors, tags, parameters, request bodies, responses, schemas, examples, servers, and
the security schemes used by each operation. Farming Labs uses the same model for OpenAPI-backed
MCP tools, keeping renderer and agent projections aligned.
DocsOpenApiMcpConfig
| Property | Type | Default | Description |
|---|---|---|---|
operations | readonly string[] | [] | Allowed operationId or METHOD /path selectors |
baseUrl | string | first OpenAPI server | API origin used for operation requests |
allowMutations | boolean | false | Permit allowlisted POST, PUT, PATCH, and DELETE operations |
headers | Record<string, string> | callback | — | Server-owned credentials resolved for each operation |
timeoutMs | number | 10000 | Request timeout |
allowInsecureHttp | boolean | false | Permit plain HTTP destinations for explicitly trusted development environments |
allowPrivateNetwork | boolean | false | Permit loopback, private, link-local, and reserved destinations |
resolveHost | callback | Node DNS | Server-owned DNS resolver used to validate every destination |
maxRedirects | number | 0 | Validated redirects to follow; redirects are blocked by default |
maxResponseBytes | number | 1000000 | Maximum response bytes read into an MCP result |
requestsPerMinute | number | 60 | Per-principal, per-operation request budget |
maxConcurrentRequests | number | 4 | Per-principal, per-operation concurrency budget |
An operation can also opt in with x-farming-labs-mcp: true. Security requirements and only the
referenced components.securitySchemes are included in the tool metadata. Credentials remain
server-owned and are applied after parsed tool parameters.
Every request and redirect is DNS-checked before egress. HTTPS, public-network destinations, bounded response streaming, rate limits, and concurrency limits are enforced by default. Development servers on private networks must opt in explicitly; never enable those exceptions for untrusted OpenAPI documents or model-supplied destinations.
This does not remove the framework route requirement on adapters that do not own the application
server. Next.js and Farm.js serve the route through withDocs(). TanStack Start, SvelteKit, Astro,
and Nuxt still need their /{path} handler files so the docs app has a route that serves the
generated API reference page.
When output: "export" is used in Next.js, the generated API reference route is skipped automatically because it requires a server route handler.
Scanned route conventions:
- Next.js:
app/api/**/route.tsandsrc/app/api/**/route.ts - TanStack Start:
src/routes/api.*.tsand nested route files inside the configured route root - Farm.js:
app/api/**/route.tsandsrc/app/api/**/route.ts - SvelteKit:
src/routes/api/**/+server.tsor+server.js - Astro:
src/pages/api/**/*.tsor.js - Nuxt:
server/api/**/*.tsor.js
Minimal route handlers for non-Next frameworks:
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 a second src/routes/api-reference.$.ts file with the same handler and
createFileRoute("/api-reference/$").
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.
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.
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.
ChangelogConfig
Generates changelog listing and entry pages from dated MDX folders inside the docs content tree.
Current support
The turn-key generated changelog pages are currently wired in Next.js when you use
withDocs().
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true inside the object | Enable generated changelog pages |
path | string | "changelog" | URL path where the changelog listing lives inside the docs layout |
contentDir | string | "changelog" | Source directory under the docs content root. Example: app/docs/changelog/2026-03-04/page.mdx |
title | string | "Changelog" | Listing page title |
description | string | — | Listing page description shown in the header and metadata |
search | boolean | true | Show the built-in changelog search field |
actionsComponent | ReactNode | Component | — | Custom action content rendered in the changelog rail |
changelog: {
enabled: true,
path: "changelogs",
contentDir: "changelog",
title: "Changelog",
description: "Latest product updates and release notes.",
search: true,
},With entry: "docs" and the config above, the public pages render at:
/docs/changelogs/docs/changelogs/2026-03-04
When you use withDocs(), the route files are generated automatically. There is no separate
__changelog.generated.tsx file to maintain.
ChangelogFrontmatter
Optional frontmatter fields for each changelog entry:
| Property | Type | Description |
|---|---|---|
title | string | Entry title shown in the feed and entry page |
description | string | Short summary shown in the feed and metadata |
image | string | Cover image path or URL |
authors | string | string[] | Author name(s) shown in entry meta |
version | string | Compact version badge like v0.1.0 |
tags | string[] | Badge list shown in the feed and entry page |
pinned | boolean | Keep important entries pinned to the top of the listing |
draft | boolean | Hide the entry from generated routes and search indexes |
---
title: "OpenAPI mode is now the default"
description: "The docs example now ships with the faster API reference experience."
version: "v0.1.13"
tags: ["api-reference", "next"]
---GithubConfig
GitHub repository configuration. Enables "Edit on GitHub" links in page footers.
| Property | Type | Default | Description |
|---|---|---|---|
url | string | required | Repository URL (e.g. "https://github.com/my-org/repo") |
branch | string | "main" | Branch name |
directory | string | — | Content subdirectory for monorepos |
// Simple
github: "https://github.com/my-org/my-docs",
// Monorepo
github: {
url: "https://github.com/my-org/monorepo",
branch: "main",
directory: "apps/docs/docs",
},The "Edit on GitHub" link only appears when both
urlanddirectoryare provided.
onCopyClick and CodeBlockCopyData
Optional callback on DocsConfig. Fired when the user clicks the copy button on a code block — in addition to copying to the clipboard. Use it for analytics, logging, or custom behavior. See Code block copy callback for setup (Next.js / TanStack Start vs SvelteKit / Astro / Nuxt).
CodeBlockCopyData
Argument passed to onCopyClick(data: CodeBlockCopyData).
| Property | Type | Description |
|---|---|---|
title | string | undefined | Code block title (e.g. from fenced code meta), if present |
content | string | Raw code content (what was copied) |
url | string | Current page URL at the time of copy |
language | string | undefined | Language/syntax hint (e.g. "tsx", "bash"), if present |
import type { CodeBlockCopyData } from "@farming-labs/docs";
import { defineDocs } from "@farming-labs/docs";
export default defineDocs({
entry: "docs",
onCopyClick(data: CodeBlockCopyData) {
// data.title, data.content, data.url, data.language
console.log("Copied:", data.language, data.title ?? "(no title)");
},
});feedback, FeedbackConfig, AgentFeedbackConfig, and feedback payloads`
Optional docs page feedback UI. When enabled, a built-in "Good / Bad" prompt is rendered at the end of the page and emits a feedback payload when the user clicks one of the buttons.
FeedbackConfig
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Show or hide the feedback UI |
question | string | "How is this guide?" | Prompt shown above the buttons |
positiveLabel | string | "Good" | Label for the positive button |
negativeLabel | string | "Bad" | Label for the negative button |
requireComment | boolean | false | Require a non-empty comment before feedback can be submitted |
onFeedback | (data: DocsFeedbackData) => void | — | Callback fired when the user clicks a button |
agent | boolean | AgentFeedbackConfig | true | Agent feedback routes served through /api/docs |
DocsFeedbackData
| Property | Type | Description |
|---|---|---|
value | "positive" | "negative" | Which feedback button the user clicked |
title | string | undefined | Current page title, when available |
description | string | undefined | Current page description, when available |
url | string | Full current page URL |
pathname | string | Current pathname without origin |
path | string | Alias of pathname |
entry | string | Docs entry root, e.g. "docs" |
slug | string | Page slug relative to the docs entry |
locale | string | undefined | Active locale when docs i18n is enabled |
import type { DocsFeedbackData } from "@farming-labs/docs";
import { defineDocs } from "@farming-labs/docs";
export default defineDocs({
entry: "docs",
feedback: {
enabled: true,
onFeedback(data: DocsFeedbackData) {
console.log("Feedback:", data.value, data.slug, data.url);
},
},
});This docs site persists feedback through a local Next.js route at
website/app/api/feedback/route.ts, backed by the DocsFeedback Prisma model in
website/prisma/schema.prisma.
See Page feedback for framework-specific notes. The built-in UI does not require a separate client bridge file.
AgentFeedbackConfig
Optional machine-facing feedback routes for agents, scripts, or evaluation flows.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable or disable the agent feedback endpoints |
route | string | "/api/docs/agent/feedback" | Public HTTP route for feedback submission |
schemaRoute | string | ${route}/schema | Public HTTP route for the machine-readable schema |
schema | Record<string, unknown> | built-in payload schema | Schema used to validate the payload object |
onFeedback | (data: DocsAgentFeedbackData) => Promise<void> | void | — | Async callback fired after a valid submission |
Notes:
- agent feedback routes are enabled by default; set
feedback: falseorfeedback: { agent: false }to opt out feedback.agentdoes not enable the human footer UI by itself- the request body always uses
{ context?, payload } - without
feedback.agent.onFeedback, valid submissions return{ ok: true, handled: false } - submitted feedback is not added to Ask AI prompts or docs search context by the framework
- in Next.js,
withDocs()adds the public route rewrites automatically - Astro, SvelteKit, TanStack Start, and Nuxt expose the same feedback contract through the shared
/api/docs?feedback=agentquery route - the shared
/api/docshandler is still the source of truth, so?feedback=agentalso works - agents can discover APIs through
GETorHEAD /.well-known/api-catalog(RFC 9727), and can discover integrity-checked skills throughGETorHEAD /.well-known/agent-skills/index.json - agents should discover site identity, locale config, capability flags, search, active markdown routes,
Accept: text/markdown,Signature-Agentsupport, JSON-LD structured data,llms.txt, OpenAPI schema routes, sitemap routes,robots.txt,AGENTS.md,skill.md, skills, MCP, and feedback routes withGET /.well-known/agent.json;GET /.well-known/agentis the public fallback andGET /api/docs/agent/specis the canonical framework route
DocsAgentFeedbackContext
| Property | Type | Description |
|---|---|---|
page | string | undefined | Docs page path such as "/docs/installation" |
url | string | undefined | Full docs URL when available |
slug | string | undefined | Docs slug relative to the entry root |
locale | string | undefined | Active locale when docs i18n is enabled |
source | string | undefined | Arbitrary source label such as "md-route", "mcp", or "api" |
DocsAgentFeedbackData
| Property | Type | Description |
|---|---|---|
context | DocsAgentFeedbackContext | undefined | Optional docs/page transport context |
payload | Record<string, unknown> | Agent feedback payload validated by feedback.agent.schema |
analyzeDocsAgentFeedback() converts recurring failed reports into evidence clusters, golden-task
drafts, issue drafts, and a draft docs PR description. readDocsAgentFeedbackFile() accepts JSON
arrays/objects and JSON Lines. Both are exported from @farming-labs/docs/server; the CLI wrapper
is docs agent feedback --input <path> [--write].
import { defineDocs } from "@farming-labs/docs";
export default defineDocs({
entry: "docs",
feedback: {
agent: {
enabled: true,
route: "/api/docs/agent/feedback",
async onFeedback(data) {
console.log(data.context?.page, data.payload);
},
},
},
});{
"context": {
"page": "/docs/installation",
"source": "md-route"
},
"payload": {
"task": "install docs in an existing Next.js app",
"understanding": "partial",
"outcome": "implemented",
"confidence": 0.78
}
}Quick checks:
curl "http://127.0.0.1:3000/.well-known/agent.json"
curl -I "http://127.0.0.1:3000/.well-known/api-catalog"
curl "http://127.0.0.1:3000/.well-known/agent-skills/index.json"
curl "http://127.0.0.1:3000/AGENTS.md"
curl "http://127.0.0.1:3000/skill.md"
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"}}'ThemeToggleConfig
Light/dark mode toggle in the sidebar.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Show or hide the toggle |
default | "light" | "dark" | "system" | "system" | Forced theme when enabled: false |
mode | "light-dark" | "light-dark-system" | "light-dark" | Toggle behavior |
// Show toggle (default)
themeToggle: true,
// Hide toggle, follow system preference
themeToggle: false,
// Hide toggle, force dark mode
themeToggle: { enabled: false, default: "dark" },
// Show toggle with system option
themeToggle: { mode: "light-dark-system" },BreadcrumbConfig
Breadcrumb navigation above page content.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Show or hide breadcrumbs |
component | Component | — | Custom breadcrumb component (Next.js only) |
breadcrumb: true, // show (default)
breadcrumb: false, // hide
breadcrumb: { enabled: false }, // hideSidebarConfig
Sidebar visibility and customization. See Sidebar for the full guide including custom sidebar components.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Show or hide the sidebar |
component | (props: SidebarComponentProps) => ReactNode | — | Custom sidebar render function. Receives { tree, collapsible, flat }. Supported in Next.js and TanStack Start — other frameworks use slots. |
footer | ReactNode | — | Content rendered below nav items |
banner | ReactNode | — | Content rendered above nav items |
collapsible | boolean | true | Whether sidebar is collapsible on desktop |
flat | boolean | false | Render all items flat (Mintlify-style, no collapsible groups) |
folderIndexBehavior | "link" | "toggle" | "hidden" | adapter default | Whether folder parents with their own page navigate, only expand/collapse, or render as label-only groups |
folderIndexBehaviorOverrides | Record<string, "link" | "toggle" | "hidden"> | — | Selective per-folder overrides keyed by the folder landing-page URL |
Folder landing pages can still override both of those with frontmatter:
---
sidebar:
folderIndexBehavior: "toggle"
---SidebarComponentProps
Props passed to a custom sidebar component:
| Property | Type | Description |
|---|---|---|
tree | SidebarTree | Full page tree with all parent-child relationships |
collapsible | boolean | Whether folders are collapsible |
flat | boolean | Whether folders are rendered flat |
SidebarTree
| Property | Type | Description |
|---|---|---|
name | string | Root name (e.g. "Docs") |
children | SidebarNode[] | Top-level sidebar items |
SidebarNode
A union of SidebarPageNode | SidebarFolderNode.
SidebarPageNode
| Property | Type | Description |
|---|---|---|
type | "page" | Node type |
name | string | Display name |
url | string | Page URL |
icon | unknown | Icon from registry |
SidebarFolderNode
| Property | Type | Description |
|---|---|---|
type | "folder" | Node type |
name | string | Display name |
icon | unknown | Icon from registry |
index | SidebarPageNode | Folder's landing page |
children | SidebarNode[] | Child pages and sub-folders |
collapsible | boolean | Whether this folder collapses |
defaultOpen | boolean | Whether it starts open |
sidebar: true, // show (default)
sidebar: false, // hide
sidebar: { collapsible: false }, // non-collapsible
sidebar: { flat: true }, // Mintlify-style flat
sidebar: { folderIndexBehavior: "toggle" }, // all folder parents only toggle children
sidebar: {
folderIndexBehavior: "link",
folderIndexBehaviorOverrides: {
"/docs/components": "toggle",
"/docs/customization": "toggle",
},
},
sidebar: { // custom component
component: ({ tree }) => <MySidebar tree={tree} />,
},DocsTheme
Theme configuration returned by theme factories.
| Property | Type | Description |
|---|---|---|
name | string | Theme identifier (e.g. "fumadocs", "darksharp") |
ui | UIConfig | Visual configuration — colors, typography, layout |
Created via theme factories:
import { fumadocs } from "@farming-labs/theme";
import { darksharp } from "@farming-labs/theme/darksharp";
import { pixelBorder } from "@farming-labs/theme/pixel-border";
import { colorful } from "@farming-labs/theme/colorful";
theme: fumadocs({ ui: { /* overrides */ } }),UIConfig
Fine-grained visual configuration passed to theme factories.
| Property | Type | Description |
|---|---|---|
colors | ColorsConfig | Theme color tokens |
typography | TypographyConfig | Font families, heading styles |
radius | string | Global border-radius (e.g. "0px", "0.5rem") |
layout | LayoutConfig | Content width, sidebar width, TOC settings |
codeBlock | CodeBlockConfig | Syntax highlighting and line numbers |
sidebar | SidebarStyleConfig | Sidebar visual style |
card | CardConfig | Card component styling |
ColorsConfig
All color tokens. Accepts any CSS color value (hex, rgb, oklch, hsl).
| Token | Description |
|---|---|
primary | Primary brand color |
primaryForeground | Text color on primary backgrounds |
background | Page background |
foreground | Default text color |
muted | Muted background (e.g. code blocks) |
mutedForeground | Text on muted backgrounds |
border | Default border color |
card | Card background |
cardForeground | Card text color |
accent | Accent color |
accentForeground | Text on accent backgrounds |
secondary | Secondary color |
secondaryForeground | Text on secondary backgrounds |
popover | Popover/dropdown background |
popoverForeground | Popover text color |
ring | Focus ring color |
colors: {
primary: "oklch(0.72 0.19 149)",
background: "hsl(0 0% 2%)",
border: "#262626",
},LayoutConfig
| Property | Type | Default | Description |
|---|---|---|---|
contentWidth | number | — | Max width of content area in px |
sidebarWidth | number | — | Sidebar width in px |
tocWidth | number | — | Table of contents width in px |
toc.enabled | boolean | true | Show table of contents |
toc.depth | number | 3 | Max heading depth for TOC |
header.height | number | — | Header height in px |
header.sticky | boolean | true | Sticky header |
CodeBlockConfig
| Property | Type | Default | Description |
|---|---|---|---|
showLineNumbers | boolean | false | Show line numbers |
showCopyButton | boolean | true | Show copy button |
theme | string | — | Shiki theme for light mode |
darkTheme | string | — | Shiki theme for dark mode |
SidebarStyleConfig
| Property | Type | Default | Description |
|---|---|---|---|
style | "default" | "bordered" | "floating" | "default" | Sidebar visual style |
background | string | — | Background color override |
borderColor | string | — | Border color override |
TypographyConfig
typography: {
font: {
style: {
sans: "Inter, sans-serif",
mono: "JetBrains Mono, monospace",
},
h1: { size: "2.25rem", weight: 700, letterSpacing: "-0.02em" },
h2: { size: "1.75rem", weight: 600 },
h3: { size: "1.25rem", weight: 600 },
body: { size: "1rem", lineHeight: "1.75" },
small: { size: "0.875rem" },
},
},FontStyle
Used for each heading level and body text.
| Property | Type | Description |
|---|---|---|
size | string | CSS font-size (e.g. "2.25rem", "clamp(1.8rem, 3vw, 2.5rem)") |
weight | string | number | CSS font-weight (e.g. 700, "bold") |
lineHeight | string | CSS line-height (e.g. "1.2", "28px") |
letterSpacing | string | CSS letter-spacing (e.g. "-0.02em") |
AIConfig
RAG-powered AI chat. See Ask AI for usage guide.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable AI chat |
mode | "search" | "floating" | "search" | UI mode: integrated in search or floating widget |
position | "bottom-right" | "bottom-left" | "bottom-center" | "bottom-right" | Floating button position (only when mode: "floating") |
floatingStyle | "panel" | "modal" | "popover" | "full-modal" | "panel" | Floating chat visual style |
triggerComponent | Component | — | Custom floating button component |
model | string | "gpt-4o-mini" | LLM model name (OpenAI-compatible) |
systemPrompt | string | auto-generated | Custom system prompt |
baseUrl | string | "https://api.openai.com/v1" | OpenAI-compatible API base URL |
apiKey | string | process.env.OPENAI_API_KEY | API key for the LLM provider |
maxResults | number | 5 | Number of doc pages to include as RAG context |
useMcp | boolean | DocsAskAIMcpConfig | false | Route Ask AI retrieval through MCP search_docs without changing the search API |
suggestedQuestions | string[] | — | Pre-filled questions shown when chat is empty |
aiLabel | string | "AI" | Display name for the AI assistant |
feedback | boolean | DocsAskAIFeedbackConfig | true | Copy, like, and dislike action row for completed answers |
onActions | (data: DocsAskAIActionData) => void | Promise<void> | — | Callback for copy, like, and dislike response actions |
packageName | string | inferred from docs context | Optional package-name override for import examples |
docsUrl | string | — | Base URL the AI uses for links |
loader | string | "shimmer-dots" | Loading indicator variant ("shimmer-dots", "circular", "dots", "typing", "wave", "bars", "pulse", "pulse-dot", "terminal", "text-blink", "text-shimmer", "loading-dots") |
loadingComponent | (props: { name: string }) => ReactNode | — | Custom loading component (overrides loader, Next.js only) |
Floating styles
| Style | Description |
|---|---|
"panel" | Tall panel sliding up from the button. No backdrop. |
"modal" | Centered modal with backdrop overlay, like Cmd+K search. |
"popover" | Compact popover near the button. |
"full-modal" | Full-screen immersive overlay with pills for suggested questions. |
PageActionsConfig
Action buttons shown on each docs page. See Page Actions for usage guide.
| Property | Type | Default | Description |
|---|---|---|---|
position | "above-title" | "below-title" | "below-title" | Where to render the buttons |
alignment | "left" | "right" | "left" | Horizontal alignment of the action row |
copyMarkdown | boolean | CopyMarkdownConfig | false | "Copy Markdown" button |
openDocs | boolean | OpenDocsConfig | false | "Open in LLM" dropdown |
CopyMarkdownConfig
| Property | Type | Default | Description |
|---|---|---|---|
format | "markdown" | "text" | "markdown" | Content format copied by the button: "markdown" fetches the .md route; "text" reads visible page text |
enabled | boolean | false | Show the button |
label | string | "Copy page" | Button label shown before the page is copied |
copiedLabel | string | "Copied!" | Button label shown after a successful copy |
includeTitle | boolean | false | Prepend the current page title to copied content. In "markdown" format this produces a # Title heading; in "text" format it prepends the plain title. Skipped if the content already begins with the title. |
OpenDocsConfig
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Show the dropdown |
target | "markdown" | "page" | "source" | "github" | "markdown" | URL inserted into {url} for built-in provider prompts |
prompt | string | "Read this documentation: {url}" | Prompt text for built-in providers |
providers | OpenDocsProvider[] | ChatGPT, Claude | LLM/tool providers |
OpenDocsProvider
| Property | Type | Description |
|---|---|---|
id | string | Built-in provider preset ("chatgpt", "claude", "cursor", "gemini", "copilot", "perplexity", "github") |
name | string | Display name override |
icon | ReactNode | Icon rendered next to the name |
target | "markdown" | "page" | "source" | "github" | Per-provider target override |
prompt | string | Per-provider prompt override |
mode | "web" | "app" | Cursor-only option. "app" uses the Cursor app deeplink |
urlTemplate | string | Custom URL template. Supports {prompt}, {url}, {pageUrl}, {markdownUrl}, {sourceUrl}, {mdxUrl}, and {githubUrl}. |
promptUrlTemplate | string | Optional URL template used by the built-in Prompt component. {prompt} is replaced with the prompt text. |
pageActions: {
copyMarkdown: { enabled: true },
openDocs: {
enabled: true,
target: "markdown",
providers: ["chatgpt", "claude", "cursor"],
},
},LastUpdatedConfig
Controls the "Last updated" date shown on docs pages.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Show or hide the last-updated date |
label | string | "Last updated" | Text prefix shown before the formatted date |
position | "footer" | "below-title" | "footer" | Where to render the date: next to "Edit on GitHub" or below the page title |
lastUpdated: {
label: "Updated",
position: "below-title",
},Set label to an empty string to show only the formatted date with no prefix. Use any locale-appropriate string to rebrand the prefix text (e.g. "Dernière mise à jour" for French docs).
Date formatting uses UTC so the calendar day is deployment-timezone independent.
ReadingTimeConfig
Estimated reading-time label shown on docs pages.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Explicitly opt in to the computed read-time label |
wordsPerMinute | number | 220 | Word-count rate used for the estimate |
format | "long" | "short" | "long" | Label style: 3 min read for "long" or 3 min for "short" |
includeCode | boolean | false | Whether fenced and inline code blocks count toward the reading time estimate. Disabled by default so the label reflects human prose. Set to true for code-heavy guides. |
readingTime: {
enabled: true,
wordsPerMinute: 220,
format: "short",
},Notes:
-
readingTimeis disabled by default; an options object must includeenabled: true -
the estimate strips code blocks, inline code, links, images, HTML, and URLs before counting
-
set
includeCode: trueto preserve fenced and inline code word counts in the estimate (useful for code-heavy guides where examples make up a significant portion of the page) -
formatchanges only the rendered label; it does not affect word counting or per-page overrides -
page frontmatter can disable or override the value with
readingTime: false,readingTime: true, orreadingTime: 8, even when the global config is off -
placement follows the page title slot: it sits directly below the page actions row when page actions are shown above or below the title
DocsMetadata
SEO metadata configuration. Page title and description also feed the generated Schema.org
JSON-LD structured data for each docs page.
| Property | Type | Default | Description |
|---|---|---|---|
titleTemplate | string | — | Page title template. %s is replaced with the page title. |
description | string | — | Default meta description |
twitterCard | "summary" | "summary_large_image" | — | Twitter card type |
metadata: {
titleTemplate: "%s – My Docs",
description: "Documentation for my project",
},Structured Data
Every docs page emits Schema.org JSON-LD automatically. The generated object uses
@type: "TechArticle" and includes the page title, description, canonical URL, dateModified,
and a BreadcrumbList.
No separate config flag is required. Absolute URLs are resolved from the first available public
base URL in sitemap.baseUrl, llmsTxt.baseUrl, robots.baseUrl, or ai.docsUrl; otherwise the
page route is emitted as a relative URL.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Installation",
"description": "Install the framework",
"url": "https://docs.example.com/docs/installation",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://docs.example.com/docs/installation"
},
"dateModified": "2026-05-10T00:00:00.000Z",
"breadcrumb": {
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Docs",
"item": "https://docs.example.com/docs"
},
{
"@type": "ListItem",
"position": 2,
"name": "Installation",
"item": "https://docs.example.com/docs/installation"
}
]
}
}
</script>The agent discovery spec advertises this as capabilities.structuredData and
structuredData.schema: "https://schema.org/TechArticle".
OGConfig
Open Graph image configuration. When endpoint is set, each docs page gets a dynamic OG image: the framework passes the page’s title and description (from frontmatter) to the endpoint, and the layout injects the resulting image URL into og:image and twitter:image meta tags. For a full guide (how dynamic OG works, what context is passed, and how the docs website implements it), see OG Images.
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable OG image generation |
type | "static" | "dynamic" | — | Static images or dynamic generation |
endpoint | string | — | API endpoint for dynamic OG images |
defaultImage | string | — | Fallback OG image URL |
Example (Next.js): In docs.config.ts, set og.endpoint to your OG route path. Then implement app/api/og/route.tsx (or route.ts) that accepts title and description query params and returns a 1200×630 image (e.g. using ImageResponse from next/og).
og: {
enabled: true,
type: "dynamic",
endpoint: "/api/og",
},Testing your OG image
- Start the dev server for the app that serves the OG route (e.g.
cd website && pnpm devorcd examples/next && pnpm dev). - Open the OG URL in the browser, e.g.
http://localhost:3000/api/og?title=Test&description=My+description
(use your app’s port if different). - After changing the OG route code, refresh the page. If the image doesn’t update, do a hard refresh (e.g. Cmd+Shift+R on macOS, Ctrl+Shift+R on Windows) or open the URL in a private/incognito window. You can also add a cache-busting query param (e.g.
&_=1,&_=2) to force a new request.
Page Frontmatter
Each docs page (.mdx / .md) supports these frontmatter fields:
| Property | Type | Description |
|---|---|---|
title | string | Required. Page title, shown in sidebar and page heading |
description | string | Page description, used for meta tags and search |
related | string[] | Related doc URLs rendered as a comma-separated metadata line in .md routes and MCP page output |
agent | PageAgentFrontmatter | Per-page structured task contract and compaction token budget |
sidebar | PageSidebarFrontmatter | Per-folder sidebar metadata such as folderIndexBehavior overrides |
readingTime | boolean | number | Disable the estimate for one page (false) or force an exact minute count |
icon | string | Icon key from the icons registry. Shows in sidebar. |
order | number | Sort order in the sidebar (only when ordering: "numeric"). Lower numbers first. |
tags | string[] | Tags for categorization |
ogImage | string | Shorthand for a single OG image path. Ignored if openGraph is set. |
openGraph | PageOpenGraph | Full Open Graph object (e.g. images: [{ url, width?, height? }]). When set, replaces generated OG from the config (e.g. dynamic endpoint). |
twitter | PageTwitter | Full Twitter card object (e.g. card, images). When set, replaces generated twitter metadata. |
hidden | boolean | If true, hide the page from the sidebar (page remains reachable by URL). Optional; behavior may vary by adapter. |
PageAgentFrontmatter
Page-level metadata for machine-readable docs workflows.
| Property | Type | Description |
|---|---|---|
tokenBudget | number | Approximate output token target for docs agent compact on this page |
access | DocsPageAccessPolicy | Deny-by-default scopes and claims for agent-facing page surfaces |
task | string | Concrete task the page helps an agent complete |
outcome | string | Observable end state that indicates completion |
appliesTo | { framework?, version?, package? } | Applicability values; each accepts a string or string array |
prerequisites | string[] | Setup or conditions that must already be true |
files | string[] | Files the task is expected to read or change |
commands | (string | PageAgentCommand)[] | Command strings or { run, cwd?, description? } objects |
sideEffects | string[] | Material state changes or external effects |
verification | (string | PageAgentVerification)[] | Checks or { description?, run?, expect? } objects |
rollback | string[] | Steps that restore the prior state |
failureModes | (string | PageAgentFailureMode)[] | Symptoms or { symptom, resolution? } objects |
---
title: "Installation"
description: "Install the framework"
agent:
tokenBudget: 777
access:
visibility: authenticated
scopes:
- docs:private
claims:
role:
- admin
- editor
task: Install the framework
outcome: The docs route renders locally.
appliesTo:
framework: nextjs
version: ">=16"
package: "@farming-labs/next"
prerequisites:
- The app uses the App Router
files:
- package.json
- next.config.ts
commands:
- run: pnpm add @farming-labs/docs @farming-labs/next
description: Install the required packages
verification:
- run: pnpm dev
expect: The docs route returns HTTP 200
failureModes:
- symptom: The route returns 404
resolution: Confirm withDocs wraps the Next.js config
---Notes:
agent.tokenBudgetoverrides globalagent.compact.maxOutputTokensdefaults and CLI--max-output-tokensfor that one pageagent.access.visibility: authenticatedrequires a principal; every listed scope and claim must match, while claim arrays accept any overlapping value- protected pages are omitted from public Markdown discovery,
llms.txt, search/Ask AI, content sync, unauthenticated MCP results, and static agent bundles; application HTML authorization remains the host application's responsibility - structured fields are normalized into served Markdown frontmatter, a deterministic
## Agent Contractsection, search/Ask AI context, Schema.orgHowToJSON-LD, and MCP task tools - MCP list-page results contain only
hasContract,task,outcome, andappliesTo; calllist_tasksorread_taskwhen an agent needs actionable discovery or the full contract docs reviewreports malformed or unknown fields with closest-name suggestions, missingtaskoroutcome, commands without verification, and side effects without rollback guidance- if the page already has a sibling
agent.md,docs agent compactcompacts that file - if the page does not have
agent.md, the command compacts the generated machine-readable page output and 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.tokenBudgetagent.compact.reviewManifestPath(default.farming-labs/agent-compaction-reviews.json) sets the project-relative path to the JSON manifest for hash-bound reviewed compaction exceptions; override per run with--review-manifest- inherited
minOutputTokensis clamped down totokenBudgetwhen needed so the compression API never receivesmin_output_tokens > max_output_tokens
PageSidebarFrontmatter
Page-level sidebar metadata used while building the docs navigation tree.
| Property | Type | Description |
|---|---|---|
folderIndexBehavior | "link" | "toggle" | "hidden" | Override how this folder page behaves in the default sidebar when it has child pages |
---
title: "Components"
sidebar:
folderIndexBehavior: "toggle"
---Notes:
- this is only read from folder landing pages such as
page.mdx/index.md - it overrides global
sidebar.folderIndexBehavior - it also overrides
sidebar.folderIndexBehaviorOverridesfor that same folder folderIndexBehavior: "hidden"keeps the folder route available but omits the folder index link from the sidebar tree; in flat mode this becomes a label-only group with child links only, and direct visits to the hidden parent route redirect to the first visible child
Static OG example — use openGraph and twitter in frontmatter to serve a static image instead of the dynamic OG endpoint:
---
title: "Title of Docs"
description: "Title of the docs goes here"
icon: "harddrive"
openGraph:
images:
- url: "/og/path-to-image/image.png"
width: 1200
height: 630
twitter:
card: "summary_large_image"
images:
- "/og/path-to-image/image.png"
---Minimal example (shorthand ogImage or generated OG):
---
title: "Getting Started"
description: "Set up your docs in 5 minutes"
related:
- /docs/installation
- /docs/customization/agent-primitive
readingTime: 4
icon: "rocket"
order: 1
---Data types
Frontmatter is YAML between the --- fences. Supported value types:
| Type | Example in frontmatter | Use in framework fields |
|---|---|---|
string | title: "Hello" or title: Hello | title, description, icon, ogImage, framework, version, locale |
number | order: 1 | order |
boolean | hidden: true | Optional (e.g. hide from sidebar) |
string[] | tags: [a, b] or tags: ["a", "b"] | tags |
You can use other YAML types (e.g. nested objects) for custom keys; the fields in the table above are used by the framework for sidebar, SEO, JSON-LD structured data, OG, search, scoped MCP context, and machine-readable markdown. Quote numeric version values so they remain strings for MCP filtering.
How frontmatter is passed
- Next.js — The MDX pipeline uses
remark-frontmatterandremark-mdx-frontmatterwithname: "metadata". The parsed frontmatter is exposed as themetadataexport from the page. The layout andgenerateMetadatareceive it when resolving the page (e.g. viagetPage(params)), and it is used forcreatePageMetadata, sidebar tree (title, icon, order), description maps, and JSON-LD structured data. - TanStack Start / SvelteKit / Astro / Nuxt — The docs loader (e.g.
import.meta.globor content layer) reads each.md/.mdxfile, parses frontmatter, and builds the nav tree and page data. Frontmatter is passed to the layout as page data (e.g.page.dataor the props your content component receives). The same fields (title,description,icon,order,tags,ogImage,framework,version,locale) are used for sidebar, meta tags, JSON-LD structured data, search, and scoped MCP context.
So in all frameworks, the same frontmatter fields drive titles, descriptions, icons, ordering, JSON-LD, and OG; only the mechanism (MDX metadata vs. content loader page.data) differs. The related field is intentionally machine-readable first: add URL strings, and .md routes plus MCP page output render them next to Description as Related: /docs/a, /docs/b without adding a visible related-links block to the human page UI. A sibling agent.md remains the page-content override, so add a Related: line there manually if the override should include it.
createTheme(options)
Create a custom theme from scratch. See Creating Themes.
import { createTheme } from "@farming-labs/docs";
export const myTheme = createTheme({
name: "my-theme",
ui: {
colors: { primary: "#ff4d8d", background: "#0a0a0a" },
radius: "0px",
sidebar: { style: "bordered" },
},
});extendTheme(base, overrides)
Extend an existing theme with overrides.
import { extendTheme } from "@farming-labs/docs";
import { fumadocs } from "@farming-labs/theme";
export const myTheme = extendTheme(fumadocs(), {
ui: {
colors: { primary: "oklch(0.72 0.19 149)" },
},
});TanStack Start Server API
createDocsServer(config)
Creates the TanStack Start docs server adapter.
import { createDocsServer } from "@farming-labs/tanstack-start/server";Parameters:
| Property | Type | Description |
|---|---|---|
config | DocsConfig | The config object from defineDocs() |
rootDir | string | Optional project root used to resolve contentDir. Defaults to process.cwd() |
Returns:
| Property | Type | Description |
|---|---|---|
load | ({ pathname, locale? }) => Promise<DocsServerLoadResult> | Loads docs page data and the sidebar tree for a pathname |
GET | ({ request }) => Response | Search, Markdown, discovery standards, llms.txt, OpenAPI, sitemap, AGENTS.md, and skill.md handler |
HEAD | ({ request }) => Promise<Response> | Bodyless public-read handler that preserves GET status and headers |
POST | ({ request }) => Promise<Response> | AI chat endpoint handler |
MCP | DocsMcpHttpHandlers | MCP HTTP transport handlers |
import { createDocsServer } from "@farming-labs/tanstack-start/server";
import docsConfig from "../../docs.config";
export const docsServer = createDocsServer({
...docsConfig,
rootDir: process.cwd(),
});TanStack Start Components
| Component | Import | Description |
|---|---|---|
TanstackDocsPage | @farming-labs/tanstack-start/react | Built-in page renderer that resolves the compiled MDX module and wraps it in the shared docs layout |
RootProvider | @farming-labs/theme/tanstack | Root provider for theme state, search, and AI UI |
import { createFileRoute } from "@tanstack/react-router";
import { TanstackDocsPage } from "@farming-labs/tanstack-start/react";
import { loadDocPage } from "@/lib/docs.functions";
import docsConfig from "../../docs.config";
export const Route = createFileRoute("/docs/")({
loader: () => loadDocPage({ data: { pathname: "/docs" } }),
component: DocsIndexPage,
});
function DocsIndexPage() {
const data = Route.useLoaderData();
return <TanstackDocsPage config={docsConfig} data={data} />;
}import { createFileRoute } from "@tanstack/react-router";
import { isDocsStandardsDiscoveryRequest } from "@farming-labs/docs";
import { docsServer } from "@/lib/docs.server";
import docsConfig from "../../docs.config";
async function handleUnsupportedDocsMethod(request: Request) {
if (
isDocsStandardsDiscoveryRequest(new URL(request.url), {
apiRoute: docsConfig.cloud?.apiRoute,
})
) {
return docsServer.GET({ request });
}
return new Response("Method Not Allowed", {
status: 405,
headers: { Allow: "GET, HEAD, POST" },
});
}
export const Route = createFileRoute("/api/docs")({
server: {
handlers: {
GET: async ({ request }) => docsServer.GET({ request }),
HEAD: async ({ request }) => docsServer.HEAD({ request }),
POST: async ({ request }) => docsServer.POST({ request }),
ANY: async ({ request }) => handleUnsupportedDocsMethod(request),
},
},
});TanStack Start also uses the
docsMdx()Vite plugin from@farming-labs/tanstack-start/viteso.mdxfiles compile without extra local module-loader glue.
SvelteKit Server API
createDocsServer(config)
Creates all server-side functions for a SvelteKit docs site.
import { createDocsServer } from "@farming-labs/svelte/server";Parameters:
| Property | Type | Description |
|---|---|---|
config | DocsConfig | The config object from defineDocs() |
_preloadedContent | Record<string, string> | Pre-loaded markdown files from import.meta.glob. Required for serverless. |
Returns:
| Property | Type | Description |
|---|---|---|
load | (event) => Promise<PageData> | Layout load function (use in +layout.server.js) |
GET | (event) => Response | Search endpoint handler |
HEAD | (event) => Promise<Response> | Bodyless public-read handler |
POST | (event) => Promise<Response> | AI chat endpoint handler |
MCP | DocsMcpHttpHandlers | MCP HTTP transport handlers |
import { createDocsServer } from "@farming-labs/svelte/server";
import config from "./docs.config";
const contentFiles = import.meta.glob(
["/docs/**/*.{md,mdx,svx}", "/AGENTS.md", "/AGENT.md", "/skill.md", "/.farming-labs/sitemap-manifest.json"],
{
query: "?raw",
import: "default",
eager: true,
},
) as Record<string, string>;
export const { load, GET, HEAD, POST, MCP } = createDocsServer({
...config,
_preloadedContent: contentFiles,
});SvelteKit Components
Imported from @farming-labs/svelte-theme:
| Component | Props | Description |
|---|---|---|
DocsLayout | tree, config, title?, titleUrl? | Main layout with sidebar, nav, search, AI |
DocsContent | data, config | Page content with TOC, breadcrumb, footer nav |
DocsPage | entry, tocEnabled, breadcrumbEnabled, previousPage, nextPage, editOnGithub, lastModified | Low-level page wrapper (used by DocsContent) |
Astro Server API
createDocsServer(config)
Same as SvelteKit — returns { load, GET, HEAD, POST, MCP }.
import { createDocsServer } from "@farming-labs/astro/server";Parameters:
| Property | Type | Description |
|---|---|---|
config | DocsConfig | The config object from defineDocs() |
_preloadedContent | Record<string, string> | Pre-loaded markdown files from import.meta.glob. Required for serverless. |
Returns:
| Property | Type | Description |
|---|---|---|
load | (pathname: string) => Promise<PageData> | Takes a URL pathname string, returns page data |
GET | ({ request }) => Response | Search endpoint handler for GET /api/docs?query=... |
HEAD | ({ request }) => Promise<Response> | Bodyless public-read handler |
POST | ({ request }) => Promise<Response> | AI chat endpoint handler with SSE streaming |
MCP | DocsMcpHttpHandlers | MCP HTTP transport handlers |
import { createDocsServer } from "@farming-labs/astro/server";
import config from "./docs.config";
const contentFiles = import.meta.glob(
["/docs/**/*.{md,mdx}", "/AGENTS.md", "/AGENT.md", "/skill.md", "/.farming-labs/sitemap-manifest.json"],
{
query: "?raw",
import: "default",
eager: true,
},
) as Record<string, string>;
export const { load, GET, HEAD, POST, MCP } = createDocsServer({
...config,
_preloadedContent: contentFiles,
});Astro Components
| Component | Import | Description |
|---|---|---|
DocsLayout | @farming-labs/astro-theme/src/components/DocsLayout.astro | Main layout with sidebar, search, and theme toggle |
DocsContent | @farming-labs/astro-theme/src/components/DocsContent.astro | Page content with metadata |
DocsPage | @farming-labs/astro-theme/src/components/DocsPage.astro | Page structure (TOC, breadcrumb, nav) |
ThemeToggle | @farming-labs/astro-theme/src/components/ThemeToggle.astro | Light/dark toggle |
SearchDialog | @farming-labs/astro-theme/src/components/SearchDialog.astro | Search + AI dialog |
CSS Imports
Same pattern as SvelteKit:
| Import | Description |
|---|---|
@farming-labs/theme/default/css | Default theme |
@farming-labs/theme/pixel-border/css | Pixel border theme |
@farming-labs/theme/darksharp/css | Darksharp theme |
Theme Factories
| Factory | Import |
|---|---|
fumadocs() | @farming-labs/astro-theme |
pixelBorder() | @farming-labs/astro-theme/pixel-border |
darksharp() | @farming-labs/astro-theme/darksharp |
Nuxt Server API
defineDocsHandler(config, useStorage)
Creates a single Nitro event handler that serves docs loading, search, and AI chat for Nuxt.
import { defineDocsHandler } from "@farming-labs/nuxt/server";Parameters:
| Property | Type | Description |
|---|---|---|
config | DocsConfig | The config object from defineDocs() |
useStorage | Function | Nitro's useStorage utility for accessing server assets |
Returns:
A Nitro event handler that responds to GET (page loading + search) and POST (AI chat) requests.
import { defineDocsHandler } from "@farming-labs/nuxt/server";
import config from "../../docs.config";
export default defineDocsHandler(config, useStorage);Nuxt uses Nitro's
serverAssetsto load markdown files. Configurenitro.serverAssetsinnuxt.config.tsto point to your docs directory.
Nuxt Components
Imported from @farming-labs/nuxt-theme:
| Component | Props | Description |
|---|---|---|
DocsLayout | tree, config, triggerComponent? | Main layout with sidebar, nav, search, AI |
DocsContent | data, config | Page content with TOC, breadcrumb, footer nav |
CSS Imports
| Import | Description |
|---|---|
@farming-labs/theme/default/css | Default theme |
Theme Factories
| Factory | Import |
|---|---|
fumadocs() | @farming-labs/nuxt-theme/fumadocs |
Next.js API
RootProvider
Client-side provider that wraps your entire app. Enables search, theme switching (light/dark), and AI chat. Must be placed in your root app/layout.tsx.
import { RootProvider } from "@farming-labs/theme";
import "./global.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<RootProvider>{children}</RootProvider>
</body>
</html>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
search | SearchProviderProps | { options: { api: "/api/docs" } } | Search configuration. API defaults to the unified docs handler. |
theme | ThemeProviderProps | — | Theme provider options (passed to next-themes) |
children | ReactNode | required | Your app content |
suppressHydrationWarningon<html>prevents React warnings caused by the theme class being injected before hydration.
withDocs(nextConfig, docsConfig?)
Wraps your Next.js config with docs framework support. Handles MDX, routing, search index generation, and auto-generates mdx-components.tsx and app/docs/layout.tsx if missing.
import { withDocs } from "@farming-labs/next/config";
export default withDocs({});The optional second argument supplies the live docs config when build-time routing cannot be read
statically from an imported, spread, or shorthand docs.config value. Protected MCP users should
pass it so RFC 9728 rewrites and the incompatible-basePath check use the resolved configuration.
createDocsLayout(config)
Creates the docs layout component from your config. Auto-generated by withDocs() — you typically don't need to create this manually.
import { createDocsLayout } from "@farming-labs/theme";
import config from "@/docs.config";
const { DocsLayout } = createDocsLayout(config);
export default function Layout({ children }) {
return <DocsLayout>{children}</DocsLayout>;
}Complete reference for all configuration options
How is this guide?