@farming-labs/theme 0.0.2-beta.22 → 0.0.2-beta.23

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.
@@ -38,12 +38,14 @@ interface DocsAPIOptions {
38
38
  * Create a unified docs API route handler.
39
39
  *
40
40
  * Returns `{ GET, POST }` for use in a Next.js route handler:
41
- * - **GET** → full-text search (same as the old `createDocsSearchAPI`)
42
- * - **POST** AI-powered chat with RAG (when AI is enabled in config)
41
+ * - **GET ?query=…** → full-text search
42
+ * - **GET ?format=llms** llms.txt (concise page listing)
43
+ * - **GET ?format=llms-full** → llms-full.txt (full page content)
44
+ * - **POST** → AI-powered chat with RAG
43
45
  *
44
46
  * @example
45
47
  * ```ts
46
- * // app/api/docs/route.ts
48
+ * // app/api/docs/route.ts (auto-generated by withDocs)
47
49
  * import { createDocsAPI } from "@farming-labs/theme/api";
48
50
  * export const { GET, POST } = createDocsAPI();
49
51
  * export const revalidate = false;
@@ -53,10 +55,9 @@ interface DocsAPIOptions {
53
55
  */
54
56
  declare function createDocsAPI(options?: DocsAPIOptions): {
55
57
  /**
56
- * GET handler — full-text search.
57
- * Query: `?query=search+term`
58
+ * GET handler — search, llms.txt, or llms-full.txt depending on query params.
58
59
  */
59
- GET: (request: Request) => Promise<Response>;
60
+ GET(request: Request): Response | Promise<Response>;
60
61
  /**
61
62
  * POST handler — AI chat with RAG.
62
63
  * Body: `{ messages: [{ role: "user", content: "How do I …?" }] }`
package/dist/docs-api.mjs CHANGED
@@ -163,16 +163,64 @@ async function handleAskAI(request, indexes, searchServer, aiConfig) {
163
163
  Connection: "keep-alive"
164
164
  } });
165
165
  }
166
+ function readLlmsTxtConfig(root) {
167
+ for (const ext of FILE_EXTS) {
168
+ const configPath = path.join(root, `docs.config.${ext}`);
169
+ if (fs.existsSync(configPath)) try {
170
+ const content = fs.readFileSync(configPath, "utf-8");
171
+ if (!content.includes("llmsTxt")) return { enabled: false };
172
+ if (/llmsTxt\s*:\s*true/.test(content)) return { enabled: true };
173
+ const enabledMatch = content.match(/llmsTxt\s*:\s*\{[^}]*enabled\s*:\s*(true|false)/s);
174
+ if (enabledMatch && enabledMatch[1] === "false") return { enabled: false };
175
+ const baseUrlMatch = content.match(/llmsTxt\s*:\s*\{[^}]*baseUrl\s*:\s*["']([^"']+)["']/s);
176
+ const siteTitleMatch = content.match(/llmsTxt\s*:\s*\{[^}]*siteTitle\s*:\s*["']([^"']+)["']/s);
177
+ const siteDescMatch = content.match(/llmsTxt\s*:\s*\{[^}]*siteDescription\s*:\s*["']([^"']+)["']/s);
178
+ const navTitleMatch = content.match(/nav\s*:\s*\{[^}]*title\s*:\s*["']([^"']+)["']/s);
179
+ return {
180
+ enabled: true,
181
+ baseUrl: baseUrlMatch?.[1],
182
+ siteTitle: siteTitleMatch?.[1] ?? navTitleMatch?.[1],
183
+ siteDescription: siteDescMatch?.[1]
184
+ };
185
+ } catch {}
186
+ }
187
+ return { enabled: false };
188
+ }
189
+ function generateLlmsTxt(indexes, options) {
190
+ const { siteTitle = "Documentation", siteDescription, baseUrl = "" } = options;
191
+ let llmsTxt = `# ${siteTitle}\n\n`;
192
+ if (siteDescription) llmsTxt += `> ${siteDescription}\n\n`;
193
+ llmsTxt += `## Pages\n\n`;
194
+ for (const page of indexes) {
195
+ llmsTxt += `- [${page.title}](${baseUrl}${page.url})`;
196
+ if (page.description) llmsTxt += `: ${page.description}`;
197
+ llmsTxt += `\n`;
198
+ }
199
+ let llmsFullTxt = `# ${siteTitle}\n\n`;
200
+ if (siteDescription) llmsFullTxt += `> ${siteDescription}\n\n`;
201
+ for (const page of indexes) {
202
+ llmsFullTxt += `## ${page.title}\n\n`;
203
+ llmsFullTxt += `URL: ${baseUrl}${page.url}\n\n`;
204
+ if (page.description) llmsFullTxt += `${page.description}\n\n`;
205
+ llmsFullTxt += `${page.content}\n\n---\n\n`;
206
+ }
207
+ return {
208
+ llmsTxt,
209
+ llmsFullTxt
210
+ };
211
+ }
166
212
  /**
167
213
  * Create a unified docs API route handler.
168
214
  *
169
215
  * Returns `{ GET, POST }` for use in a Next.js route handler:
170
- * - **GET** → full-text search (same as the old `createDocsSearchAPI`)
171
- * - **POST** AI-powered chat with RAG (when AI is enabled in config)
216
+ * - **GET ?query=…** → full-text search
217
+ * - **GET ?format=llms** llms.txt (concise page listing)
218
+ * - **GET ?format=llms-full** → llms-full.txt (full page content)
219
+ * - **POST** → AI-powered chat with RAG
172
220
  *
173
221
  * @example
174
222
  * ```ts
175
- * // app/api/docs/route.ts
223
+ * // app/api/docs/route.ts (auto-generated by withDocs)
176
224
  * import { createDocsAPI } from "@farming-labs/theme/api";
177
225
  * export const { GET, POST } = createDocsAPI();
178
226
  * export const revalidate = false;
@@ -186,13 +234,34 @@ function createDocsAPI(options) {
186
234
  const docsDir = path.join(root, "app", entry);
187
235
  const language = options?.language ?? "english";
188
236
  const aiConfig = options?.ai ?? readAIConfig(root);
237
+ const llmsConfig = readLlmsTxtConfig(root);
189
238
  const indexes = scanDocsDir(docsDir, entry);
239
+ let _llmsCache = null;
240
+ function getLlmsContent() {
241
+ if (!_llmsCache) _llmsCache = generateLlmsTxt(indexes, {
242
+ siteTitle: llmsConfig.siteTitle ?? "Documentation",
243
+ siteDescription: llmsConfig.siteDescription,
244
+ baseUrl: llmsConfig.baseUrl ?? ""
245
+ });
246
+ return _llmsCache;
247
+ }
190
248
  const searchAPI = createSearchAPI("simple", {
191
249
  language,
192
250
  indexes
193
251
  });
194
252
  return {
195
- GET: searchAPI.GET,
253
+ GET(request) {
254
+ const format = new URL(request.url).searchParams.get("format");
255
+ if (format === "llms") return new Response(getLlmsContent().llmsTxt, { headers: {
256
+ "Content-Type": "text/plain; charset=utf-8",
257
+ "Cache-Control": "public, max-age=3600"
258
+ } });
259
+ if (format === "llms-full") return new Response(getLlmsContent().llmsFullTxt, { headers: {
260
+ "Content-Type": "text/plain; charset=utf-8",
261
+ "Cache-Control": "public, max-age=3600"
262
+ } });
263
+ return searchAPI.GET(request);
264
+ },
196
265
  async POST(request) {
197
266
  if (!aiConfig.enabled) return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
198
267
  return handleAskAI(request, indexes, searchAPI, aiConfig);
@@ -357,6 +357,7 @@ function createDocsLayout(config) {
357
357
  const lastUpdatedRaw = config.lastUpdated;
358
358
  const lastUpdatedEnabled = lastUpdatedRaw !== false && (typeof lastUpdatedRaw !== "object" || lastUpdatedRaw.enabled !== false);
359
359
  const lastUpdatedPosition = typeof lastUpdatedRaw === "object" ? lastUpdatedRaw.position ?? "footer" : "footer";
360
+ const llmsTxtEnabled = resolveBool(config.llmsTxt);
360
361
  const openDocsProviders = (typeof pageActions?.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((p) => ({
361
362
  name: p.name,
362
363
  urlTemplate: p.urlTemplate,
@@ -418,6 +419,7 @@ function createDocsLayout(config) {
418
419
  lastModifiedMap,
419
420
  lastUpdatedEnabled,
420
421
  lastUpdatedPosition,
422
+ llmsTxtEnabled,
421
423
  descriptionMap,
422
424
  children
423
425
  })
@@ -33,6 +33,8 @@ interface DocsPageClientProps {
33
33
  lastUpdatedEnabled?: boolean;
34
34
  /** Where to show the "Last updated" date: "footer" (next to Edit on GitHub) or "below-title" */
