@farming-labs/theme 0.2.58 → 0.2.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ //#region src/bounded-route-cache.ts
2
+ /**
3
+ * A small per-locale LRU cache for request-derived route variants.
4
+ *
5
+ * Locales come from the configured i18n locale list, while routes can be
6
+ * inferred from request URLs. Bounding each locale prevents arbitrary request
7
+ * paths from growing the cache indefinitely.
8
+ */
9
+ var BoundedRouteCache = class {
10
+ entries = /* @__PURE__ */ new Map();
11
+ constructor(maxRoutesPerLocale) {
12
+ this.maxRoutesPerLocale = maxRoutesPerLocale;
13
+ if (!Number.isInteger(maxRoutesPerLocale) || maxRoutesPerLocale < 1) throw new RangeError("maxRoutesPerLocale must be a positive integer");
14
+ }
15
+ get(locale, route) {
16
+ const routes = this.entries.get(locale);
17
+ if (!routes?.has(route)) return void 0;
18
+ const value = routes.get(route);
19
+ routes.delete(route);
20
+ routes.set(route, value);
21
+ return value;
22
+ }
23
+ set(locale, route, value) {
24
+ let routes = this.entries.get(locale);
25
+ if (!routes) {
26
+ routes = /* @__PURE__ */ new Map();
27
+ this.entries.set(locale, routes);
28
+ }
29
+ routes.delete(route);
30
+ routes.set(route, value);
31
+ while (routes.size > this.maxRoutesPerLocale) {
32
+ const oldestRoute = routes.keys().next().value;
33
+ if (oldestRoute === void 0) break;
34
+ routes.delete(oldestRoute);
35
+ }
36
+ }
37
+ };
38
+
39
+ //#endregion
40
+ export { BoundedRouteCache };
@@ -34,6 +34,8 @@ interface AIOptions {
34
34
  }
