@farming-labs/theme 0.1.13 → 0.1.16

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, DocsSearchConfig, OrderingItem } from "@farming-labs/docs";
1
+ import { ChangelogConfig, DocsI18nConfig, DocsMcpConfig, DocsSearchConfig, OrderingItem } from "@farming-labs/docs";
2
2
 
3
3
  //#region src/docs-api.d.ts
4
4
  interface AIProviderConfig {
@@ -31,6 +31,8 @@ interface DocsAPIOptions {
31
31
  entry?: string;
32
32
  /** Override the docs content directory when it does not live in app/<entry>. */
33
33
  contentDir?: string;
34
+ /** Changelog configuration. */
35
+ changelog?: boolean | ChangelogConfig;
34
36
  /** Search language (default: "english") */
35
37
  language?: string;
36
38
  /** AI chat configuration */
package/dist/docs-api.mjs CHANGED
@@ -3,7 +3,7 @@ import { getNextAppDir } from "./get-app-dir.mjs";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import matter from "gray-matter";
6
- import { performDocsSearch, resolveDocsI18n, resolveDocsLocale, resolveSearchRequestConfig } from "@farming-labs/docs";
6
+ import { performDocsSearch, resolveChangelogConfig, resolveDocsI18n, resolveDocsLocale, resolveSearchRequestConfig } from "@farming-labs/docs";
7
7
  import { createDocsMcpHttpHandler, createFilesystemDocsMcpSource } from "@farming-labs/docs/server";
8
8
 
9
9
  //#region src/docs-api.ts
@@ -278,10 +278,18 @@ function stripMdx(raw) {
278
278
  const { content } = matter(raw);
279
279
  return content.replace(/^(import|export)\s.*$/gm, "").replace(/<[^>]+\/>/g, "").replace(/<\/?[A-Z][^>]*>/g, "").replace(/<\/?[a-z][^>]*>/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/(\*{1,3}|_{1,3})(.*?)\1/g, "$2").replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}\s*$/gm, "").replace(/\n{3,}/g, "\n\n").trim();
280
280
  }