35
35
  lastUpdatedPosition?: "footer" | "below-title";
36
+ /** Whether llms.txt is enabled — shows links in footer */
37
+ llmsTxtEnabled?: boolean;
36
38
  /** Map of pathname → frontmatter description */
37
39
  descriptionMap?: Record<string, string>;
38
40
  /** Frontmatter description to display below the page title (overrides descriptionMap) */
@@ -55,6 +57,7 @@ declare function DocsPageClient({
55
57
  lastModifiedMap,
56
58
  lastUpdatedEnabled,
57
59
  lastUpdatedPosition,
60
+ llmsTxtEnabled,
58
61
  descriptionMap,
59
62
  description,
60
63
  children
@@ -63,7 +63,7 @@ function buildGithubFileUrl(githubUrl, branch, pathname, directory) {
63
63
  const segments = pathname.replace(/^\//, "").replace(/\/$/, "");
64
64
  return `${githubUrl}/tree/${branch}/${directory ? `${directory}/` : ""}app/${segments}/page.mdx`;
65
65
  }
66
- function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, entry = "docs", copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, githubBranch = "main", githubDirectory, lastModifiedMap, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", descriptionMap, description, children }) {
66
+ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, entry = "docs", copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, githubBranch = "main", githubDirectory, lastModifiedMap, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, children }) {
67
67
  const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
68
68
  const [toc, setToc] = useState([]);
69
69
  const pathname = usePathname();
@@ -109,7 +109,7 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
109
109
  const lastModified = lastUpdatedEnabled ? lastModifiedMap?.[normalizedPath] : void 0;
110
110
  const showLastUpdatedBelowTitle = !!lastModified && lastUpdatedPosition === "below-title";
111
111
  const showLastUpdatedInFooter = !!lastModified && lastUpdatedPosition === "footer";
112
- const showFooter = !!githubFileUrl || showLastUpdatedInFooter;
112
+ const showFooter = !!githubFileUrl || showLastUpdatedInFooter || llmsTxtEnabled;
113
113
  const needsBelowTitleBlock = showLastUpdatedBelowTitle || showActions;
114
114
  useEffect(() => {
115
115
  if (!needsBelowTitleBlock) return;
@@ -188,10 +188,29 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
188
188
  children
189
189
  }), showFooter && /* @__PURE__ */ jsxs("div", {
190
190
  className: "not-prose fd-page-footer",
191
- children: [githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }), showLastUpdatedInFooter && lastModified && /* @__PURE__ */ jsxs("span", {
192
- className: "fd-last-updated-footer",
193
- children: ["Last updated ", lastModified]
194
- })]
191
+ children: [
192
+ githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }),
193
+ llmsTxtEnabled && /* @__PURE__ */ jsxs("span", {
194
+ className: "fd-llms-txt-links",
195
+ children: [/* @__PURE__ */ jsx("a", {
196
+ href: "/api/docs?format=llms",
197
+ target: "_blank",
198
+ rel: "noopener noreferrer",
199
+ className: "fd-llms-txt-link",
200
+ children: "llms.txt"
201
+ }), /* @__PURE__ */ jsx("a", {
202
+ href: "/api/docs?format=llms-full",
203
+ target: "_blank",
204
+ rel: "noopener noreferrer",
205
+ className: "fd-llms-txt-link",
206
+ children: "llms-full.txt"
207
+ })]
208
+ }),
209
+ showLastUpdatedInFooter && lastModified && /* @__PURE__ */ jsxs("span", {
210
+ className: "fd-last-updated-footer",
211
+ children: ["Last updated ", lastModified]
212
+ })
213
+ ]
195
214
  })]
