@farming-labs/theme 0.1.2 → 0.1.4

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.
@@ -1,4 +1,4 @@
1
- import { DocsI18nConfig, DocsMcpConfig, OrderingItem } from "@farming-labs/docs";
1
+ import { DocsI18nConfig, DocsMcpConfig, DocsSearchConfig, OrderingItem } from "@farming-labs/docs";
2
2
 
3
3
  //#region src/docs-api.d.ts
4
4
  interface AIProviderConfig {
@@ -26,14 +26,19 @@ interface AIOptions {
26
26
  maxResults?: number;
27
27
  }
28
28
  interface DocsAPIOptions {
29
+ rootDir?: string;
29
30
  /** Docs entry folder (default: read from docs.config) */
30
31
  entry?: string;
32
+ /** Override the docs content directory when it does not live in app/<entry>. */
33
+ contentDir?: string;
31
34
  /** Search language (default: "english") */
32
35
  language?: string;
33
36
  /** AI chat configuration */
34
37
  ai?: AIOptions;
35
38
  /** i18n config (optional) */
36
39
  i18n?: DocsI18nConfig;
40
+ /** Search configuration */
41
+ search?: boolean | DocsSearchConfig;
37
42
  }
38
43
  interface DocsMCPAPIOptions {
39
44
  rootDir?: string;
@@ -44,6 +49,7 @@ interface DocsMCPAPIOptions {
44
49
  };
45
50
  ordering?: "alphabetical" | "numeric" | OrderingItem[];
46
51
  mcp?: boolean | DocsMcpConfig;
52
+ search?: boolean | DocsSearchConfig;
47
53
  }
48
54
  /**
49
55
  * Create a unified docs API route handler.
@@ -57,18 +63,22 @@ interface DocsMCPAPIOptions {
57
63
  * @example
58
64
  * ```ts
59
65
  * // app/api/docs/route.ts (auto-generated by withDocs)
60
- * import { createDocsAPI } from "@farming-labs/theme/api";
66
+ * import { createDocsAPI } from "@farming-labs/next/api";
61
67
  * export const { GET, POST } = createDocsAPI();
62
68
  * export const revalidate = false;
63
69
  * ```
64
70
  *
65
71
  * @param options - Optional overrides (entry, language, ai config)
66
72
  */
73
+ /**
74
+ * @deprecated Prefer `createDocsAPI` from `@farming-labs/next/api` in Next.js apps.
75
+ * The `@farming-labs/theme/api` path is kept for compatibility and will be phased out.
76
+ */
67
77
  declare function createDocsAPI(options?: DocsAPIOptions): {
68
78
  /**
69
79
  * GET handler — search, llms.txt, or llms-full.txt depending on query params.
70
80
  */
71
- GET(request: Request): Response | Promise<Response>;
81
+ GET(request: Request): Promise<Response>;
72
82
  /**
73
83
  * POST handler — AI chat with RAG.
74
84
  * Body: `{ messages: [{ role: "user", content: "How do I …?" }] }`
@@ -81,6 +91,10 @@ declare function createDocsAPI(options?: DocsAPIOptions): {
81
91
  *
82
92
  * Returns `{ GET, POST, DELETE }` for use in a Next.js route handler.
83
93
  */
94
+ /**
95
+ * @deprecated Prefer `createDocsMCPAPI` from `@farming-labs/next/api` in Next.js apps.
96
+ * The `@farming-labs/theme/api` path is kept for compatibility and will be phased out.
97
+ */
84
98
  declare function createDocsMCPAPI(options?: DocsMCPAPIOptions): {
85
99
  GET(request: Request): Promise<Response>;
86
100
  POST(request: Request): Promise<Response>;
package/dist/docs-api.mjs CHANGED
@@ -1,10 +1,9 @@
1
1
  import { withLangInUrl } from "./i18n.mjs";
2
2
  import { getNextAppDir } from "./get-app-dir.mjs";
3
- import { resolveDocsI18n, resolveDocsLocale } from "@farming-labs/docs";
3
+ import { performDocsSearch, resolveDocsI18n, resolveDocsLocale, resolveSearchRequestConfig } from "@farming-labs/docs";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import matter from "gray-matter";
7
- import { createSearchAPI } from "fumadocs-core/search/server";
8
7
  import { createDocsMcpHttpHandler, createFilesystemDocsMcpSource } from "@farming-labs/docs/server";
9
8
 
10
9
  //#region src/docs-api.ts
@@ -22,7 +21,7 @@ import { createDocsMcpHttpHandler, createFilesystemDocsMcpSource } from "@farmin
22
21
  * @example
23
22
  * ```ts
24
23
  * // app/api/docs/route.ts (auto-generated by withDocs)
25
- * import { createDocsAPI } from "@farming-labs/theme/api";
24
+ * import { createDocsAPI } from "@farming-labs/next/api";
26
25
  * export const { GET, POST } = createDocsAPI();
27
26
  * export const revalidate = false;
28
27
  * ```
@@ -289,13 +288,16 @@ function scanDocsDir(docsDir, entry, locale) {
289
288
  const { data } = matter(raw);
290
289
  const title = data.title || slugParts[slugParts.length - 1]?.replace(/-/g, " ") || "Documentation";
291
290
  const description = data.description;
291
+ const { content: rawContent } = matter(raw);
292
292
  const content = stripMdx(raw);
293
293
  const url = withLangInUrl(slugParts.length === 0 ? `/${entry}` : `/${entry}/${slugParts.join("/")}`, locale);
294
294
  indexes.push({
295
295
  title,
296
296
  description,
297
297
  content,
298
- url
298
+ rawContent,
299
+ url,
300
+ locale
299
301
  });
300
302
  } catch {}
301
303
  let entries;
@@ -334,7 +336,7 @@ function resolveModelAndProvider(aiConfig, requestedModelId) {
334
336
  apiKey
335
337
  };
336
338
  }
337
- async function handleAskAI(request, indexes, searchServer, aiConfig) {
339
+ async function handleAskAI(request, indexes, aiConfig) {
338
340
  let body;
339
341
  try {
340
342
  body = await request.json();
@@ -445,21 +447,45 @@ function generateLlmsTxt(indexes, options) {
445
447
  * @example
446
448
  * ```ts
447
449
  * // app/api/docs/route.ts (auto-generated by withDocs)
448
- * import { createDocsAPI } from "@farming-labs/theme/api";
450
+ * import { createDocsAPI } from "@farming-labs/next/api";
449
451
  * export const { GET, POST } = createDocsAPI();
450
452
  * export const revalidate = false;
451
453
  * ```
452
454
  *
453
455
  * @param options - Optional overrides (entry, language, ai config)
454
456
  */
457
+ /**
458
+ * @deprecated Prefer `createDocsAPI` from `@farming-labs/next/api` in Next.js apps.
459
+ * The `@farming-labs/theme/api` path is kept for compatibility and will be phased out.
460
+ */
455
461
  function createDocsAPI(options) {
456
- const root = process.cwd();
462
+ const root = options?.rootDir ?? process.cwd();
457
463
  const entry = options?.entry ?? readEntry(root);
458
464
  const appDir = getNextAppDir(root);
459
- const language = options?.language ?? "english";
465
+ const contentDir = options?.contentDir ?? path.join(appDir, entry);
460
466
  const i18n = resolveDocsI18n(options?.i18n ?? readI18nConfig(root));
461
467
  const aiConfig = options?.ai ?? readAIConfig(root);
468
+ const searchConfig = options?.search;
462
469
  const llmsConfig = readLlmsTxtConfig(root);
470
+ function resolveDocsDirCandidates(locale) {
471
+ const relativeCandidates = /* @__PURE__ */ new Set();
472
+ if (path.isAbsolute(contentDir)) return [locale ? path.join(contentDir, locale) : contentDir];
473
+ relativeCandidates.add(contentDir);
474
+ relativeCandidates.add(path.join("app", entry));
475
+ relativeCandidates.add(path.join("src", "app", entry));
476
+ const rootCandidates = new Set([root]);
477
+ if (path.basename(root) === "server") rootCandidates.add(path.resolve(root, "..", ".."));
478
+ else {
479
+ rootCandidates.add(path.join(root, ".next", "server"));
480
+ rootCandidates.add(path.join(root, ".next-build", "server"));
481
+ }
482
+ const resolved = /* @__PURE__ */ new Set();
483
+ for (const base of rootCandidates) for (const relative of relativeCandidates) {
484
+ const candidate = path.join(base, relative);
485
+ resolved.add(locale ? path.join(candidate, locale) : candidate);
486
+ }
487
+ return [...resolved];
488
+ }
463
489
  function resolveLocaleFromRequest(request) {
464
490
  if (!i18n) return void 0;
465
491
  const direct = resolveDocsLocale(new URL(request.url).searchParams, i18n);
@@ -474,37 +500,29 @@ function createDocsAPI(options) {
474
500
  function resolveContextFromRequest(request) {
475
501
  if (!i18n) return {
476
502
  entryPath: entry,
477
- docsDir: path.join(root, appDir, entry)
503
+ docsDirs: resolveDocsDirCandidates()
478
504
  };
479
505
  const locale = resolveLocaleFromRequest(request) ?? i18n.defaultLocale;
480
506
  return {
481
507
  entryPath: entry,
482
508
  locale,
483
- docsDir: path.join(root, appDir, entry, locale)
509
+ docsDirs: resolveDocsDirCandidates(locale)
484
510
  };
485
511
  }
486
512
  const indexesByLocale = /* @__PURE__ */ new Map();
487
- const searchApiByLocale = /* @__PURE__ */ new Map();
488
513
  const llmsCacheByLocale = /* @__PURE__ */ new Map();
489
514
  function getIndexes(ctx) {
490
515
  const key = ctx.locale ?? "__default__";
491
516
  const cached = indexesByLocale.get(key);
492
517
  if (cached) return cached;
493
- const next = scanDocsDir(ctx.docsDir, ctx.entryPath, ctx.locale);
518
+ let next = [];
519
+ for (const docsDir of ctx.docsDirs) {
520
+ next = scanDocsDir(docsDir, ctx.entryPath, ctx.locale);
521
+ if (next.length > 0) break;
522
+ }
494
523
  indexesByLocale.set(key, next);
495
524
  return next;
496
525
  }
497
- function getSearchAPI(ctx) {
498
- const key = ctx.locale ?? "__default__";
499
- const cached = searchApiByLocale.get(key);
500
- if (cached) return cached;
501
- const api = createSearchAPI("simple", {
502
- language,
503
- indexes: getIndexes(ctx)
504
- });
505
- searchApiByLocale.set(key, api);
506
- return api;
507
- }
508
526
  function getLlmsContent(ctx) {
509
527
  const key = ctx.locale ?? "__default__";
510
528
  const cached = llmsCacheByLocale.get(key);
@@ -518,9 +536,10 @@ function createDocsAPI(options) {
518
536
  return next;
519
537
  }
520
538
  return {
521
- GET(request) {
539
+ async GET(request) {
522
540
  const ctx = resolveContextFromRequest(request);
523
- const format = new URL(request.url).searchParams.get("format");
541
+ const url = new URL(request.url);
542
+ const format = url.searchParams.get("format");
524
543
  if (format === "llms") return new Response(getLlmsContent(ctx).llmsTxt, { headers: {
525
544
  "Content-Type": "text/plain; charset=utf-8",
526
545
  "Cache-Control": "public, max-age=3600"
@@ -529,12 +548,21 @@ function createDocsAPI(options) {
529
548
  "Content-Type": "text/plain; charset=utf-8",
530
549
  "Cache-Control": "public, max-age=3600"
531
550
  } });
532
- return getSearchAPI(ctx).GET(request);
551
+ const query = url.searchParams.get("query")?.trim();
552
+ if (!query) return new Response(JSON.stringify([]), { headers: { "Content-Type": "application/json" } });
553
+ const results = await performDocsSearch({
554
+ pages: getIndexes(ctx),
555
+ query,
556
+ search: resolveSearchRequestConfig(searchConfig, request.url),
557
+ locale: ctx.locale,
558
+ pathname: url.searchParams.get("pathname") ?? void 0,
559
+ siteTitle: llmsConfig.siteTitle ?? "Documentation"
560
+ });
561
+ return new Response(JSON.stringify(results), { headers: { "Content-Type": "application/json" } });
533
562
  },
534
563
  async POST(request) {
535
564
  if (!aiConfig.enabled) return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
536
- const ctx = resolveContextFromRequest(request);
537
- return handleAskAI(request, getIndexes(ctx), getSearchAPI(ctx), aiConfig);
565
+ return handleAskAI(request, getIndexes(resolveContextFromRequest(request)), aiConfig);
538
566
  }
539
567
  };
540
568
  }
@@ -543,6 +571,10 @@ function createDocsAPI(options) {
543
571
  *
544
572
  * Returns `{ GET, POST, DELETE }` for use in a Next.js route handler.
545
573
  */
574
+ /**
575
+ * @deprecated Prefer `createDocsMCPAPI` from `@farming-labs/next/api` in Next.js apps.
576
+ * The `@farming-labs/theme/api` path is kept for compatibility and will be phased out.
577
+ */
546
578
  function createDocsMCPAPI(options = {}) {
547
579
  const rootDir = options.rootDir ?? process.cwd();
548
580
  const entry = options.entry ?? readEntry(rootDir);
@@ -558,6 +590,7 @@ function createDocsMCPAPI(options = {}) {
558
590
  ordering: options.ordering
559
591
  }),
560
592
  mcp: options.mcp ?? readMcpConfig(rootDir),
593
+ search: options.search,
561
594
  defaultName: navTitle
562
595
  });
563
596
  return {
@@ -18,6 +18,9 @@ function stripHtml(html) {
18
18
  }
19
19
  return html.replace(/<[^>]+>/g, "");
20
20
  }
21
+ function stripSearchPreview(text) {
22
+ return text.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "").replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/^\|?[\s:-]+(\|[\s:-]+)+\|?\s*$/gm, "").replace(/\|/g, " ").replace(/^[-*+]\s+/gm, "").replace(/(\*{1,3}|_{1,3})(.*?)\1/g, "$2").replace(/`([^`]+)`/g, "$1").replace(/`+/g, "").replace(/\s{2,}/g, " ").trim();
23
+ }
21
24
  function fuzzyScore(query, text) {
22
25
  const q = query.trim().toLowerCase();
23
26
  const t = text.toLowerCase();
@@ -366,12 +369,12 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
366
369
  const res = await fetch(requestUrl.toString());
367
370
  if (!res.ok || cancelled) return;
368
371
  const items = (await res.json()).map((r) => {
369
- const label = stripHtml(r.content);
372
+ const label = stripSearchPreview(stripHtml(r.content));
370
373
  const { score, indices } = fuzzyScore(debouncedQuery, label);
371
374
  return {
372
375
  id: r.id,
373
376
  label,
374
- subtitle: labelForType(r.type),
377
+ subtitle: r.description ? stripSearchPreview(stripHtml(r.description)) : labelForType(r.type),
375
378
  url: withLangInUrl(r.url, activeLocale),
376
379
  icon: iconForType(r.type),
377
380
  score,
@@ -89,6 +89,27 @@ function localizeInternalLinks(root, locale) {
89
89
  } catch {}
90
90
  }
91
91
  }
92
+ function decodeHashTarget(hash) {
93
+ const value = hash.startsWith("#") ? hash.slice(1) : hash;
94
+ try {
95
+ return decodeURIComponent(value);
96
+ } catch {
97
+ return value;
98
+ }
99
+ }
100
+ function escapeIdSelector(value) {
101
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
102
+ return value.replace(/["\\.#:[\]>+~(){}^$|*?=!'`\s]/g, "\\$&");
103
+ }
104
+ function scrollToHashTarget(hash) {
105
+ if (!hash || hash === "#") return false;
106
+ const targetId = decodeHashTarget(hash);
107
+ if (!targetId) return false;
108
+ const target = document.getElementById(targetId) ?? document.querySelector(`#${escapeIdSelector(targetId)}`);
109
+ if (!target) return false;
110
+ target.scrollIntoView({ block: "start" });
111
+ return true;
112
+ }
92
113
  function injectTitleDecorations(node, { description, belowTitle }) {
93
114
  if (!description && !belowTitle) return {
94
115
  node,
@@ -172,6 +193,33 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
172
193
  children,
173
194
  pathname
174
195
  ]);
196
+ useEffect(() => {
197
+ let frame = 0;
198
+ let timeout = 0;
199
+ let cancelled = false;
200
+ const scheduleScroll = (attempt = 0) => {
201
+ if (cancelled) return;
202
+ const hash = window.location.hash;
203
+ if (!hash) return;
204
+ if (scrollToHashTarget(hash) || attempt >= 20) return;
205
+ timeout = window.setTimeout(() => {
206
+ frame = requestAnimationFrame(() => scheduleScroll(attempt + 1));
207
+ }, 100);
208
+ };
209
+ frame = requestAnimationFrame(() => scheduleScroll());
210
+ const onHashChange = () => {
211
+ cancelAnimationFrame(frame);
212
+ clearTimeout(timeout);
213
+ frame = requestAnimationFrame(() => scheduleScroll());
214
+ };
215
+ window.addEventListener("hashchange", onHashChange);
216
+ return () => {
217
+ cancelled = true;
218
+ window.removeEventListener("hashchange", onHashChange);
219
+ cancelAnimationFrame(frame);
220
+ clearTimeout(timeout);
221
+ };
222
+ }, [pathname, children]);
175
223
  const showActions = copyMarkdown || openDocs;
176
224
  const showActionsBelowTitle = showActions && pageActionsPosition === "below-title";
177
225
  const showActionsAboveTitle = showActions && pageActionsPosition === "above-title";
package/dist/search.d.mts CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Search API — backward-compatible re-export.
4
4
  *
5
- * New projects should use `createDocsAPI` from `@farming-labs/theme/api`
5
+ * New projects should use `createDocsAPI` from `@farming-labs/next/api`
6
6
  * which provides a unified handler for both search (GET) and AI chat (POST).
7
7
  *
8
8
  * This module is kept for backward compatibility with existing projects
@@ -11,7 +11,7 @@
11
11
  * @example
12
12
  * ```ts
13
13
  * // Recommended (new): app/api/docs/route.ts
14
- * import { createDocsAPI } from "@farming-labs/theme/api";
14
+ * import { createDocsAPI } from "@farming-labs/next/api";
15
15
  * export const { GET, POST } = createDocsAPI();
16
16
  *
17
17
  * // Legacy: app/api/search/route.ts
@@ -20,14 +20,14 @@
20
20
  * ```
21
21
  */
22
22
  /**
23
- * @deprecated Use `createDocsAPI` from `@farming-labs/theme/api` instead.
23
+ * @deprecated Use `createDocsAPI` from `@farming-labs/next/api` instead.
24
24
  * This function is kept for backward compatibility.
25
25
  */
26
26
  declare function createDocsSearchAPI(options?: {
27
27
  entry?: string;
28
28
  language?: string;
29
29
  }): {
30
- GET(request: Request): Response | Promise<Response>;
30
+ GET(request: Request): Promise<Response>;
31
31
  POST(request: Request): Promise<Response>;
32
32
  };
33
33
  //#endregion
package/dist/search.mjs CHANGED
@@ -4,7 +4,7 @@ import { createDocsAPI } from "./docs-api.mjs";
4
4
  /**
5
5
  * Search API — backward-compatible re-export.
6
6
  *
7
- * New projects should use `createDocsAPI` from `@farming-labs/theme/api`
7
+ * New projects should use `createDocsAPI` from `@farming-labs/next/api`
8
8
  * which provides a unified handler for both search (GET) and AI chat (POST).
9
9
  *
10
10
  * This module is kept for backward compatibility with existing projects
@@ -13,7 +13,7 @@ import { createDocsAPI } from "./docs-api.mjs";
13
13
  * @example
14
14
  * ```ts
15
15
  * // Recommended (new): app/api/docs/route.ts
16
- * import { createDocsAPI } from "@farming-labs/theme/api";
16
+ * import { createDocsAPI } from "@farming-labs/next/api";
17
17
  * export const { GET, POST } = createDocsAPI();
18
18
  *
19
19
  * // Legacy: app/api/search/route.ts
@@ -22,7 +22,7 @@ import { createDocsAPI } from "./docs-api.mjs";
22
22
  * ```
23
23
  */
24
24
  /**
25
- * @deprecated Use `createDocsAPI` from `@farming-labs/theme/api` instead.
25
+ * @deprecated Use `createDocsAPI` from `@farming-labs/next/api` instead.
26
26
  * This function is kept for backward compatibility.
27
27
  */
28
28
  function createDocsSearchAPI(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -127,7 +127,7 @@
127
127
  "tsdown": "^0.20.3",
128
128
  "typescript": "^5.9.3",
129
129
  "vitest": "^3.2.4",
130
- "@farming-labs/docs": "0.1.2"
130
+ "@farming-labs/docs": "0.1.4"
131
131
  },
132
132
  "peerDependencies": {
133
133
  "@farming-labs/docs": ">=0.0.1",