281
- function scanDocsDir(docsDir, entry, locale) {
281
+ function scanDocsDir(docsDir, entry, locale, excludedDirs = []) {
282
282
  const indexes = [];
283
+ function isExcluded(dir) {
284
+ const resolved = path.resolve(dir);
285
+ return excludedDirs.some((excluded) => {
286
+ const relative = path.relative(excluded, resolved);
287
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
288
+ });
289
+ }
283
290
  function scan(dir, slugParts) {
284
291
  if (!fs.existsSync(dir)) return;
292
+ if (isExcluded(dir)) return;
285
293
  const pagePath = path.join(dir, "page.mdx");
286
294
  if (fs.existsSync(pagePath)) try {
287
295
  const raw = fs.readFileSync(pagePath, "utf-8");
@@ -316,6 +324,82 @@ function scanDocsDir(docsDir, entry, locale) {
316
324
  scan(docsDir, []);
317
325
  return indexes;
318
326
  }
327
+ function scanChangelogDir(changelogDir, entryPath, changelogPath, locale) {
328
+ if (!fs.existsSync(changelogDir)) return [];
329
+ const indexes = [];
330
+ let entries;
331
+ try {
332
+ entries = fs.readdirSync(changelogDir).sort().reverse();
333
+ } catch {
334
+ return indexes;
335
+ }
336
+ for (const name of entries) {
337
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(name)) continue;
338
+ const entryDir = path.join(changelogDir, name);
339
+ let isDirectory = false;
340
+ try {
341
+ isDirectory = fs.existsSync(entryDir) && fs.statSync(entryDir).isDirectory();
342
+ } catch {
343
+ continue;
344
+ }
345
+ if (!isDirectory) continue;
346
+ const pagePath = path.join(entryDir, "page.mdx");
347
+ if (!fs.existsSync(pagePath)) continue;
348
+ try {
349
+ const raw = fs.readFileSync(pagePath, "utf-8");
350
+ const { data, content: rawContent } = matter(raw);
351
+ if (data.draft === true) continue;
352
+ const title = data.title || name.replace(/-/g, " ");
353
+ const description = data.description;
354
+ const content = stripMdx(raw);
355
+ const url = withLangInUrl(`/${entryPath}/${changelogPath}/${name}`, locale);
356
+ const tags = Array.isArray(data.tags) ? data.tags.filter((value) => typeof value === "string") : void 0;
357
+ indexes.push({
358
+ title,
359
+ description,
360
+ content,
361
+ rawContent,
362
+ url,
363
+ locale,
364
+ type: "changelog",
365
+ version: typeof data.version === "string" ? data.version : void 0,
366
+ tags
367
+ });
368
+ } catch {}
369
+ }
370
+ return indexes;
371
+ }
372
+ function normalizePathSegment(value) {
373
+ return value.replace(/^\/+|\/+$/g, "");
374
+ }
375
+ function normalizeUrlPath(value) {
376
+ const normalized = value.replace(/\/+/g, "/");
377
+ if (normalized === "/") return normalized;
378
+ return normalized.replace(/\/+$/, "");
379
+ }
380
+ function normalizeRequestedMarkdownPath(entry, requestedPath) {
381
+ const trimmed = requestedPath.trim().replace(/\.md$/i, "");
382
+ if (!trimmed) return `/${entry}`;
383
+ const normalized = normalizeUrlPath(trimmed.startsWith("/") ? trimmed : `/${trimmed}`);
384
+ const normalizedEntry = `/${normalizePathSegment(entry)}`;
385
+ if (normalized === normalizedEntry || normalized.startsWith(`${normalizedEntry}/`)) return normalized;
386
+ const slug = normalizePathSegment(trimmed);
387
+ return slug ? normalizeUrlPath(`${normalizedEntry}/${slug}`) : normalizedEntry;
388
+ }
389
+ function findDocsMcpPage(entry, pages, requestedPath) {
390
+ const normalizedRequest = normalizeRequestedMarkdownPath(entry, requestedPath);
391
+ for (const page of pages) if (normalizeUrlPath(page.url) === normalizedRequest) return page;
392
+ const normalizedSlug = normalizePathSegment(requestedPath.replace(/^\//, "").replace(/\.md$/i, ""));
393
+ for (const page of pages) if (normalizePathSegment(page.slug) === normalizedSlug) return page;
394
+ return null;
395
+ }
396
+ function renderMarkdownDocument(page) {
397
+ if ("agentRawContent" in page && page.agentRawContent !== void 0) return page.agentRawContent;
398
+ const lines = [`# ${page.title}`, `URL: ${page.url}`];
399
+ if (page.description) lines.push(`Description: ${page.description}`);
400
+ lines.push("", page.rawContent ?? page.content);
401
+ return lines.join("\n");
402
+ }
319
403
  const DEFAULT_SYSTEM_PROMPT = `You are a helpful documentation assistant. Answer questions based on the provided documentation context. Be concise and accurate. If the answer is not in the context, say so honestly. Use markdown formatting for code examples and links.`;
320
404
  function resolveModelAndProvider(aiConfig, requestedModelId) {
321
405
  const raw = aiConfig.model;
@@ -463,6 +547,7 @@ function createDocsAPI(options) {
463
547
  const entry = options?.entry ?? readEntry(root);
464
548
  const appDir = getNextAppDir(root);
465
549
  const contentDir = options?.contentDir ?? path.join(appDir, entry);
550
+ const changelogConfig = resolveChangelogConfig(options?.changelog);
466
551
  const i18n = resolveDocsI18n(options?.i18n ?? readI18nConfig(root));
467
552
  const aiConfig = options?.ai ?? readAIConfig(root);
468
553
  const searchConfig = options?.search;
@@ -498,32 +583,78 @@ function createDocsAPI(options) {
498
583
  } catch {}
499
584
  return i18n.defaultLocale;
500
585
  }
586
+ function resolveChangelogDirCandidates(docsDirs) {
587
+ if (!changelogConfig.enabled) return [];
588
+ if (path.isAbsolute(changelogConfig.contentDir)) return [changelogConfig.contentDir];
589
+ return docsDirs.map((docsDir) => path.join(docsDir, changelogConfig.contentDir));
590
+ }
591
+ function isWithinDir(candidate, target) {
592
+ const relative = path.relative(target, candidate);
593
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
594
+ }
501
595
  function resolveContextFromRequest(request) {
502
- if (!i18n) return {
503
- entryPath: entry,
504
- docsDirs: resolveDocsDirCandidates()
505
- };
596
+ if (!i18n) {
597
+ const docsDirs = resolveDocsDirCandidates();
598
+ return {
599
+ entryPath: entry,
600
+ docsDirs,
601
+ changelogDirs: resolveChangelogDirCandidates(docsDirs)
602
+ };
603
+ }
506
604
  const locale = resolveLocaleFromRequest(request) ?? i18n.defaultLocale;
605
+ const docsDirs = resolveDocsDirCandidates(locale);
507
606
  return {
508
607
  entryPath: entry,
509
608
  locale,
510
- docsDirs: resolveDocsDirCandidates(locale)
609
+ docsDirs,
610
+ changelogDirs: resolveChangelogDirCandidates(docsDirs)
511
611
  };
512
612
  }
513
613
  const indexesByLocale = /* @__PURE__ */ new Map();
514
614
  const llmsCacheByLocale = /* @__PURE__ */ new Map();
615
+ const markdownSourcesByLocale = /* @__PURE__ */ new Map();
515
616
  function getIndexes(ctx) {
516
617
  const key = ctx.locale ?? "__default__";
517
618
  const cached = indexesByLocale.get(key);
518
619
  if (cached) return cached;
519
620
  let next = [];
520
621
  for (const docsDir of ctx.docsDirs) {
521
- next = scanDocsDir(docsDir, ctx.entryPath, ctx.locale);
522
- if (next.length > 0) break;
622
+ const excludedDirs = ctx.changelogDirs.filter((dir) => isWithinDir(dir, docsDir));
623
+ const docsPages = scanDocsDir(docsDir, ctx.entryPath, ctx.locale, excludedDirs);
624
+ if (docsPages.length === 0) continue;
625
+ next = docsPages;
626
+ break;
627
+ }
628
+ if (changelogConfig.enabled) {
629
+ const changelogPages = ctx.changelogDirs.flatMap((dir) => scanChangelogDir(dir, ctx.entryPath, changelogConfig.path, ctx.locale));
630
+ next = [...next, ...changelogPages];
523
631
  }
524
632
  indexesByLocale.set(key, next);
525
633
  return next;
526
634
  }
635
+ function getMarkdownSources(ctx) {
636
+ const key = ctx.locale ?? "__default__";
637
+ const cached = markdownSourcesByLocale.get(key);
638
+ if (cached) return cached;
639
+ const sources = ctx.docsDirs.map((docsDir) => createFilesystemDocsMcpSource({
640
+ rootDir: root,
641
+ entry: ctx.entryPath,
642
+ contentDir: docsDir
643
+ }));
644
+ markdownSourcesByLocale.set(key, sources);
645
+ return sources;
646
+ }
647
+ async function getMarkdownDocument(ctx, requestedPath) {
648
+ for (const source of getMarkdownSources(ctx)) {
649
+ const page = findDocsMcpPage(ctx.entryPath, await source.getPages(), requestedPath);
650
+ if (page) return renderMarkdownDocument(page);
651
+ }
652
+ const normalizedRequest = normalizeRequestedMarkdownPath(ctx.entryPath, requestedPath);
653
+ const fallbackPage = getIndexes(ctx).find((page) => normalizeUrlPath(page.url) === normalizedRequest);
654
+ if (fallbackPage) return renderMarkdownDocument(fallbackPage);
655
+ for (const page of getIndexes(ctx)) if (normalizePathSegment(page.url.replace(/^\/+/, "").replace(`${ctx.entryPath}/`, "")) === normalizePathSegment(requestedPath.replace(/^\/+/, "").replace(/\.md$/i, ""))) return renderMarkdownDocument(page);
656
+ return null;
657
+ }
527
658
  function getLlmsContent(ctx) {
528
659
  const key = ctx.locale ?? "__default__";
529
660
  const cached = llmsCacheByLocale.get(key);
@@ -541,6 +672,21 @@ function createDocsAPI(options) {
541
672
  const ctx = resolveContextFromRequest(request);
542
673
  const url = new URL(request.url);
543
674
  const format = url.searchParams.get("format");
675
+ if (format === "markdown") {
676
+ const document = await getMarkdownDocument(ctx, url.searchParams.get("path")?.trim() ?? "");
677
+ if (!document) return new Response("Not Found", {
678
+ status: 404,
679
+ headers: {
680
+ "Content-Type": "text/plain; charset=utf-8",
681
+ "X-Robots-Tag": "noindex"
682
+ }
683
+ });
684
+ return new Response(document, { headers: {
685
+ "Content-Type": "text/markdown; charset=utf-8",
686
+ "Cache-Control": "public, max-age=0, s-maxage=3600",
687
+ "X-Robots-Tag": "noindex"
688
+ } });
689
+ }
544
690
  if (format === "llms") return new Response(getLlmsContent(ctx).llmsTxt, { headers: {
545
691
  "Content-Type": "text/plain; charset=utf-8",
546
692
  "Cache-Control": "public, max-age=3600"
@@ -10,7 +10,7 @@ import path from "node:path";
10
10
  import matter from "gray-matter";
11
11
  import { DocsLayout } from "fumadocs-ui/layouts/docs";
12
12
  import { Suspense } from "react";
13
- import { buildPageOpenGraph, buildPageTwitter } from "@farming-labs/docs";
13
+ import { buildPageOpenGraph, buildPageTwitter, resolveChangelogConfig } from "@farming-labs/docs";
14
14
  import { jsx, jsxs } from "react/jsx-runtime";
15
15
 
16
16
  //#region src/docs-layout.tsx
@@ -33,10 +33,19 @@ function readFrontmatter(filePath) {
33
33
  }
34
34
  }
35
35
  /** Check if a directory has any subdirectories that contain page.mdx. */
36
- function hasChildPages(dir) {
36
+ function isWithinDir(candidate, target) {
37
+ const relative = path.relative(target, candidate);
38
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
39
+ }
40
+ function isExcludedDir(dir, excludedDirs) {
41
+ const resolved = path.resolve(dir);
42
+ return excludedDirs.some((excluded) => isWithinDir(resolved, excluded));
43
+ }
44
+ function hasChildPages(dir, excludedDirs) {
37
45
  if (!fs.existsSync(dir)) return false;
38
46
  for (const name of fs.readdirSync(dir)) {
39
47
  const full = path.join(dir, name);
48
+ if (isExcludedDir(full, excludedDirs)) continue;
40
49
  if (fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, "page.mdx"))) return true;
41
50
  }
42
51
  return false;
@@ -76,11 +85,70 @@ function resolveDocsLocaleContext(config, locale) {
76
85
  docsDir: resolveContentDir(resolvedLocale)
77
86
  };
78
87
  }
88
+ function getExcludedDocsDirs(config, ctx) {
89
+ const changelog = resolveChangelogConfig(config.changelog);
90
+ if (!changelog.enabled) return [];
91
+ const dir = path.isAbsolute(changelog.contentDir) ? changelog.contentDir : path.join(ctx.docsDir, changelog.contentDir);
92
+ return [path.resolve(dir)];
93
+ }
94
+ function readChangelogTreeEntries(config, ctx) {
95
+ const changelog = resolveChangelogConfig(config.changelog);
96
+ if (!changelog.enabled) return [];
97
+ const changelogDir = path.isAbsolute(changelog.contentDir) ? changelog.contentDir : path.join(ctx.docsDir, changelog.contentDir);
98
+ if (!fs.existsSync(changelogDir)) return [];
99
+ const entries = [];
100
+ for (const name of fs.readdirSync(changelogDir)) {
101
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(name)) continue;
102
+ const entryDir = path.join(changelogDir, name);
103
+ if (!fs.existsSync(entryDir) || !fs.statSync(entryDir).isDirectory()) continue;
104
+ const pagePath = path.join(entryDir, "page.mdx");
105
+ if (!fs.existsSync(pagePath)) continue;
106
+ const data = readFrontmatter(pagePath);
107
+ if (data.draft === true) continue;
108
+ entries.push({
109
+ slug: name,
110
+ date: name,
111
+ title: data.title ?? name.replace(/-/g, " "),
112
+ pinned: data.pinned === true
113
+ });
114
+ }
115
+ return entries.sort((left, right) => {
116
+ if (left.pinned !== right.pinned) return left.pinned ? -1 : 1;
117
+ return right.date.localeCompare(left.date);
118
+ });
119
+ }
120
+ function buildChangelogTree(config, ctx, flat = false) {
121
+ const changelog = resolveChangelogConfig(config.changelog);
122
+ if (!changelog.enabled) return null;
123
+ const entries = readChangelogTreeEntries(config, ctx);
124
+ if (entries.length === 0) return null;
125
+ const url = `/${ctx.entryPath}/${changelog.path}`;
126
+ const children = entries.map((entry) => ({
127
+ type: "page",
128
+ name: entry.title,
129
+ url: `${url}/${entry.slug}`
130
+ }));
131
+ return {
132
+ type: "folder",
133
+ name: changelog.title,
134
+ index: {
135
+ type: "page",
136
+ name: changelog.title,
137
+ url
138
+ },
139
+ children,
140
+ ...flat ? {
141
+ collapsible: false,
142
+ defaultOpen: true
143
+ } : {}
144
+ };
145
+ }
79
146
  function buildTree(config, ctx, flat = false) {
80
147
  const docsDir = ctx.docsDir;
81
148
  const icons = config.icons;
82
149
  const ordering = config.ordering;
83
150
  const rootChildren = [];
151
+ const excludedDirs = getExcludedDocsDirs(config, ctx);
84
152
  if (fs.existsSync(path.join(docsDir, "page.mdx"))) {
85
153
  const data = readFrontmatter(path.join(docsDir, "page.mdx"));
86
154
  rootChildren.push({
@@ -92,6 +160,7 @@ function buildTree(config, ctx, flat = false) {
92
160
  }
93
161
  function buildNode(dir, name, baseSlug, slugOrder) {
94
162
  const full = path.join(dir, name);
163
+ if (isExcludedDir(full, excludedDirs)) return null;
95
164
  if (!fs.statSync(full).isDirectory()) return null;
96
165
  const pagePath = path.join(full, "page.mdx");
97
166
  if (!fs.existsSync(pagePath)) return null;
@@ -100,7 +169,7 @@ function buildTree(config, ctx, flat = false) {
100
169
  const url = `/${ctx.entryPath}/${slug.join("/")}`;
101
170
  const icon = resolveIcon(data.icon, icons);
102
171
  const displayName = data.title ?? name.replace(/-/g, " ");
103
- if (hasChildPages(full)) {
172
+ if (hasChildPages(full, excludedDirs)) {
104
173
  const folderChildren = scanDir(full, slug, slugOrder);
105
174
  return {
106
175
  type: "folder",
@@ -135,10 +204,12 @@ function buildTree(config, ctx, flat = false) {
135
204
  for (const item of slugOrder) slugMap.set(item.slug, item);
136
205
  for (const item of slugOrder) {
137
206
  if (!entries.includes(item.slug)) continue;
207
+ if (isExcludedDir(path.join(dir, item.slug), excludedDirs)) continue;
138
208
  const node = buildNode(dir, item.slug, baseSlug, item.children);
139
209
  if (node) nodes.push(node);
140
210
  }
141
211
  for (const name of entries) {
212
+ if (isExcludedDir(path.join(dir, name), excludedDirs)) continue;
142
213
  if (slugMap.has(name)) continue;
143
214
  const node = buildNode(dir, name, baseSlug);
144
215
  if (node) nodes.push(node);
@@ -149,6 +220,7 @@ function buildTree(config, ctx, flat = false) {
149
220
  const nodes = [];
150
221
  for (const name of entries) {
151
222
  const full = path.join(dir, name);
223
+ if (isExcludedDir(full, excludedDirs)) continue;
152
224
  if (!fs.statSync(full).isDirectory()) continue;
153
225
  const pagePath = path.join(full, "page.mdx");
154
226
  if (!fs.existsSync(pagePath)) continue;
@@ -168,6 +240,7 @@ function buildTree(config, ctx, flat = false) {
168
240
  }
169
241
  const nodes = [];
170
242
  for (const name of entries) {
243
+ if (isExcludedDir(path.join(dir, name), excludedDirs)) continue;
171
244
  const node = buildNode(dir, name, baseSlug);
172
245
  if (node) nodes.push(node);
173
246
  }
@@ -175,6 +248,14 @@ function buildTree(config, ctx, flat = false) {
175
248
  }
176
249
  const rootSlugOrder = Array.isArray(ordering) ? ordering : void 0;
177
250
  rootChildren.push(...scanDir(docsDir, [], rootSlugOrder));
251
+ const changelogTree = buildChangelogTree(config, ctx, flat);
252
+ if (changelogTree) {
253
+ if (rootChildren.length > 0) rootChildren.push({
254
+ type: "separator",
255
+ name: "Updates"
256
+ });
257
+ rootChildren.push(changelogTree);
258
+ }
178
259
  return {
179
260
  name: "Docs",
180
261
  children: rootChildren
@@ -186,6 +267,7 @@ function localizeTreeUrls(tree, locale) {
186
267
  ...node,
187
268
  url: withLangInUrl(node.url, locale)
188
269
  };
270
+ if (node.type === "separator") return node;
189
271
  return {
190
272
  ...node,
191
273
  index: node.index ? {
@@ -204,9 +286,10 @@ function localizeTreeUrls(tree, locale) {
204
286
  * Scan all page.mdx files under the docs entry directory and build
205
287
  * a map of URL pathname → formatted last-modified date string.
206
288
  */
207
- function buildLastModifiedMap(ctx) {
289
+ function buildLastModifiedMap(config, ctx) {
208
290
  const docsDir = ctx.docsDir;
209
291
  const map = {};
292
+ const excludedDirs = getExcludedDocsDirs(config, ctx);
210
293
  function formatDate(date) {
211
294
  return date.toLocaleDateString("en-US", {
212
295
  year: "numeric",
@@ -216,6 +299,7 @@ function buildLastModifiedMap(ctx) {
216
299
  }
217
300
  function scan(dir, slugParts) {
218
301
  if (!fs.existsSync(dir)) return;
302
+ if (isExcludedDir(dir, excludedDirs)) return;
219
303
  const pagePath = path.join(dir, "page.mdx");
220
304
  if (fs.existsSync(pagePath)) {
221
305
  const url = slugParts.length === 0 ? `/${ctx.entryPath}` : `/${ctx.entryPath}/${slugParts.join("/")}`;
@@ -233,11 +317,13 @@ function buildLastModifiedMap(ctx) {
233
317
  * Scan all page.mdx files and build a map of URL pathname → description
234
318
  * from the frontmatter `description` field.
235
319
  */
236
- function buildDescriptionMap(ctx) {
320
+ function buildDescriptionMap(config, ctx) {
237
321
  const docsDir = ctx.docsDir;
238
322
  const map = {};
323
+ const excludedDirs = getExcludedDocsDirs(config, ctx);
239
324
  function scan(dir, slugParts) {
240
325
  if (!fs.existsSync(dir)) return;
326
+ if (isExcludedDir(dir, excludedDirs)) return;
241
327
  const pagePath = path.join(dir, "page.mdx");
242
328
  if (fs.existsSync(pagePath)) {
243
329
  const desc = readFrontmatter(pagePath).description;
@@ -446,6 +532,8 @@ function createDocsLayout(config, options) {
446
532
  const i18n = resolveDocsI18nConfig(getDocsI18n(config));
447
533
  const activeLocale = localeContext.locale ?? i18n?.defaultLocale;
448
534
  const docsApiUrl = withLangInUrl("/api/docs", activeLocale);
535
+ const changelogConfig = resolveChangelogConfig(config.changelog);
536
+ const changelogBasePath = changelogConfig.enabled ? `/${localeContext.entryPath}/${changelogConfig.path}` : void 0;
449
537
  const navTitle = config.nav?.title ?? "Docs";
450
538
  const navUrl = withLangInUrl(config.nav?.url ?? `/${localeContext.entryPath}`, activeLocale);
451
539
  const themeSwitch = resolveThemeSwitch(config.themeToggle);
@@ -498,8 +586,8 @@ function createDocsLayout(config, options) {
498
586
  aiModels = rawModelConfig.models ?? aiModels;
499
587
  aiDefaultModelId = rawModelConfig.defaultModel ?? rawModelConfig.models?.[0]?.id ?? aiDefaultModelId;
500
588
  }
501
- const lastModifiedMap = buildLastModifiedMap(localeContext);
502
- const descriptionMap = buildDescriptionMap(localeContext);
589
+ const lastModifiedMap = buildLastModifiedMap(config, localeContext);
590
+ const descriptionMap = buildDescriptionMap(config, localeContext);
503
591
  return function DocsLayoutWrapper({ children }) {
504
592
  const tree = buildTree(config, localeContext, !!sidebarFlat);
505
593
  const localizedTree = i18n ? localizeTreeUrls(tree, activeLocale) : tree;
@@ -577,6 +665,7 @@ function createDocsLayout(config, options) {
577
665
  tocEnabled,
578
666
  tocStyle,
579
667
  breadcrumbEnabled,
668
+ changelogBasePath,
580
669
  entry: localeContext.entryPath,
581
670
  locale: activeLocale,
582
671
  copyMarkdown: copyMarkdownEnabled,
@@ -13,6 +13,7 @@ interface DocsPageClientProps {
13
13
  tocEnabled: boolean;
14
14
  tocStyle?: "default" | "directional";
15
15
  breadcrumbEnabled?: boolean;
16
+ changelogBasePath?: string;
16
17
  /** The docs entry folder name (e.g. "docs") — used to strip from breadcrumb */
17
18
  entry?: string;
18
19
  /** Active locale (used for llms.txt links) */
@@ -62,6 +63,7 @@ declare function DocsPageClient({
62
63
  tocEnabled,
63
64
  tocStyle,
64
65
  breadcrumbEnabled,
66
+ changelogBasePath,
65
67
  entry,
66
68
  locale,
67
69
  copyMarkdown,
@@ -158,16 +158,21 @@ function TitleDecorations({ description, belowTitle }) {
158
158
  if (!description && !belowTitle) return null;
159
159
  return /* @__PURE__ */ jsxs(Fragment, { children: [description, belowTitle] });
160
160
  }
161
- function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, entry = "docs", locale, copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackOnFeedback, children }) {
161
+ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, changelogBasePath, entry = "docs", locale, copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackOnFeedback, children }) {
162
162
  const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
163
163
  const [toc, setToc] = useState([]);
164
164
  const [titlePortalHost, setTitlePortalHost] = useState(null);
165
+ const [browserPath, setBrowserPath] = useState(null);
165
166
  const pathname = usePathname();
166
167
  const activeLocale = resolveClientLocale(useWindowSearchParams(), locale);
167
168
  const llmsLangParam = activeLocale ? `&lang=${encodeURIComponent(activeLocale)}` : "";
168
169
  const pageDescription = description ?? descriptionMap?.[pathname.replace(/\/$/, "") || "/"];
170
+ const normalizedPath = (browserPath ?? pathname).replace(/\/$/, "") || "/";
171
+ const isChangelogRoute = !!(changelogBasePath && (normalizedPath === changelogBasePath || normalizedPath.startsWith(`${changelogBasePath}/`)));
172
+ const effectiveTocEnabled = isChangelogRoute ? false : tocEnabled;
173
+ const effectiveBreadcrumbEnabled = isChangelogRoute ? false : breadcrumbEnabled;
169
174
  useEffect(() => {
170
- if (!tocEnabled) return;
175
+ if (!effectiveTocEnabled) return;
171
176
  const timer = requestAnimationFrame(() => {
172
177
  const container = document.getElementById("nd-page");
173
178
  if (!container) return;
@@ -179,7 +184,7 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
179
184
  })));
180
185
  });
181
186
  return () => cancelAnimationFrame(timer);
182
- }, [tocEnabled, pathname]);
187
+ }, [effectiveTocEnabled, pathname]);
183
188
  useEffect(() => {
184
189
  if (!activeLocale) return;
185
190
  const timer = requestAnimationFrame(() => {
@@ -193,6 +198,19 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
193
198
  children,
194
199
  pathname
195
200
  ]);
201
+ useEffect(() => {
202
+ setBrowserPath(window.location.pathname);
203
+ }, [pathname]);
204
+ useEffect(() => {
205
+ const root = document.documentElement;
206
+ if (isChangelogRoute) {
207
+ root.dataset.fdRouteKind = "changelog";
208
+ return () => {
209
+ if (root.dataset.fdRouteKind === "changelog") delete root.dataset.fdRouteKind;
210
+ };
211
+ }
212
+ if (root.dataset.fdRouteKind === "changelog") delete root.dataset.fdRouteKind;
213
+ }, [isChangelogRoute]);
196
214
  useEffect(() => {
197
215
  let frame = 0;
198
216
  let timeout = 0;
@@ -220,15 +238,14 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
220
238
  clearTimeout(timeout);
221
239
  };
222
240
  }, [pathname, children]);
223
- const showActions = copyMarkdown || openDocs;
241
+ const showActions = !isChangelogRoute && (copyMarkdown || openDocs);
224
242
  const showActionsBelowTitle = showActions && pageActionsPosition === "below-title";
225
243
  const showActionsAboveTitle = showActions && pageActionsPosition === "above-title";
226
244
  const githubFileUrl = editOnGithubUrl ?? (githubUrl ? buildGithubFileUrl(githubUrl, githubBranch, pathname, entry, activeLocale, githubDirectory, contentDir) : void 0);
227
- const normalizedPath = pathname.replace(/\/$/, "") || "/";
228
- const lastModified = lastUpdatedEnabled ? lastModifiedProp ?? lastModifiedMap?.[normalizedPath] : void 0;
245
+ const lastModified = !isChangelogRoute && lastUpdatedEnabled ? lastModifiedProp ?? lastModifiedMap?.[normalizedPath] : void 0;
229
246
  const showLastUpdatedBelowTitle = !!lastModified && lastUpdatedPosition === "below-title";
230
247
  const showLastUpdatedInFooter = !!lastModified && lastUpdatedPosition === "footer";
231
- const showFooter = !!githubFileUrl || showLastUpdatedInFooter || llmsTxtEnabled;
248
+ const showFooter = !isChangelogRoute && (!!githubFileUrl || showLastUpdatedInFooter || llmsTxtEnabled);
232
249
  const titleDescription = pageDescription ? /* @__PURE__ */ jsx("p", {
233
250
  className: "fd-page-description",
234
251
  children: pageDescription
@@ -283,18 +300,20 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
283
300
  belowTitle: belowTitleBlock
284
301
  }), titlePortalHost) : null;
285
302
  return /* @__PURE__ */ jsxs(DocsPage, {
303
+ full: false,
286
304
  toc,
287
305
  tableOfContent: {
288
- enabled: tocEnabled,
306
+ enabled: effectiveTocEnabled,
289
307
  style: fdTocStyle
290
308
  },
291
309
  tableOfContentPopover: {
292
- enabled: tocEnabled,
310
+ enabled: effectiveTocEnabled,
293
311
  style: fdTocStyle
294
312
  },
295
313
  breadcrumb: { enabled: false },
314
+ footer: { enabled: !isChangelogRoute },
296
315
  children: [
297
- breadcrumbEnabled && /* @__PURE__ */ jsx(PathBreadcrumb, {
316
+ effectiveBreadcrumbEnabled && /* @__PURE__ */ jsx(PathBreadcrumb, {
298
317
  pathname,
299
318
  entry,
300
319
  locale: activeLocale
@@ -324,7 +343,7 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
324
343
  children: decoratedChildren
325
344
  }),
326
345
  titleDecorationsPortal,
327
- feedbackEnabled && /* @__PURE__ */ jsx(DocsFeedback, {
346
+ !isChangelogRoute && feedbackEnabled && /* @__PURE__ */ jsx(DocsFeedback, {
328
347
  pathname: normalizedPath,
329
348
  entry,
330
349
  locale: activeLocale,
package/dist/index.d.mts CHANGED
@@ -12,8 +12,8 @@ import { PageActions } from "./page-actions.mjs";
12
12
  import { withLangInUrl } from "./i18n.mjs";
13
13
  import { HoverLink, HoverLinkProps } from "./hover-link.mjs";
14
14
  import { DocsLayout } from "fumadocs-ui/layouts/docs";
15
- import { AIConfig, BreadcrumbConfig, CopyMarkdownConfig, DocsConfig, DocsFeedbackData, DocsFeedbackValue, DocsMetadata, DocsNav, DocsTheme, FeedbackConfig, FontStyle, OGConfig, OpenDocsConfig, OpenDocsProvider, PageActionsConfig, PageFrontmatter, SidebarConfig, ThemeToggleConfig, TypographyConfig, UIConfig, createTheme, deepMerge, defineDocs, extendTheme } from "@farming-labs/docs";
15
+ import { AIConfig, BreadcrumbConfig, ChangelogConfig, ChangelogFrontmatter, CopyMarkdownConfig, DocsConfig, DocsFeedbackData, DocsFeedbackValue, DocsMetadata, DocsNav, DocsTheme, FeedbackConfig, FontStyle, OGConfig, OpenDocsConfig, OpenDocsProvider, PageActionsConfig, PageFrontmatter, SidebarConfig, ThemeToggleConfig, TypographyConfig, UIConfig, createTheme, deepMerge, defineDocs, extendTheme } from "@farming-labs/docs";
16
16
  import { DocsBody, DocsPage } from "fumadocs-ui/layouts/docs/page";
17
17
  import { Tab, Tabs } from "fumadocs-ui/components/tabs";
18
18
  import { CodeBlock, CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger, Pre } from "fumadocs-ui/components/codeblock";
19
- export { type AIConfig, type BreadcrumbConfig, CodeBlock, CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger, CommandGridUIDefaults, ConcreteUIDefaults, type CopyMarkdownConfig, DocsBody, DocsClientHooks, DocsCommandSearch, type DocsConfig, DocsFeedback, type DocsFeedbackData, type DocsFeedbackProps, type DocsFeedbackValue, DocsLayout, type DocsMetadata, type DocsNav, DocsPage, DocsPageClient, type DocsTheme, type FeedbackConfig, type FontStyle, DefaultUIDefaults as FumadocsUIDefaults, HardlineUIDefaults, HoverLink, type HoverLinkProps, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, PageActions, type PageActionsConfig, type PageFrontmatter, Pre, RootProvider, type SidebarConfig, Tab, Tabs, type ThemeToggleConfig, type TypographyConfig, type UIConfig, commandGrid, concrete, createDocsLayout, createDocsMetadata, createPageMetadata, createTheme, deepMerge, defineDocs, extendTheme, fumadocs, hardline, withLangInUrl };
19
+ export { type AIConfig, type BreadcrumbConfig, type ChangelogConfig, type ChangelogFrontmatter, CodeBlock, CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger, CommandGridUIDefaults, ConcreteUIDefaults, type CopyMarkdownConfig, DocsBody, DocsClientHooks, DocsCommandSearch, type DocsConfig, DocsFeedback, type DocsFeedbackData, type DocsFeedbackProps, type DocsFeedbackValue, DocsLayout, type DocsMetadata, type DocsNav, DocsPage, DocsPageClient, type DocsTheme, type FeedbackConfig, type FontStyle, DefaultUIDefaults as FumadocsUIDefaults, HardlineUIDefaults, HoverLink, type HoverLinkProps, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, PageActions, type PageActionsConfig, type PageFrontmatter, Pre, RootProvider, type SidebarConfig, Tab, Tabs, type ThemeToggleConfig, type TypographyConfig, type UIConfig, commandGrid, concrete, createDocsLayout, createDocsMetadata, createPageMetadata, createTheme, deepMerge, defineDocs, extendTheme, fumadocs, hardline, withLangInUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.1.13",
3
+ "version": "0.1.16",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -133,7 +133,7 @@
133
133
  "tsdown": "^0.20.3",
134
134
  "typescript": "^5.9.3",
135
135
  "vitest": "^3.2.4",
136
- "@farming-labs/docs": "0.1.13"
136
+ "@farming-labs/docs": "0.1.16"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "@farming-labs/docs": ">=0.0.1",