35
35
  interface DocsAPIOptions {
36
36
  rootDir?: string;
37
+ /** Internal docs API route used by query-form discovery endpoints. */
38
+ apiRoute?: string;
37
39
  /** Docs entry folder (default: read from docs.config) */
38
40
  entry?: string;
39
41
  /** Public docs route prefix. Defaults to the docs entry path. */
@@ -66,6 +68,8 @@ interface DocsAPIOptions {
66
68
  mcp?: boolean | DocsMcpConfig;
67
69
  /** llms.txt configuration. Enabled by default; set false to opt out. */
68
70
  llmsTxt?: boolean | LlmsTxtOptions;
71
+ /** Override RFC 9727 API catalog discovery without disabling Agent Skills discovery. */
72
+ apiCatalog?: boolean;
69
73
  /** Sitemap configuration used for sitemap.xml, sitemap.md, and docs/sitemap.md. */
70
74
  sitemap?: boolean | DocsSitemapConfig;
71
75
  /** Robots.txt generation policy used for the agent discovery spec. */
@@ -93,7 +97,7 @@ type LlmsTxtOptions = LlmsTxtConfig;
93
97
  /**
94
98
  * Create a unified docs API route handler.
95
99
  *
96
- * Returns `{ GET, POST }` for use in a Next.js route handler:
100
+ * Returns `{ GET, HEAD, POST }` for use in a Next.js route handler:
97
101
  * - **GET ?query=…** → full-text search
98
102
  * - **GET ?format=llms** → llms.txt (concise page listing)
99
103
  * - **GET ?format=llms-full** → llms-full.txt (full page content)
@@ -104,7 +108,7 @@ type LlmsTxtOptions = LlmsTxtConfig;
104
108
  * ```ts
105
109
  * // app/api/docs/route.ts (auto-generated by withDocs)
106
110
  * import { createDocsAPI } from "@farming-labs/next/api";
107
- * export const { GET, POST } = createDocsAPI();
111
+ * export const { GET, HEAD, POST } = createDocsAPI();
108
112
  * export const revalidate = false;
109
113
  * ```
110
114
  *
@@ -115,6 +119,7 @@ type LlmsTxtOptions = LlmsTxtConfig;
115
119
  * The `@farming-labs/theme/api` path is kept for compatibility and will be phased out.
116
120
  */
117
121
  declare function createDocsAPI(options?: DocsAPIOptions): {
122
+ HEAD(request: Request): Promise<Response>;
118
123
  /**
119
124
  * GET handler — search, markdown, llms.txt, llms-full.txt, or skill.md.
120
125
  */
package/dist/docs-api.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  import { withLangInUrl } from "./i18n.mjs";
2
2
  import { getNextAppDir } from "./get-app-dir.mjs";
3
+ import { BoundedRouteCache } from "./bounded-route-cache.mjs";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import matter from "gray-matter";
6
- import { DEFAULT_SITEMAP_MD_DOCS_ROUTE, acceptsDocsMarkdown, buildDocsAskAIContext, buildDocsConfigMap, buildDocsDiagnostics, createDocsAgentTraceContext, createDocsAgentTraceId, createDocsMarkdownResponse, createDocsRobotsResponse, createDocsSitemapResponse, detectDocsMarkdownAgentRequest, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, emitDocsTelemetryAgentSurfaceEvent, emitDocsTelemetryProjectEvent, formatDocsAskAIPackageHints, getDocsLlmsTxtMaxCharsIssue, hasDocsMarkdownSignatureAgent, inferDocsTelemetryAgentSurface, isDocsConfigRequest, isDocsDiagnosticsRequest, normalizeDocsRelated, normalizePageAgentFrontmatter, performDocsSearch, renderDocsLlmsTxt, renderDocsMarkdownDocument, resolveAskAISearchRequestConfig, resolveChangelogConfig, resolveDocsAgentContractMcpTools, resolveDocsAudienceMdxContent, resolveDocsI18n, resolveDocsLlmsTxtRequest, resolveDocsLlmsTxtSections, resolveDocsLocale, resolveDocsMetadataBaseUrl, resolveDocsSitemapConfig, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig, selectDocsLlmsTxtContent, stripGeneratedAgentProvenance } from "@farming-labs/docs";
7
+ import { DEFAULT_AGENT_SKILLS_INDEX_ROUTE, DEFAULT_AGENT_SKILLS_ROUTE_PREFIX, DEFAULT_API_CATALOG_ROUTE, DEFAULT_SITEMAP_MD_DOCS_ROUTE, acceptsDocsMarkdown, buildDocsAskAIContext, buildDocsConfigMap, buildDocsDiagnostics, createDocsAgentTraceContext, createDocsAgentTraceId, createDocsMarkdownResponse, createDocsRobotsResponse, createDocsSitemapResponse, createDocsStandardsDiscoveryResponse, detectDocsMarkdownAgentRequest, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, emitDocsTelemetryAgentSurfaceEvent, emitDocsTelemetryProjectEvent, formatDocsAskAIPackageHints, getDocsDiscoveryLinkHeader, getDocsLlmsTxtMaxCharsIssue, hasDocsMarkdownSignatureAgent, inferDocsTelemetryAgentSurface, isDocsConfigRequest, isDocsDiagnosticsRequest, normalizeDocsRelated, normalizePageAgentFrontmatter, performDocsSearch, renderDocsLlmsTxt, renderDocsMarkdownDocument, resolveAskAISearchRequestConfig, resolveChangelogConfig, resolveDocsAgentContractMcpTools, resolveDocsAudienceMdxContent, resolveDocsI18n, resolveDocsLlmsTxtRequest, resolveDocsLlmsTxtSections, resolveDocsLocale, resolveDocsMetadataBaseUrl, resolveDocsRequestApiRoute, resolveDocsSitemapConfig, resolveDocsStandardsDiscoveryRequest, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig, selectDocsLlmsTxtContent, stripGeneratedAgentProvenance } from "@farming-labs/docs";
7
8
  import { buildApiReferenceOpenApiDocumentAsync, createDocsMcpHttpHandler, createFilesystemDocsMcpSource, readDocsSitemapManifest, resolveApiReferenceConfig, resolveDocsMcpConfig } from "@farming-labs/docs/server";
8
9
 
9
10
  //#region src/docs-api.ts
@@ -24,7 +25,7 @@ import { buildApiReferenceOpenApiDocumentAsync, createDocsMcpHttpHandler, create
24
25
  * ```ts
25
26
  * // app/api/docs/route.ts (auto-generated by withDocs)
26
27
  * import { createDocsAPI } from "@farming-labs/next/api";
27
- * export const { GET, POST } = createDocsAPI();
28
+ * export const { GET, HEAD, POST } = createDocsAPI();
28
29
  * export const revalidate = false;
29
30
  * ```
30
31
  */
@@ -35,6 +36,7 @@ const FILE_EXTS = [
35
36
  "js"
36
37
  ];
37
38
  const DEFAULT_DOCS_API_ROUTE = "/api/docs";
39
+ const MAX_LLMS_CACHE_ROUTES_PER_LOCALE = 8;
38
40
  const DEFAULT_DOCS_CLOUD_API_BASE_URL = "https://api.farming-labs.dev";
39
41
  const DEFAULT_DOCS_CLOUD_API_KEY_ENV = "DOCS_CLOUD_API_KEY";
40
42
  const DEFAULT_AGENT_SPEC_ROUTE = "/api/docs/agent/spec";
@@ -202,12 +204,13 @@ function resolveApiReferenceOpenApiDiscovery(value) {
202
204
  return {
203
205
  enabled: true,
204
206
  url: `${DEFAULT_DOCS_API_ROUTE}?format=openapi`,
207
+ urlSource: "default",
205
208
  source: apiReference.specUrl ? "configured" : "generated",
206
209
  specUrl: apiReference.specUrl,
207
210
  apiReferencePath: `/${apiReference.path}`
208
211
  };
209
212
  }
210
- function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, sitemap, robots, openapi }) {
213
+ function buildAgentSpec({ origin, entry, apiRoute, apiCatalog, i18n, search, mcp, feedback, llms, sitemap, robots, openapi }) {
211
214
  const normalizedEntry = normalizePathSegment(entry) || "docs";
212
215
  const localesEnabled = i18n !== null;
213
216
  const searchEnabled = isSearchEnabled(search);
@@ -215,6 +218,7 @@ function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, site
215
218
  const robotsEnabled = isRobotsDiscoveryEnabled(robots);
216
219
  const llmsSections = resolveDocsLlmsTxtSections(llms);
217
220
  const agentContractMcpTools = resolveDocsAgentContractMcpTools(mcp);
221
+ const agentSpecRoute = apiRoute === DEFAULT_DOCS_API_ROUTE ? DEFAULT_AGENT_SPEC_ROUTE : `${apiRoute}?agent=spec`;
218
222
  return {
219
223
  version: "1",
220
224
  name: "@farming-labs/docs",
@@ -247,32 +251,46 @@ function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, site
247
251
  structuredData: true,
248
252
  apiReference: openapi.enabled,
249
253
  openapi: openapi.enabled,
254
+ apiCatalog,
255
+ agentSkillsDiscovery: true,
250
256
  agentFeedback: feedback.enabled,
251
257
  locales: localesEnabled
252
258
  },
253
259
  api: {
254
- docs: DEFAULT_DOCS_API_ROUTE,
255
- config: `${DEFAULT_DOCS_API_ROUTE}?format=config`,
256
- diagnostics: `${DEFAULT_DOCS_API_ROUTE}?format=diagnostics`,
257
- agentSpec: DEFAULT_AGENT_SPEC_ROUTE,
260
+ docs: apiRoute,
261
+ config: `${apiRoute}?format=config`,
262
+ diagnostics: `${apiRoute}?format=diagnostics`,
263
+ agentSpec: agentSpecRoute,
258
264
  agentSpecDefault: DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE,
259
265
  agentSpecFallback: DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE,
260
266
  agentSpecWellKnown: DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE,
261
267
  agentSpecWellKnownJson: DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE,
262
- agentSpecQuery: `${DEFAULT_DOCS_API_ROUTE}?agent=spec`,
263
- agents: `${DEFAULT_DOCS_API_ROUTE}?format=agents`,
264
- openapi: `${DEFAULT_DOCS_API_ROUTE}?format=openapi`
268
+ agentSpecQuery: `${apiRoute}?agent=spec`,
269
+ agents: `${apiRoute}?format=agents`,
270
+ openapi: `${apiRoute}?format=openapi`,
271
+ ...apiCatalog ? {
272
+ apiCatalog: DEFAULT_API_CATALOG_ROUTE,
273
+ apiCatalogQuery: `${apiRoute}?format=api-catalog`
274
+ } : {},
275
+ agentSkillsIndex: DEFAULT_AGENT_SKILLS_INDEX_ROUTE
276
+ },
277
+ apiCatalog: {
278
+ enabled: apiCatalog,
279
+ route: apiCatalog ? DEFAULT_API_CATALOG_ROUTE : null,
280
+ api: apiCatalog ? `${apiRoute}?format=api-catalog` : null,
281
+ mediaType: "application/linkset+json",
282
+ profile: "https://www.rfc-editor.org/info/rfc9727"
265
283
  },
266
284
  config: {
267
285
  format: "docs-config-map.v1",
268
- endpoint: `${DEFAULT_DOCS_API_ROUTE}?format=config`
286
+ endpoint: `${apiRoute}?format=config`
269
287
  },
270
288
  markdown: {
271
289
  enabled: true,
272
290
  acceptHeader: "text/markdown",
273
291
  pagePattern: `/${normalizedEntry}/{slug}.md`,
274
292
  rootPage: `/${normalizedEntry}.md`,
275
- apiPattern: `${DEFAULT_DOCS_API_ROUTE}?format=markdown&path={slug}`,
293
+ apiPattern: `${apiRoute}?format=markdown&path={slug}`,
276
294
  resolutionOrder: [
277
295
  "agent.md",
278
296
  "agent audience projection",
@@ -310,8 +328,8 @@ function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, site
310
328
  enabled: llms.enabled,
311
329
  defaultTxt: DEFAULT_LLMS_TXT_ROUTE,
312
330
  defaultFull: DEFAULT_LLMS_FULL_TXT_ROUTE,
313
- txt: `${DEFAULT_DOCS_API_ROUTE}?format=llms`,
314
- full: `${DEFAULT_DOCS_API_ROUTE}?format=llms-full`,
331
+ txt: `${apiRoute}?format=llms`,
332
+ full: `${apiRoute}?format=llms-full`,
315
333
  publicTxt: DEFAULT_LLMS_TXT_ROUTE,
316
334
  publicFull: DEFAULT_LLMS_FULL_TXT_ROUTE,
317
335
  wellKnownTxt: DEFAULT_LLMS_TXT_WELL_KNOWN_ROUTE,
@@ -329,14 +347,14 @@ function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, site
329
347
  xml: {
330
348
  enabled: sitemapConfig.xml.enabled,
331
349
  route: sitemapConfig.xml.route,
332
- api: `${DEFAULT_DOCS_API_ROUTE}?format=sitemap-xml`
350
+ api: `${apiRoute}?format=sitemap-xml`
333
351
  },
334
352
  markdown: {
335
353
  enabled: sitemapConfig.markdown.enabled,
336
354
  route: sitemapConfig.markdown.route,
337
355
  docsRoute: sitemapConfig.markdown.docsRoute,
338
356
  wellKnownRoute: sitemapConfig.markdown.wellKnownRoute,
339
- api: `${DEFAULT_DOCS_API_ROUTE}?format=sitemap-md`,
357
+ api: `${apiRoute}?format=sitemap-md`,
340
358
  defaultDocsRoute: DEFAULT_SITEMAP_MD_DOCS_ROUTE
341
359
  }
342
360
  },
@@ -363,7 +381,7 @@ function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, site
363
381
  },
364
382
  openapi: {
365
383
  enabled: openapi.enabled,
366
- url: openapi.url ?? null,
384
+ url: openapi.enabled ? `${apiRoute}?format=openapi` : null,
367
385
  source: openapi.source ?? null,
368
386
  specUrl: openapi.specUrl ?? null,
369
387
  apiReferencePath: openapi.apiReferencePath ?? null,
@@ -371,7 +389,7 @@ function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, site
371
389
  },
372
390
  search: {
373
391
  enabled: searchEnabled,
374
- endpoint: `${DEFAULT_DOCS_API_ROUTE}?query={query}`,
392
+ endpoint: `${apiRoute}?query={query}`,
375
393
  method: "GET",
376
394
  queryParam: "query",
377
395
  localeParam: "lang"
@@ -381,7 +399,7 @@ function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, site
381
399
  file: "AGENTS.md",
382
400
  route: DEFAULT_AGENTS_MD_ROUTE,
383
401
  wellKnown: DEFAULT_AGENTS_MD_WELL_KNOWN_ROUTE,
384
- api: `${DEFAULT_DOCS_API_ROUTE}?format=agents`,
402
+ api: `${apiRoute}?format=agents`,
385
403
  generatedFallback: true,
386
404
  aliases: [DEFAULT_AGENT_MD_ROUTE, DEFAULT_AGENT_MD_WELL_KNOWN_ROUTE]
387
405
  },
@@ -390,8 +408,16 @@ function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, site
390
408
  file: "skill.md",
391
409
  route: DEFAULT_SKILL_MD_ROUTE,
392
410
  wellKnown: DEFAULT_SKILL_MD_WELL_KNOWN_ROUTE,
393
- api: `${DEFAULT_DOCS_API_ROUTE}?format=skill`,
411
+ api: `${apiRoute}?format=skill`,
394
412
  generatedFallback: true,
413
+ discovery: {
414
+ schema: "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
415
+ index: DEFAULT_AGENT_SKILLS_INDEX_ROUTE,
416
+ artifact: `${DEFAULT_AGENT_SKILLS_ROUTE_PREFIX}/{name}/SKILL.md`,
417
+ apiIndex: `${apiRoute}?format=agent-skills`,
418
+ apiArtifact: `${apiRoute}?format=agent-skill&name={name}`,
419
+ digest: "sha256"
420
+ },
395
421
  registry: "skills.sh",
396
422
  install: "npx skills add farming-labs/docs",
397
423
  recommended: [{
@@ -415,8 +441,8 @@ function buildAgentSpec({ origin, entry, i18n, search, mcp, feedback, llms, site
415
441
  enabled: feedback.enabled,
416
442
  schema: feedback.schemaRoute,
417
443
  submit: feedback.route,
418
- schemaQuery: `${DEFAULT_DOCS_API_ROUTE}?feedback=agent&schema=1`,
419
- submitQuery: `${DEFAULT_DOCS_API_ROUTE}?feedback=agent`
444
+ schemaQuery: `${apiRoute}?feedback=agent&schema=1`,
445
+ submitQuery: `${apiRoute}?feedback=agent`
420
446
  },
421
447
  instructions: {
422
448
  preferMarkdownRoutes: true,
@@ -618,9 +644,14 @@ function readCloudConfig(root) {
618
644
  const configObject = extractRootConfigObject(content, sanitized);
619
645
  const cloudBlock = extractObjectLiteral(configObject?.content ?? content, configObject?.sanitized ?? sanitized, "cloud");
620
646
  if (!cloudBlock) continue;
621
- const apiKeyBlock = extractObjectLiteral(cloudBlock, stripCommentsAndStrings(cloudBlock), "apiKey");
647
+ const cloudBlockSanitized = stripCommentsAndStrings(cloudBlock);
648
+ const apiKeyBlock = extractObjectLiteral(cloudBlock, cloudBlockSanitized, "apiKey");
622
649
  const apiKeyEnv = apiKeyBlock ? readStringFromBlock(apiKeyBlock, "env") : void 0;
623
- return apiKeyEnv ? { apiKey: { env: apiKeyEnv } } : {};
650
+ const apiRoute = readTopLevelString(cloudBlock, cloudBlockSanitized, "apiRoute");
651
+ return {
652
+ ...apiKeyEnv ? { apiKey: { env: apiKeyEnv } } : {},
653
+ ...apiRoute ? { apiRoute } : {}
654
+ };
624
655
  } catch {}
625
656
  }
626
657
  }
@@ -702,6 +733,14 @@ function readTopLevelBoolean(content, key) {
702
733
  if (!match) return void 0;
703
734
  return match[1] === "true";
704
735
  }
736
+ function readTopLevelString(content, sanitized, key) {
737
+ const keyIndex = findTopLevelPropertyIndex(sanitized, key);
738
+ if (keyIndex === -1) return void 0;
739
+ const colonIndex = sanitized.indexOf(":", keyIndex + key.length);
740
+ if (colonIndex === -1) return void 0;
741
+ const match = content.slice(colonIndex + 1).match(/^\s*(?:"([^"]*)"|'([^']*)')/s);
742
+ return match?.[1] ?? match?.[2];
743
+ }
705
744
  function findTopLevelPropertyIndex(content, key) {
706
745
  let depth = 0;
707
746
  for (let index = 0; index < content.length; index += 1) {
@@ -726,11 +765,11 @@ function findTopLevelPropertyIndex(content, key) {
726
765
  return -1;
727
766
  }
728
767
  function extractRootConfigObject(content, sanitized) {
729
- const defineDocsIndex = sanitized.indexOf("defineDocs");
768
+ const defineDocsIndex = sanitized.match(/\bdefineDocs\s*\(/)?.index ?? -1;
730
769
  const exportDefaultIndex = sanitized.indexOf("export default");
731
770
  let braceStart = -1;
732
771
  if (defineDocsIndex !== -1) {
733
- const parenIndex = sanitized.indexOf("(", defineDocsIndex);
772
+ const parenIndex = sanitized.indexOf("(", defineDocsIndex + 10);
734
773
  if (parenIndex !== -1) braceStart = sanitized.indexOf("{", parenIndex);
735
774
  }
736
775
  if (braceStart === -1 && exportDefaultIndex !== -1) braceStart = sanitized.indexOf("{", exportDefaultIndex);
@@ -1005,6 +1044,10 @@ function normalizeUrlPath(value) {
1005
1044
  if (normalized === "/") return normalized;
1006
1045
  return normalized.replace(/\/+$/, "");
1007
1046
  }
1047
+ function normalizeDocsApiRoute(value) {
1048
+ const route = value?.trim().split(/[?#]/, 1)[0] || DEFAULT_DOCS_API_ROUTE;
1049
+ return normalizeUrlPath(route.startsWith("/") ? route : `/${route}`);
1050
+ }
1008
1051
  function normalizeRequestedMarkdownPath(entry, requestedPath) {
1009
1052
  const trimmed = requestedPath.trim().replace(/\.md$/i, "");
1010
1053
  if (!trimmed) return `/${entry}`;
@@ -1186,13 +1229,15 @@ function renderMarkdownDocument(page, options = {}) {
1186
1229
  sitemap: options.sitemap
1187
1230
  });
1188
1231
  }
1189
- function renderSkillDocument({ origin, entry, search, mcp, feedback, llms, sitemap, robots, openapi }) {
1232
+ function renderSkillDocument({ origin, entry, apiRoute, apiCatalog, search, mcp, feedback, llms, sitemap, robots, openapi }) {
1190
1233
  const normalizedEntry = normalizePathSegment(entry) || "docs";
1191
1234
  const siteTitle = compactSkillText(llms.siteTitle ?? "Documentation");
1192
1235
  const siteDescription = llms.siteDescription ? compactSkillText(llms.siteDescription) : void 0;
1193
1236
  const searchEnabled = isSearchEnabled(search);
1194
1237
  const sitemapConfig = resolveDocsSitemapConfig(sitemap, { baseUrl: llms.baseUrl });
1195
1238
  const robotsEnabled = isRobotsDiscoveryEnabled(robots);
1239
+ const agentSpecRoute = apiRoute === DEFAULT_DOCS_API_ROUTE ? DEFAULT_AGENT_SPEC_ROUTE : `${apiRoute}?agent=spec`;
1240
+ const openapiUrl = openapi.urlSource === "default" && openapi.url === `${DEFAULT_DOCS_API_ROUTE}?format=openapi` ? `${apiRoute}?format=openapi` : openapi.url;
1196
1241
  const llmsSections = resolveDocsLlmsTxtSections(llms);
1197
1242
  const lines = [
1198
1243
  "---",
@@ -1205,9 +1250,11 @@ function renderSkillDocument({ origin, entry, search, mcp, feedback, llms, sitem
1205
1250
  `Base URL: ${origin}`
1206
1251
  ];
1207
1252
  if (siteDescription) lines.push(`Description: ${siteDescription}`);
1208
- lines.push("", "## When To Use", "Use this skill when you need to read or implement against this documentation site.", "", "## Start Here", `- Fetch ${DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE}; fall back to ${DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE} or ${DEFAULT_AGENT_SPEC_ROUTE}.`, `- Fetch /${normalizedEntry}.md for the root docs page.`, `- Fetch /${normalizedEntry}/{slug}.md for page-specific context.`, "- You can also request text/markdown from normal page URLs.");
1209
- if (searchEnabled) lines.push(`- Search with ${DEFAULT_DOCS_API_ROUTE}?query={query} when you do not know the page.`);
1210
- if (openapi.enabled && openapi.url) lines.push(`- Fetch ${openapi.url} for the machine-readable OpenAPI schema before scraping API reference pages.`);
1253
+ lines.push("", "## When To Use", "Use this skill when you need to read or implement against this documentation site.", "", "## Start Here", `- Fetch ${DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE}; fall back to ${DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE} or ${agentSpecRoute}.`);
1254
+ if (apiCatalog) lines.push(`- Fetch ${DEFAULT_API_CATALOG_ROUTE} for the standards-based API catalog.`);
1255
+ lines.push(`- Fetch ${DEFAULT_AGENT_SKILLS_INDEX_ROUTE} to discover the published site skill and verify its SHA-256 digest.`, `- Fetch /${normalizedEntry}.md for the root docs page.`, `- Fetch /${normalizedEntry}/{slug}.md for page-specific context.`, "- You can also request text/markdown from normal page URLs.");
1256
+ if (searchEnabled) lines.push(`- Search with ${apiRoute}?query={query} when you do not know the page.`);
1257
+ if (openapi.enabled && openapiUrl) lines.push(`- Fetch ${openapiUrl} for the machine-readable OpenAPI schema before scraping API reference pages.`);
1211
1258
  if (llms.enabled) {
1212
1259
  lines.push(`- Use ${DEFAULT_LLMS_TXT_ROUTE} for a compact docs index.`, `- Use ${DEFAULT_LLMS_FULL_TXT_ROUTE} for full markdown context.`);
1213
1260
  for (const section of llmsSections) lines.push(`- Use ${section.route} for the ${section.title} llms.txt section.`);
@@ -1219,7 +1266,9 @@ function renderSkillDocument({ origin, entry, search, mcp, feedback, llms, sitem
1219
1266
  if (robotsEnabled) lines.push(`- Check ${DEFAULT_ROBOTS_TXT_ROUTE} for crawler and AI-agent access policy.`);
1220
1267
  if (mcp.enabled) lines.push(`- Use ${DEFAULT_MCP_WELL_KNOWN_ROUTE} or ${DEFAULT_MCP_PUBLIC_ROUTE} for MCP tools when your environment supports MCP.`);
1221
1268
  if (feedback.enabled) lines.push(`- Read ${feedback.schemaRoute} before posting agent feedback to ${feedback.route}.`);
1222
- lines.push("", "## Routes", `- Agent instructions: ${DEFAULT_AGENTS_MD_ROUTE}`, `- Agent instructions well-known alias: ${DEFAULT_AGENTS_MD_WELL_KNOWN_ROUTE}`, `- Agent instructions API format: ${DEFAULT_DOCS_API_ROUTE}?format=agents`, `- Skill document: ${DEFAULT_SKILL_MD_ROUTE}`, `- Skill well-known alias: ${DEFAULT_SKILL_MD_WELL_KNOWN_ROUTE}`, `- Skill API format: ${DEFAULT_DOCS_API_ROUTE}?format=skill`, `- Agent discovery: ${DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE}`, `- Agent discovery fallback: ${DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE}`, `- Markdown root: /${normalizedEntry}.md`, `- Markdown pages: /${normalizedEntry}/{slug}.md`);
1269
+ lines.push("", "## Routes", `- Agent instructions: ${DEFAULT_AGENTS_MD_ROUTE}`, `- Agent instructions well-known alias: ${DEFAULT_AGENTS_MD_WELL_KNOWN_ROUTE}`, `- Agent instructions API format: ${apiRoute}?format=agents`, `- Skill document: ${DEFAULT_SKILL_MD_ROUTE}`, `- Skill well-known alias: ${DEFAULT_SKILL_MD_WELL_KNOWN_ROUTE}`, `- Skill API format: ${apiRoute}?format=skill`, `- Agent discovery: ${DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE}`, `- Agent discovery fallback: ${DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE}`);
1270
+ if (apiCatalog) lines.push(`- API catalog: ${DEFAULT_API_CATALOG_ROUTE}`);
1271
+ lines.push(`- Agent Skills index: ${DEFAULT_AGENT_SKILLS_INDEX_ROUTE}`, `- Agent Skills artifacts: ${DEFAULT_AGENT_SKILLS_ROUTE_PREFIX}/{name}/SKILL.md`, `- Markdown root: /${normalizedEntry}.md`, `- Markdown pages: /${normalizedEntry}/{slug}.md`);
1223
1272
  if (robotsEnabled) lines.push(`- Robots policy: ${DEFAULT_ROBOTS_TXT_ROUTE}`);
1224
1273
  if (llms.enabled) {
1225
1274
  lines.push(`- llms.txt: ${DEFAULT_LLMS_TXT_ROUTE}`, `- llms-full.txt: ${DEFAULT_LLMS_FULL_TXT_ROUTE}`, `- llms well-known aliases: ${DEFAULT_LLMS_TXT_WELL_KNOWN_ROUTE}, ${DEFAULT_LLMS_FULL_TXT_WELL_KNOWN_ROUTE}`);
@@ -1228,8 +1277,8 @@ function renderSkillDocument({ origin, entry, search, mcp, feedback, llms, sitem
1228
1277
  lines.push(`- ${section.title} llms-full.txt: ${section.fullRoute}`);
1229
1278
  }
1230
1279
  }
1231
- if (openapi.enabled && openapi.url) {
1232
- lines.push(`- OpenAPI schema: ${openapi.url}`);
1280
+ if (openapi.enabled && openapiUrl) {
1281
+ lines.push(`- OpenAPI schema: ${openapiUrl}`);
1233
1282
  if (openapi.apiReferencePath) lines.push(`- API reference: ${openapi.apiReferencePath}`);
1234
1283
  }
1235
1284
  if (sitemapConfig.enabled) {
@@ -1240,13 +1289,15 @@ function renderSkillDocument({ origin, entry, search, mcp, feedback, llms, sitem
1240
1289
  lines.push("", "## Reusable Framework Skills", "For framework setup, CLI, page actions, Ask AI, or configuration work, install the reusable Farming Labs skills:", "", "```sh", "npx skills add farming-labs/docs", "```");
1241
1290
  return lines.join("\n");
1242
1291
  }
1243
- function renderAgentsDocument({ origin, entry, search, mcp, feedback, llms, sitemap, robots, openapi }) {
1292
+ function renderAgentsDocument({ origin, entry, apiRoute, apiCatalog, search, mcp, feedback, llms, sitemap, robots, openapi }) {
1244
1293
  const normalizedEntry = normalizePathSegment(entry) || "docs";
1245
1294
  const siteTitle = compactSkillText(llms.siteTitle ?? "Documentation");
1246
1295
  const siteDescription = llms.siteDescription ? compactSkillText(llms.siteDescription) : void 0;
1247
1296
  const searchEnabled = isSearchEnabled(search);
1248
1297
  const sitemapConfig = resolveDocsSitemapConfig(sitemap, { baseUrl: llms.baseUrl });
1249
1298
  const robotsEnabled = isRobotsDiscoveryEnabled(robots);
1299
+ const agentSpecRoute = apiRoute === DEFAULT_DOCS_API_ROUTE ? DEFAULT_AGENT_SPEC_ROUTE : `${apiRoute}?agent=spec`;
1300
+ const openapiUrl = openapi.urlSource === "default" && openapi.url === `${DEFAULT_DOCS_API_ROUTE}?format=openapi` ? `${apiRoute}?format=openapi` : openapi.url;
1250
1301
  const llmsSections = resolveDocsLlmsTxtSections(llms);
1251
1302
  const lines = [
1252
1303
  "# Agent Instructions",
@@ -1255,7 +1306,9 @@ function renderAgentsDocument({ origin, entry, search, mcp, feedback, llms, site
1255
1306
  `Base URL: ${origin}`
1256
1307
  ];
1257
1308
  if (siteDescription) lines.push(`Description: ${siteDescription}`);
1258
- lines.push("", "## Start Here", `- Read ${DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE} first; fall back to ${DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE} or ${DEFAULT_AGENT_SPEC_ROUTE}.`, `- Read /${normalizedEntry}.md for the root docs page.`, `- Read /${normalizedEntry}/{slug}.md for page-specific context.`);
1309
+ lines.push("", "## Start Here", `- Read ${DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE} first; fall back to ${DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE} or ${agentSpecRoute}.`);
1310
+ if (apiCatalog) lines.push(`- Read ${DEFAULT_API_CATALOG_ROUTE} for the standards-based API catalog.`);
1311
+ lines.push(`- Read ${DEFAULT_AGENT_SKILLS_INDEX_ROUTE} to discover the published site skill and verify its SHA-256 digest.`, `- Read /${normalizedEntry}.md for the root docs page.`, `- Read /${normalizedEntry}/{slug}.md for page-specific context.`);
1259
1312
  if (llms.enabled) {
1260
1313
  lines.push(`- Use ${DEFAULT_LLMS_TXT_ROUTE} as the compact docs map.`, `- Use ${DEFAULT_LLMS_FULL_TXT_ROUTE} when you need the full markdown bundle.`);
1261
1314
  for (const section of llmsSections) lines.push(`- Use ${section.route} for the ${section.title} section map.`);
@@ -1265,11 +1318,13 @@ function renderAgentsDocument({ origin, entry, search, mcp, feedback, llms, site
1265
1318
  if (sitemapConfig.xml.enabled) lines.push(`- Use ${sitemapConfig.xml.route} for canonical URLs and freshness metadata.`);
1266
1319
  }
1267
1320
  if (robotsEnabled) lines.push(`- Check ${DEFAULT_ROBOTS_TXT_ROUTE} before crawling broadly.`);
1268
- if (searchEnabled) lines.push(`- Search with ${DEFAULT_DOCS_API_ROUTE}?query={query} when the route is unknown.`);
1269
- if (openapi.enabled && openapi.url) lines.push(`- Fetch ${openapi.url} before scraping API reference pages; prefer schemas over prose.`);
1321
+ if (searchEnabled) lines.push(`- Search with ${apiRoute}?query={query} when the route is unknown.`);
1322
+ if (openapi.enabled && openapiUrl) lines.push(`- Fetch ${openapiUrl} before scraping API reference pages; prefer schemas over prose.`);
1270
1323
  if (mcp.enabled) lines.push(`- Use MCP at ${DEFAULT_MCP_PUBLIC_ROUTE} or ${DEFAULT_MCP_WELL_KNOWN_ROUTE} when your environment supports MCP tools.`);
1271
1324
  if (feedback.enabled) lines.push(`- Read ${feedback.schemaRoute} before posting feedback to ${feedback.route}.`);
1272
- lines.push("", "## Working Rules", "- Prefer markdown routes, llms.txt, sitemap.md, OpenAPI schemas, and MCP tools over scraping rendered HTML.", "- Treat generated context files as discovery aids, then fetch the smallest page or section that answers the task.", "- Preserve canonical docs URLs when citing pages back to humans.", "- If a route returns a markdown 404, use the sitemap and discovery spec before guessing a slug.", "", "## Public Routes", `- Agent instructions: ${DEFAULT_AGENTS_MD_ROUTE}`, `- Agent instructions well-known alias: ${DEFAULT_AGENTS_MD_WELL_KNOWN_ROUTE}`, `- Agent instructions API format: ${DEFAULT_DOCS_API_ROUTE}?format=agents`, `- Agent instructions aliases: ${DEFAULT_AGENT_MD_ROUTE}, ${DEFAULT_AGENT_MD_WELL_KNOWN_ROUTE}`, `- Site skill: ${DEFAULT_SKILL_MD_ROUTE}`, `- Site skill well-known alias: ${DEFAULT_SKILL_MD_WELL_KNOWN_ROUTE}`, `- Site skill API format: ${DEFAULT_DOCS_API_ROUTE}?format=skill`, `- Markdown root: /${normalizedEntry}.md`, `- Markdown pages: /${normalizedEntry}/{slug}.md`, `- Agent discovery: ${DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE}`, `- Agent discovery fallback: ${DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE}`);
1325
+ lines.push("", "## Working Rules", "- Prefer markdown routes, llms.txt, sitemap.md, OpenAPI schemas, and MCP tools over scraping rendered HTML.", "- Treat generated context files as discovery aids, then fetch the smallest page or section that answers the task.", "- Preserve canonical docs URLs when citing pages back to humans.", "- If a route returns a markdown 404, use the sitemap and discovery spec before guessing a slug.", "", "## Public Routes", `- Agent instructions: ${DEFAULT_AGENTS_MD_ROUTE}`, `- Agent instructions well-known alias: ${DEFAULT_AGENTS_MD_WELL_KNOWN_ROUTE}`, `- Agent instructions API format: ${apiRoute}?format=agents`, `- Agent instructions aliases: ${DEFAULT_AGENT_MD_ROUTE}, ${DEFAULT_AGENT_MD_WELL_KNOWN_ROUTE}`, `- Site skill: ${DEFAULT_SKILL_MD_ROUTE}`, `- Site skill well-known alias: ${DEFAULT_SKILL_MD_WELL_KNOWN_ROUTE}`, `- Site skill API format: ${apiRoute}?format=skill`, `- Markdown root: /${normalizedEntry}.md`, `- Markdown pages: /${normalizedEntry}/{slug}.md`, `- Agent discovery: ${DEFAULT_AGENT_SPEC_WELL_KNOWN_JSON_ROUTE}`, `- Agent discovery fallback: ${DEFAULT_AGENT_SPEC_WELL_KNOWN_ROUTE}`);
1326
+ if (apiCatalog) lines.push(`- API catalog: ${DEFAULT_API_CATALOG_ROUTE}`);
1327
+ lines.push(`- Agent Skills index: ${DEFAULT_AGENT_SKILLS_INDEX_ROUTE}`, `- Agent Skills artifacts: ${DEFAULT_AGENT_SKILLS_ROUTE_PREFIX}/{name}/SKILL.md`);
1273
1328
  if (llms.enabled) {
1274
1329
  lines.push(`- llms.txt: ${DEFAULT_LLMS_TXT_ROUTE}`, `- llms-full.txt: ${DEFAULT_LLMS_FULL_TXT_ROUTE}`, `- llms well-known aliases: ${DEFAULT_LLMS_TXT_WELL_KNOWN_ROUTE}, ${DEFAULT_LLMS_FULL_TXT_WELL_KNOWN_ROUTE}`);
1275
1330
  for (const section of llmsSections) {
@@ -1282,8 +1337,8 @@ function renderAgentsDocument({ origin, entry, search, mcp, feedback, llms, site
1282
1337
  if (sitemapConfig.xml.enabled) lines.push(`- Sitemap XML: ${sitemapConfig.xml.route}`);
1283
1338
  if (sitemapConfig.markdown.enabled) lines.push(`- Sitemap Markdown: ${sitemapConfig.markdown.route}`, ...sitemapConfig.markdown.docsRoute ? [`- Sitemap docs alias: ${sitemapConfig.markdown.docsRoute}`] : [], `- Sitemap well-known alias: ${sitemapConfig.markdown.wellKnownRoute}`);
1284
1339
  }
1285
- if (openapi.enabled && openapi.url) {
1286
- lines.push(`- OpenAPI schema: ${openapi.url}`);
1340
+ if (openapi.enabled && openapiUrl) {
1341
+ lines.push(`- OpenAPI schema: ${openapiUrl}`);
1287
1342
  if (openapi.apiReferencePath) lines.push(`- API reference: ${openapi.apiReferencePath}`);
1288
1343
  }
1289
1344
  if (mcp.enabled) lines.push(`- MCP: ${DEFAULT_MCP_PUBLIC_ROUTE}, ${DEFAULT_MCP_WELL_KNOWN_ROUTE}`);
@@ -2223,26 +2278,35 @@ function readLlmsTxtConfig(root) {
2223
2278
  const configPath = path.join(root, `docs.config.${ext}`);
2224
2279
  if (fs.existsSync(configPath)) try {
2225
2280
  const content = fs.readFileSync(configPath, "utf-8");
2226
- const navTitleMatch = content.match(/nav\s*:\s*\{[^}]*title\s*:\s*["']([^"']+)["']/s);
2227
- if (!content.includes("llmsTxt")) return {
2281
+ const sanitized = stripCommentsAndStrings(content);
2282
+ const configObject = extractRootConfigObject(content, sanitized);
2283
+ const scopedContent = configObject?.content ?? content;
2284
+ const scopedSanitized = configObject?.sanitized ?? sanitized;
2285
+ const navBlock = extractObjectLiteral(scopedContent, scopedSanitized, "nav");
2286
+ const navTitle = navBlock ? readTopLevelString(navBlock, stripCommentsAndStrings(navBlock), "title") : void 0;
2287
+ const llmsTxtBoolean = readTopLevelBoolean(scopedSanitized, "llmsTxt");
2288
+ if (llmsTxtBoolean === true) return {
2228
2289
  enabled: true,
2229
- siteTitle: navTitleMatch?.[1]
2290
+ siteTitle: navTitle
2230
2291
  };
2231
- if (/llmsTxt\s*:\s*true/.test(content)) return {
2292
+ if (llmsTxtBoolean === false) return { enabled: false };
2293
+ const llmsTxtBlock = extractObjectLiteral(scopedContent, scopedSanitized, "llmsTxt");
2294
+ if (!llmsTxtBlock) return {
2232
2295
  enabled: true,
2233
- siteTitle: navTitleMatch?.[1]
2296
+ siteTitle: navTitle
2234
2297
  };
2235
- if (/llmsTxt\s*:\s*false/.test(content)) return { enabled: false };
2236
- const enabledMatch = content.match(/llmsTxt\s*:\s*\{[^}]*enabled\s*:\s*(true|false)/s);
2237
- if (enabledMatch && enabledMatch[1] === "false") return { enabled: false };
2238
- const baseUrlMatch = content.match(/llmsTxt\s*:\s*\{[^}]*baseUrl\s*:\s*["']([^"']+)["']/s);
2239
- const siteTitleMatch = content.match(/llmsTxt\s*:\s*\{[^}]*siteTitle\s*:\s*["']([^"']+)["']/s);
2240
- const siteDescMatch = content.match(/llmsTxt\s*:\s*\{[^}]*siteDescription\s*:\s*["']([^"']+)["']/s);
2298
+ const llmsTxtSanitized = stripCommentsAndStrings(llmsTxtBlock);
2299
+ if (readTopLevelBoolean(llmsTxtSanitized, "enabled") === false) return { enabled: false };
2300
+ const apiCatalog = readTopLevelBoolean(llmsTxtSanitized, "apiCatalog");
2301
+ const baseUrl = readTopLevelString(llmsTxtBlock, llmsTxtSanitized, "baseUrl");
2302
+ const siteTitle = readTopLevelString(llmsTxtBlock, llmsTxtSanitized, "siteTitle");
2303
+ const siteDescription = readTopLevelString(llmsTxtBlock, llmsTxtSanitized, "siteDescription");
2241
2304
  return {
2242
2305
  enabled: true,
2243
- baseUrl: baseUrlMatch?.[1],
2244
- siteTitle: siteTitleMatch?.[1] ?? navTitleMatch?.[1],
2245
- siteDescription: siteDescMatch?.[1]
2306
+ apiCatalog,
2307
+ baseUrl,
2308
+ siteTitle: siteTitle ?? navTitle,
2309
+ siteDescription
2246
2310
  };
2247
2311
  } catch {}
2248
2312
  }
@@ -2347,7 +2411,7 @@ function generateLlmsTxt(indexes, options) {
2347
2411
  /**
2348
2412
  * Create a unified docs API route handler.
2349
2413
  *
2350
- * Returns `{ GET, POST }` for use in a Next.js route handler:
2414
+ * Returns `{ GET, HEAD, POST }` for use in a Next.js route handler:
2351
2415
  * - **GET ?query=…** → full-text search
2352
2416
  * - **GET ?format=llms** → llms.txt (concise page listing)
2353
2417
  * - **GET ?format=llms-full** → llms-full.txt (full page content)
@@ -2358,7 +2422,7 @@ function generateLlmsTxt(indexes, options) {
2358
2422
  * ```ts
2359
2423
  * // app/api/docs/route.ts (auto-generated by withDocs)
2360
2424
  * import { createDocsAPI } from "@farming-labs/next/api";
2361
- * export const { GET, POST } = createDocsAPI();
2425
+ * export const { GET, HEAD, POST } = createDocsAPI();
2362
2426
  * export const revalidate = false;
2363
2427
  * ```
2364
2428
  *
@@ -2382,8 +2446,15 @@ function createDocsAPI(options) {
2382
2446
  const i18n = resolveDocsI18n(i18nConfig);
2383
2447
  const aiConfig = options?.ai ?? readAIConfig(root);
2384
2448
  const cloudConfig = options?.cloud ?? readCloudConfig(root);
2449
+ const configuredApiRouteInput = options?.apiRoute ?? cloudConfig?.apiRoute;
2450
+ const configuredApiRoute = normalizeDocsApiRoute(configuredApiRouteInput);
2451
+ const effectiveCloudConfig = options?.apiRoute !== void 0 || cloudConfig?.apiRoute !== void 0 ? {
2452
+ ...cloudConfig,
2453
+ apiRoute: configuredApiRoute
2454
+ } : cloudConfig;
2385
2455
  const searchConfig = options?.search;
2386
2456
  const llmsConfig = resolveLlmsTxtConfig(options?.llmsTxt, readLlmsTxtConfig(root));
2457
+ const apiCatalogEnabled = options?.apiCatalog ?? llmsConfig.apiCatalog ?? true;
2387
2458
  const sitemapConfig = options?.sitemap ?? readSitemapConfig(root);
2388
2459
  const robotsConfig = options?.robots ?? readRobotsConfig(root);
2389
2460
  const markdownMetadataBaseUrl = resolveDocsMetadataBaseUrl({
@@ -2403,6 +2474,7 @@ function createDocsAPI(options) {
2403
2474
  const openapiDiscovery = resolveApiReferenceOpenApiDiscovery(apiReferenceConfig);
2404
2475
  const rawMcpConfig = options?.mcp ?? readMcpConfig(root);
2405
2476
  const mcpConfig = resolveDocsMcpConfig(rawMcpConfig, { defaultName: llmsConfig.siteTitle ?? "Documentation" });
2477
+ const discoveryLinkHeader = getDocsDiscoveryLinkHeader({ includeApiCatalog: apiCatalogEnabled });
2406
2478
  const telemetryConfig = {
2407
2479
  entry,
2408
2480
  docsPath,
@@ -2410,7 +2482,7 @@ function createDocsAPI(options) {
2410
2482
  telemetry: options?.telemetry,
2411
2483
  search: searchConfig,
2412
2484
  ai: aiConfig,
2413
- cloud: cloudConfig,
2485
+ cloud: effectiveCloudConfig,
2414
2486
  i18n: i18nConfig ?? void 0,
2415
2487
  feedback: options?.feedback,
2416
2488
  mcp: rawMcpConfig,
@@ -2420,15 +2492,15 @@ function createDocsAPI(options) {
2420
2492
  apiReference: apiReferenceConfig,
2421
2493
  metadata: options?.metadata
2422
2494
  };
2423
- const { rootDir: _rootDir, language: _language, ...docsConfigMapOptions } = options ?? {};
2495
+ const { rootDir: _rootDir, language: _language, apiRoute: _apiRoute, ...docsConfigMapOptions } = options ?? {};
2424
2496
  const docsConfigMapInput = { ...docsConfigMapOptions };
2497
+ if (effectiveCloudConfig !== void 0) docsConfigMapInput.cloud = effectiveCloudConfig;
2425
2498
  const setConfigMapFallback = (key, value) => {
2426
2499
  if (Object.prototype.hasOwnProperty.call(docsConfigMapInput, key) || value === void 0) return;
2427
2500
  docsConfigMapInput[key] = value;
2428
2501
  };
2429
2502
  setConfigMapFallback("entry", entry);
2430
2503
  setConfigMapFallback("ai", Object.keys(aiConfig).length > 0 ? aiConfig : void 0);
2431
- setConfigMapFallback("cloud", cloudConfig);
2432
2504
  setConfigMapFallback("search", searchConfig);
2433
2505
  setConfigMapFallback("i18n", i18nConfig ?? void 0);
2434
2506
  setConfigMapFallback("mcp", rawMcpConfig);
@@ -2437,8 +2509,12 @@ function createDocsAPI(options) {
2437
2509
  ...docsConfigMapInput,
2438
2510
  docsPath: docsConfigMapInput.docsPath ?? docsPath,
2439
2511
  contentDir: docsConfigMapInput.contentDir ?? contentDir,
2512
+ cloud: effectiveCloudConfig,
2440
2513
  feedback: docsConfigMapInput.feedback ?? options?.feedback,
2441
- llmsTxt: docsConfigMapInput.llmsTxt ?? llmsConfig,
2514
+ llmsTxt: {
2515
+ ...llmsConfig,
2516
+ apiCatalog: apiCatalogEnabled
2517
+ },
2442
2518
  sitemap: docsConfigMapInput.sitemap ?? sitemapConfig,
2443
2519
  robots: docsConfigMapInput.robots ?? robotsConfig,
2444
2520
  staticExport: docsConfigMapInput.staticExport ?? options?.staticExport
@@ -2521,7 +2597,7 @@ function createDocsAPI(options) {
2521
2597
  };
2522
2598
  }
2523
2599
  const indexesByLocale = /* @__PURE__ */ new Map();
2524
- const llmsCacheByLocale = /* @__PURE__ */ new Map();
2600
+ const llmsCache = new BoundedRouteCache(MAX_LLMS_CACHE_ROUTES_PER_LOCALE);
2525
2601
  const markdownSourcesByLocale = /* @__PURE__ */ new Map();
2526
2602
  function getIndexes(ctx) {
2527
2603
  const key = ctx.locale ?? "__default__";
@@ -2594,26 +2670,73 @@ function createDocsAPI(options) {
2594
2670
  };
2595
2671
  return null;
2596
2672
  }
2597
- function getLlmsContent(ctx) {
2598
- const key = ctx.locale ?? "__default__";
2599
- const cached = llmsCacheByLocale.get(key);
2673
+ function getLlmsContent(ctx, apiRoute) {
2674
+ const cached = llmsCache.get(ctx.locale, apiRoute);
2600
2675
  if (cached) return cached;
2601
2676
  const next = generateLlmsTxt(getIndexes(ctx), {
2602
2677
  siteTitle: llmsConfig.siteTitle ?? "Documentation",
2603
2678
  siteDescription: llmsConfig.siteDescription,
2604
2679
  baseUrl: llmsConfig.baseUrl ?? "",
2680
+ apiRoute,
2605
2681
  maxChars: llmsConfig.maxChars,
2606
2682
  sections: llmsConfig.sections,
2683
+ apiCatalog: apiCatalogEnabled,
2607
2684
  openapi: openapiDiscovery
2608
2685
  });
2609
- llmsCacheByLocale.set(key, next);
2686
+ llmsCache.set(ctx.locale, apiRoute, next);
2610
2687
  return next;
2611
2688
  }
2612
- return {
2689
+ async function resolveStandardsResponse(request, url) {
2690
+ const apiRoute = resolveDocsRequestApiRoute(url, configuredApiRouteInput);
2691
+ const resolved = resolveDocsStandardsDiscoveryRequest(url, { apiRoute });
2692
+ if (!resolved) return null;
2693
+ const method = request.method.toUpperCase();
2694
+ const needsSkill = (method === "GET" || method === "HEAD") && resolved.kind !== "api-catalog";
2695
+ const fallbackSkillDocument = needsSkill ? renderSkillDocument({
2696
+ origin: url.origin,
2697
+ entry,
2698
+ apiRoute,
2699
+ apiCatalog: apiCatalogEnabled,
2700
+ i18n,
2701
+ search: searchConfig,
2702
+ mcp: mcpConfig,
2703
+ feedback: agentFeedbackConfig,
2704
+ llms: llmsConfig,
2705
+ sitemap: sitemapConfig,
2706
+ robots: robotsConfig,
2707
+ openapi: openapiDiscovery
2708
+ }) : "";
2709
+ return createDocsStandardsDiscoveryResponse({
2710
+ request,
2711
+ preferredSkillDocument: needsSkill ? readRootSkillDocument(root) : null,
2712
+ fallbackSkillDocument,
2713
+ origin: url.origin,
2714
+ entry,
2715
+ docsPath,
2716
+ apiCatalog: apiCatalogEnabled,
2717
+ apiRoute,
2718
+ i18n,
2719
+ search: searchConfig,
2720
+ mcp: mcpConfig,
2721
+ feedback: agentFeedbackConfig,
2722
+ llms: llmsConfig,
2723
+ sitemap: sitemapConfig,
2724
+ robots: robotsConfig,
2725
+ openapi: openapiDiscovery,
2726
+ markdown: {
2727
+ acceptHeader: true,
2728
+ signatureAgentHeader: true
2729
+ }
2730
+ });
2731
+ }
2732
+ const handlers = {
2613
2733
  async GET(request) {
2614
2734
  trackTelemetryRequest(request);
2615
2735
  const ctx = resolveContextFromRequest(request);
2616
2736
  const url = new URL(request.url);
2737
+ const requestMethod = request.method.toUpperCase();
2738
+ const isHeadRequest = requestMethod === "HEAD";
2739
+ const requestApiRoute = resolveDocsRequestApiRoute(url, configuredApiRouteInput);
2617
2740
  const requestAnalyticsProperties = getRequestAnalyticsProperties(request);
2618
2741
  if (isDocsConfigRequest(url)) return Response.json(buildDocsConfigMap(docsConfigMapInput), { headers: {
2619
2742
  "Cache-Control": "public, max-age=0, s-maxage=3600",
@@ -2621,7 +2744,9 @@ function createDocsAPI(options) {
2621
2744
  } });
2622
2745
  if (isDocsDiagnosticsRequest(url)) return Response.json(buildDocsDiagnostics(docsDiagnosticsInput, {
2623
2746
  adapter: "next",
2747
+ apiCatalog: apiCatalogEnabled,
2624
2748
  entry,
2749
+ apiRoute: requestApiRoute,
2625
2750
  i18n,
2626
2751
  mcp: mcpConfig,
2627
2752
  feedback: agentFeedbackConfig,
@@ -2630,6 +2755,8 @@ function createDocsAPI(options) {
2630
2755
  "Cache-Control": "public, max-age=0, s-maxage=3600",
2631
2756
  "X-Robots-Tag": "noindex"
2632
2757
  } });
2758
+ const standardsResponse = await resolveStandardsResponse(request, url);
2759
+ if (standardsResponse) return standardsResponse;
2633
2760
  if (resolveAgentSpecRequest(url)) {
2634
2761
  await emitDocsAnalyticsEvent(analytics, {
2635
2762
  type: "agent_spec_request",
@@ -2639,12 +2766,20 @@ function createDocsAPI(options) {
2639
2766
  locale: ctx.locale,
2640
2767
  properties: {
2641
2768
  ...requestAnalyticsProperties,
2642
- method: "GET"
2769
+ method: requestMethod
2643
2770
  }
2644
2771
  });
2772
+ if (isHeadRequest) return new Response(null, { headers: {
2773
+ "Content-Type": "application/json",
2774
+ "Cache-Control": "public, max-age=0, s-maxage=3600",
2775
+ Link: discoveryLinkHeader,
2776
+ "X-Robots-Tag": "noindex"
2777
+ } });
2645
2778
  return Response.json(buildAgentSpec({
2646
2779
  origin: url.origin,
2647
2780
  entry,
2781
+ apiRoute: requestApiRoute,
2782
+ apiCatalog: apiCatalogEnabled,
2648
2783
  i18n,
2649
2784
  search: searchConfig,
2650
2785
  mcp: mcpConfig,
@@ -2655,6 +2790,7 @@ function createDocsAPI(options) {
2655
2790
  openapi: openapiDiscovery
2656
2791
  }), { headers: {
2657
2792
  "Cache-Control": "public, max-age=0, s-maxage=3600",
2793
+ Link: discoveryLinkHeader,
2658
2794
  "X-Robots-Tag": "noindex"
2659
2795
  } });
2660
2796
  }
@@ -2666,6 +2802,11 @@ function createDocsAPI(options) {
2666
2802
  "X-Robots-Tag": "noindex"
2667
2803
  }
2668
2804
  });
2805
+ if (isHeadRequest) return new Response(null, { headers: {
2806
+ "Content-Type": "application/json; charset=utf-8",
2807
+ "Cache-Control": "public, max-age=0, s-maxage=3600",
2808
+ "X-Robots-Tag": "noindex"
2809
+ } });
2669
2810
  const document = await buildApiReferenceOpenApiDocumentAsync(apiReferenceDocsConfig, {
2670
2811
  framework: "next",
2671
2812
  rootDir: root,
@@ -2691,10 +2832,10 @@ function createDocsAPI(options) {
2691
2832
  locale: ctx.locale,
2692
2833
  properties: {
2693
2834
  ...requestAnalyticsProperties,
2694
- method: "GET"
2835
+ method: requestMethod
2695
2836
  }
2696
2837
  });
2697
- return new Response(JSON.stringify(agentFeedbackConfig.schema, null, 2), { headers: {
2838
+ return new Response(isHeadRequest ? null : JSON.stringify(agentFeedbackConfig.schema, null, 2), { headers: {
2698
2839
  "Content-Type": "application/schema+json; charset=utf-8",
2699
2840
  "Cache-Control": "public, max-age=0, s-maxage=3600",
2700
2841
  "X-Robots-Tag": "noindex"
@@ -2709,12 +2850,14 @@ function createDocsAPI(options) {
2709
2850
  locale: ctx.locale,
2710
2851
  properties: {
2711
2852
  ...requestAnalyticsProperties,
2712
- method: "GET"
2853
+ method: requestMethod
2713
2854
  }
2714
2855
  });
2715
- return new Response(readRootAgentsDocument(root) ?? renderAgentsDocument({
2856
+ return new Response(isHeadRequest ? null : readRootAgentsDocument(root) ?? renderAgentsDocument({
2716
2857
  origin: url.origin,
2717
2858
  entry,
2859
+ apiRoute: requestApiRoute,
2860
+ apiCatalog: apiCatalogEnabled,
2718
2861
  search: searchConfig,
2719
2862
  mcp: mcpConfig,
2720
2863
  feedback: agentFeedbackConfig,
@@ -2725,6 +2868,7 @@ function createDocsAPI(options) {
2725
2868
  }), { headers: {
2726
2869
  "Content-Type": "text/markdown; charset=utf-8",
2727
2870
  "Cache-Control": "public, max-age=0, s-maxage=3600",
2871
+ Link: discoveryLinkHeader,
2728
2872
  "X-Robots-Tag": "noindex"
2729
2873
  } });
2730
2874
  }
@@ -2737,12 +2881,14 @@ function createDocsAPI(options) {
2737
2881
  locale: ctx.locale,
2738
2882
  properties: {
2739
2883
  ...requestAnalyticsProperties,
2740
- method: "GET"
2884
+ method: requestMethod
2741
2885
  }
2742
2886
  });
2743
- return new Response(readRootSkillDocument(root) ?? renderSkillDocument({
2887
+ return new Response(isHeadRequest ? null : readRootSkillDocument(root) ?? renderSkillDocument({
2744
2888
  origin: url.origin,
2745
2889
  entry,
2890
+ apiRoute: requestApiRoute,
2891
+ apiCatalog: apiCatalogEnabled,
2746
2892
  i18n,
2747
2893
  search: searchConfig,
2748
2894
  mcp: mcpConfig,
@@ -2754,12 +2900,14 @@ function createDocsAPI(options) {
2754
2900
  }), { headers: {
2755
2901
  "Content-Type": "text/markdown; charset=utf-8",
2756
2902
  "Cache-Control": "public, max-age=0, s-maxage=3600",
2903
+ Link: discoveryLinkHeader,
2757
2904
  "X-Robots-Tag": "noindex"
2758
2905
  } });
2759
2906
  }
2760
2907
  const robotsResponse = createDocsRobotsResponse({
2761
2908
  request,
2762
2909
  entry,
2910
+ apiCatalog: apiCatalogEnabled,
2763
2911
  sitemap: sitemapConfig,
2764
2912
  baseUrl: llmsConfig.baseUrl ?? url.origin,
2765
2913
  robots: robotsConfig
@@ -2767,6 +2915,7 @@ function createDocsAPI(options) {
2767
2915
  if (robotsResponse) return robotsResponse;
2768
2916
  const sitemapResponse = createDocsSitemapResponse({
2769
2917
  request,
2918
+ apiRoute: requestApiRoute,
2770
2919
  sitemap: sitemapConfig,
2771
2920
  entry,
2772
2921
  siteTitle: llmsConfig.siteTitle ?? "Documentation",
@@ -2815,6 +2964,7 @@ function createDocsAPI(options) {
2815
2964
  });
2816
2965
  return createDocsMarkdownResponse({
2817
2966
  request,
2967
+ apiRoute: requestApiRoute,
2818
2968
  document: null,
2819
2969
  entry,
2820
2970
  requestedPath: markdownRequest.requestedPath,
@@ -2855,6 +3005,7 @@ function createDocsAPI(options) {
2855
3005
  });
2856
3006
  return createDocsMarkdownResponse({
2857
3007
  request,
3008
+ apiRoute: requestApiRoute,
2858
3009
  document,
2859
3010
  entry,
2860
3011
  requestedPath: markdownRequest.requestedPath,
@@ -2865,7 +3016,7 @@ function createDocsAPI(options) {
2865
3016
  sitemap: sitemapConfig
2866
3017
  });
2867
3018
  }
2868
- const llmsRequest = resolveDocsLlmsTxtRequest(url, llmsConfig, docsPath);
3019
+ const llmsRequest = resolveDocsLlmsTxtRequest(url, llmsConfig, docsPath, { apiRoute: requestApiRoute });
2869
3020
  if (llmsRequest) {
2870
3021
  if (!llmsConfig.enabled) return new Response("Not Found", {
2871
3022
  status: 404,
@@ -2874,7 +3025,7 @@ function createDocsAPI(options) {
2874
3025
  "X-Robots-Tag": "noindex"
2875
3026
  }
2876
3027
  });
2877
- const selected = selectDocsLlmsTxtContent(getLlmsContent(ctx), llmsRequest);
3028
+ const selected = selectDocsLlmsTxtContent(getLlmsContent(ctx, requestApiRoute), llmsRequest);
2878
3029
  if (!selected) return new Response("Not Found", {
2879
3030
  status: 404,
2880
3031
  headers: {
@@ -2941,6 +3092,8 @@ function createDocsAPI(options) {
2941
3092
  trackTelemetryRequest(request);
2942
3093
  const url = new URL(request.url);
2943
3094
  const requestAnalyticsProperties = getRequestAnalyticsProperties(request);
3095
+ const standardsResponse = await resolveStandardsResponse(request, url);
3096
+ if (standardsResponse) return standardsResponse;
2944
3097
  const agentFeedbackRequest = resolveAgentFeedbackRequest(url, agentFeedbackConfig);
2945
3098
  if (agentFeedbackRequest) {
2946
3099
  if (agentFeedbackRequest.kind === "schema") return Response.json({ error: "Method Not Allowed" }, {
@@ -3036,6 +3189,17 @@ function createDocsAPI(options) {
3036
3189
  }), analytics, observability, { locale: ctx.locale });
3037
3190
  }
3038
3191
  };
3192
+ return {
3193
+ ...handlers,
3194
+ async HEAD(request) {
3195
+ const response = await handlers.GET(request);
3196
+ return new Response(null, {
3197
+ status: response.status,
3198
+ statusText: response.statusText,
3199
+ headers: response.headers
3200
+ });
3201
+ }
3202
+ };
3039
3203
  }
3040
3204
  /**
3041
3205
  * Create MCP route handlers for `/api/docs/mcp`.
package/dist/search.d.mts CHANGED
@@ -27,6 +27,7 @@ declare function createDocsSearchAPI(options?: {
27
27
  entry?: string;
28
28
  language?: string;
29
29
  }): {
30
+ HEAD(request: Request): Promise<Response>;
30
31
  GET(request: Request): Promise<Response>;
31
32
  POST(request: Request): Promise<Response>;
32
33
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.2.58",
3
+ "version": "0.2.59",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -146,7 +146,7 @@
146
146
  "tsdown": "^0.20.3",
147
147
  "typescript": "^5.9.3",
148
148
  "vitest": "^4.1.8",
149
- "@farming-labs/docs": "0.2.58"
149
+ "@farming-labs/docs": "0.2.59"
150
150
  },
151
151
  "peerDependencies": {
152
152
  "@farming-labs/docs": ">=0.0.1",