@farming-labs/theme 0.1.1 → 0.1.3

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 {
@@ -34,6 +34,8 @@ interface DocsAPIOptions {
34
34
  ai?: AIOptions;
35
35
  /** i18n config (optional) */
36
36
  i18n?: DocsI18nConfig;
37
+ /** Search configuration */
38
+ search?: boolean | DocsSearchConfig;
37
39
  }
38
40
  interface DocsMCPAPIOptions {
39
41
  rootDir?: string;
@@ -44,6 +46,7 @@ interface DocsMCPAPIOptions {
44
46
  };
45
47
  ordering?: "alphabetical" | "numeric" | OrderingItem[];
46
48
  mcp?: boolean | DocsMcpConfig;
49
+ search?: boolean | DocsSearchConfig;
47
50
  }
48
51
  /**
49
52
  * Create a unified docs API route handler.
@@ -57,7 +60,7 @@ interface DocsMCPAPIOptions {
57
60
  * @example
58
61
  * ```ts
59
62
  * // app/api/docs/route.ts (auto-generated by withDocs)
60
- * import { createDocsAPI } from "@farming-labs/theme/api";
63
+ * import { createDocsAPI } from "@farming-labs/next/api";
61
64
  * export const { GET, POST } = createDocsAPI();
62
65
  * export const revalidate = false;
63
66
  * ```
@@ -68,7 +71,7 @@ declare function createDocsAPI(options?: DocsAPIOptions): {
68
71
  /**
69
72
  * GET handler — search, llms.txt, or llms-full.txt depending on query params.
70
73
  */
71
- GET(request: Request): Response | Promise<Response>;
74
+ GET(request: Request): Promise<Response>;
72
75
  /**
73
76
  * POST handler — AI chat with RAG.
74
77
  * Body: `{ messages: [{ role: "user", content: "How do I …?" }] }`
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,7 +447,7 @@ 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
  * ```
@@ -456,9 +458,9 @@ function createDocsAPI(options) {
456
458
  const root = process.cwd();
457
459
  const entry = options?.entry ?? readEntry(root);
458
460
  const appDir = getNextAppDir(root);
459
- const language = options?.language ?? "english";
460
461
  const i18n = resolveDocsI18n(options?.i18n ?? readI18nConfig(root));
461
462
  const aiConfig = options?.ai ?? readAIConfig(root);
463
+ const searchConfig = options?.search;
462
464
  const llmsConfig = readLlmsTxtConfig(root);
463
465
  function resolveLocaleFromRequest(request) {
464
466
  if (!i18n) return void 0;
@@ -484,7 +486,6 @@ function createDocsAPI(options) {
484
486
  };
485
487
  }
486
488
  const indexesByLocale = /* @__PURE__ */ new Map();
487
- const searchApiByLocale = /* @__PURE__ */ new Map();
488
489
  const llmsCacheByLocale = /* @__PURE__ */ new Map();
489
490
  function getIndexes(ctx) {
490
491
  const key = ctx.locale ?? "__default__";
@@ -494,17 +495,6 @@ function createDocsAPI(options) {
494
495
  indexesByLocale.set(key, next);
495
496
  return next;
496
497
  }
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
498
  function getLlmsContent(ctx) {
509
499
  const key = ctx.locale ?? "__default__";
510
500
  const cached = llmsCacheByLocale.get(key);
@@ -518,9 +508,10 @@ function createDocsAPI(options) {
518
508
  return next;
519
509
  }
520
510
  return {
521
- GET(request) {
511
+ async GET(request) {
522
512
  const ctx = resolveContextFromRequest(request);
523
- const format = new URL(request.url).searchParams.get("format");
513
+ const url = new URL(request.url);
514
+ const format = url.searchParams.get("format");
524
515
  if (format === "llms") return new Response(getLlmsContent(ctx).llmsTxt, { headers: {
525
516
  "Content-Type": "text/plain; charset=utf-8",
526
517
  "Cache-Control": "public, max-age=3600"
@@ -529,12 +520,21 @@ function createDocsAPI(options) {
529
520
  "Content-Type": "text/plain; charset=utf-8",
530
521
  "Cache-Control": "public, max-age=3600"
531
522
  } });
532
- return getSearchAPI(ctx).GET(request);
523
+ const query = url.searchParams.get("query")?.trim();
524
+ if (!query) return new Response(JSON.stringify([]), { headers: { "Content-Type": "application/json" } });
525
+ const results = await performDocsSearch({
526
+ pages: getIndexes(ctx),
527
+ query,
528
+ search: resolveSearchRequestConfig(searchConfig, request.url),
529
+ locale: ctx.locale,
530
+ pathname: url.searchParams.get("pathname") ?? void 0,
531
+ siteTitle: llmsConfig.siteTitle ?? "Documentation"
532
+ });
533
+ return new Response(JSON.stringify(results), { headers: { "Content-Type": "application/json" } });
533
534
  },
534
535
  async POST(request) {
535
536
  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);
537
+ return handleAskAI(request, getIndexes(resolveContextFromRequest(request)), aiConfig);
538
538
  }
539
539
  };
540
540
  }
@@ -558,6 +558,7 @@ function createDocsMCPAPI(options = {}) {
558
558
  ordering: options.ordering
559
559
  }),
560
560
  mcp: options.mcp ?? readMcpConfig(rootDir),
561
+ search: options.search,
561
562
  defaultName: navTitle
562
563
  });
563
564
  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.1",
3
+ "version": "0.1.3",
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.1"
130
+ "@farming-labs/docs": "0.1.3"
131
131
  },
132
132
  "peerDependencies": {
133
133
  "@farming-labs/docs": ">=0.0.1",