196
215
  })
197
216
  ]
package/dist/search.d.mts CHANGED
@@ -27,7 +27,7 @@ declare function createDocsSearchAPI(options?: {
27
27
  entry?: string;
28
28
  language?: string;
29
29
  }): {
30
- GET: (request: Request) => Promise<Response>;
30
+ GET(request: Request): Response | Promise<Response>;
31
31
  POST(request: Request): Promise<Response>;
32
32
  };
33
33
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.0.2-beta.22",
3
+ "version": "0.0.2-beta.23",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -98,7 +98,7 @@
98
98
  "next": ">=14.0.0",
99
99
  "tsdown": "^0.20.3",
100
100
  "typescript": "^5.9.3",
101
- "@farming-labs/docs": "0.0.2-beta.22"
101
+ "@farming-labs/docs": "0.0.2-beta.23"
102
102
  },
103
103
  "peerDependencies": {
104
104
  "@farming-labs/docs": ">=0.0.1",
package/styles/base.css CHANGED
@@ -409,6 +409,29 @@ figure.shiki:has(figcaption) figcaption {
409
409
  margin-left: auto;
410
410
  }
411
411
 
412
+ .fd-llms-txt-links {
413
+ display: inline-flex;
414
+ align-items: center;
415
+ gap: 0.5rem;
416
+ }
417
+
418
+ .fd-llms-txt-link {
419
+ color: var(--color-fd-muted-foreground, hsl(0 0% 45%));
420
+ font-size: 0.75rem;
421
+ font-family: var(--fd-font-mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace);
422
+ text-decoration: none;
423
+ padding: 0.125rem 0.375rem;
424
+ border-radius: 0.25rem;
425
+ border: 1px solid var(--color-fd-border, hsl(0 0% 80% / 50%));
426
+ transition: color 150ms, border-color 150ms;
427
+ }
428
+
429
+ .fd-llms-txt-link:hover {
430
+ /* color: var(--color-fd-foreground, hsl(0 0% 10%));
431
+ border-color: var(--color-fd-foreground, hsl(0 0% 10%)); */
432
+ text-decoration: none;
433
+ }
434
+
412
435
  /* ─── Code block copy button: show on hover ────────────────────────── */
413
436
 
414
437
  figure.shiki > button,
@@ -425,6 +425,29 @@ figure.shiki > div:first-child {
425
425
  font-family: var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace));
426
426
  text-transform: uppercase;
427
427
  }
428
+ /* llms.txt links */
429
+ .fd-llms-txt-links {
430
+ display: inline-flex;
431
+ align-items: center;
432
+ gap: 0.5rem;
433
+ }
434
+
435
+ .fd-llms-txt-link {
436
+ color: var(--color-fd-muted-foreground, hsl(0 0% 45%));
437
+ font-size: 0.65rem !important;
438
+ font-family: var(--fd-font-mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace);
439
+ text-decoration: none;
440
+ padding: 0.015rem 0.4rem !important;
441
+ border-radius: 0px !important;
442
+ border: 0.5px solid var(--color-fd-border, hsl(0 0% 80% / 50%));
443
+ transition: color 150ms, border-color 150ms;
444
+ }
445
+
446
+ .fd-llms-txt-link:hover {
447
+ color: var(--color-fd-foreground, hsl(0 0% 10%)) !important;
448
+ border: 0.5px solid var(--color-fd-muted-foreground, hsl(0 0% 10% / 10%)) !important;
449
+ text-decoration: none;
450
+ }
428
451
 
429
452
  /* ─── Page Actions (pixel-border overrides) ───────────────────────── */
430
453