@farming-labs/theme 0.1.108 → 0.1.109

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.
@@ -34,6 +34,8 @@ interface DocsAPIOptions {
34
34
  rootDir?: string;
35
35
  /** Docs entry folder (default: read from docs.config) */
36
36
  entry?: string;
37
+ /** Public docs route prefix. Defaults to the docs entry path. */
38
+ docsPath?: string;
37
39
  /** Override the docs content directory when it does not live in app/<entry>. */
38
40
  contentDir?: string;
39
41
  /** Changelog 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 { buildDocsAskAIContext, createDocsAgentTraceContext, createDocsAgentTraceId, createDocsRobotsResponse, createDocsSitemapResponse, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, formatDocsAskAIPackageHints, getDocsLlmsTxtMaxCharsIssue, getDocsMarkdownCanonicalLinkHeader, getDocsMarkdownVaryHeader, hasDocsMarkdownSignatureAgent, normalizeDocsRelated, performDocsSearch, renderDocsLlmsTxt, renderDocsMarkdownNotFound, renderDocsRelatedMarkdownLines, resolveAskAISearchRequestConfig, resolveChangelogConfig, resolveDocsI18n, resolveDocsLlmsTxtRequest, resolveDocsLlmsTxtSections, resolveDocsLocale, resolveDocsSitemapConfig, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig, selectDocsLlmsTxtContent } from "@farming-labs/docs";
6
+ import { buildDocsAskAIContext, createDocsAgentTraceContext, createDocsAgentTraceId, createDocsRobotsResponse, createDocsSitemapResponse, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, formatDocsAskAIPackageHints, getDocsLlmsTxtMaxCharsIssue, getDocsMarkdownVaryHeader, hasDocsMarkdownSignatureAgent, normalizeDocsRelated, performDocsSearch, renderDocsLlmsTxt, renderDocsMarkdownNotFound, renderDocsRelatedMarkdownLines, resolveAskAISearchRequestConfig, resolveChangelogConfig, resolveDocsI18n, resolveDocsLlmsTxtRequest, resolveDocsLlmsTxtSections, resolveDocsLocale, resolveDocsSitemapConfig, resolvePageSidebarFolderIndexBehavior, resolveSearchRequestConfig, selectDocsLlmsTxtContent } from "@farming-labs/docs";
7
7
  import { buildApiReferenceOpenApiDocumentAsync, createDocsMcpHttpHandler, createFilesystemDocsMcpSource, readDocsSitemapManifest, resolveApiReferenceConfig, resolveDocsMcpConfig } from "@farming-labs/docs/server";
8
8
 
9
9
  //#region src/docs-api.ts
@@ -807,7 +807,7 @@ function isHiddenFolderIndexPageDir(dir) {
807
807
  return false;
808
808
  }
809
809
  }
810
- function scanDocsDir(docsDir, entry, locale, excludedDirs = []) {
810
+ function scanDocsDir(docsDir, entry, locale, excludedDirs = [], publicPath = `/${normalizePathSegment(entry) || "docs"}`) {
811
811
  const indexes = [];
812
812
  function isExcluded(dir) {
813
813
  const resolved = path.resolve(dir);
@@ -830,7 +830,7 @@ function scanDocsDir(docsDir, entry, locale, excludedDirs = []) {
830
830
  const rawContent = resolveAgentMdxContent(fileContent, "human");
831
831
  const agentRawContent = resolveAgentMdxContent(fileContent, "agent");
832
832
  const content = stripMdx(rawContent);
833
- const url = withLangInUrl(slugParts.length === 0 ? `/${entry}` : `/${entry}/${slugParts.join("/")}`, locale);
833
+ const url = withLangInUrl(publicDocsRoute(publicPath, slugParts), locale);
834
834
  indexes.push({
835
835
  title,
836
836
  description,
@@ -861,7 +861,7 @@ function scanDocsDir(docsDir, entry, locale, excludedDirs = []) {
861
861
  scan(docsDir, []);
862
862
  return resolveRelatedForSearchPages(indexes);
863
863
  }
864
- function scanChangelogDir(changelogDir, entryPath, changelogPath, locale) {
864
+ function scanChangelogDir(changelogDir, entryPath, changelogPath, locale, publicPath = `/${normalizePathSegment(entryPath) || "docs"}`) {
865
865
  if (!fs.existsSync(changelogDir)) return [];
866
866
  const indexes = [];
867
867
  let entries;
@@ -890,7 +890,7 @@ function scanChangelogDir(changelogDir, entryPath, changelogPath, locale) {
890
890
  const rawContent = resolveAgentMdxContent(fileContent, "human");
891
891
  const agentRawContent = resolveAgentMdxContent(fileContent, "agent");
892
892
  const content = stripMdx(rawContent);
893
- const url = withLangInUrl(`/${entryPath}/${changelogPath}/${name}`, locale);
893
+ const url = withLangInUrl(publicDocsRoute(publicPath, [changelogPath, name]), locale);
894
894
  const tags = Array.isArray(data.tags) ? data.tags.filter((value) => typeof value === "string") : void 0;
895
895
  indexes.push({
896
896
  title,
@@ -935,6 +935,38 @@ function normalizeRequestedMarkdownPath(entry, requestedPath) {
935
935
  const slug = normalizePathSegment(trimmed);
936
936
  return slug ? normalizeUrlPath(`${normalizedEntry}/${slug}`) : normalizedEntry;
937
937
  }
938
+ function normalizePublicRequestedMarkdownPath(ctx, requestedPath) {
939
+ const trimmed = requestedPath.trim().replace(/\.md$/i, "");
940
+ if (!trimmed) return ctx.publicPath || "/";
941
+ const normalized = normalizeUrlPath(trimmed.startsWith("/") ? trimmed : `/${trimmed}`);
942
+ const normalizedEntry = `/${normalizePathSegment(ctx.entryPath)}`;
943
+ if (normalized === normalizedEntry) return ctx.publicPath || "/";
944
+ if (normalized.startsWith(`${normalizedEntry}/`)) {
945
+ const suffix = normalized.slice(normalizedEntry.length + 1);
946
+ return publicDocsRoute(ctx.publicPath, suffix.split("/").filter(Boolean));
947
+ }
948
+ if (ctx.publicPath) {
949
+ if (normalized === ctx.publicPath || normalized.startsWith(`${ctx.publicPath}/`)) return normalized;
950
+ } else if (normalized === "/") return "/";
951
+ const slug = normalizePathSegment(trimmed);
952
+ return publicDocsRoute(ctx.publicPath, slug ? slug.split("/").filter(Boolean) : []);
953
+ }
954
+ function normalizePublicDocsSlug(ctx, value) {
955
+ const pathname = normalizeUrlPath(value);
956
+ const normalizedEntry = `/${normalizePathSegment(ctx.entryPath)}`;
957
+ if (ctx.publicPath) {
958
+ if (pathname === ctx.publicPath) return "";
959
+ if (pathname.startsWith(`${ctx.publicPath}/`)) return normalizePathSegment(pathname.slice(ctx.publicPath.length + 1));
960
+ } else if (pathname === "/") return "";
961
+ if (pathname === normalizedEntry) return "";
962
+ if (pathname.startsWith(`${normalizedEntry}/`)) return normalizePathSegment(pathname.slice(normalizedEntry.length + 1));
963
+ return normalizePathSegment(pathname);
964
+ }
965
+ function getPublicMarkdownCanonicalLinkHeader({ origin, ctx, requestedPath }) {
966
+ const canonicalUrl = new URL(normalizePublicRequestedMarkdownPath(ctx, requestedPath), origin);
967
+ if (ctx.locale) canonicalUrl.searchParams.set("lang", ctx.locale);
968
+ return `<${canonicalUrl.toString()}>; rel="canonical"`;
969
+ }
938
970
  function findDocsMcpPage(entry, pages, requestedPath) {
939
971
  const normalizedRequest = normalizeRequestedMarkdownPath(entry, requestedPath);
940
972
  for (const page of pages) if (normalizeUrlPath(page.url) === normalizedRequest) return page;
@@ -985,6 +1017,95 @@ function resolveMarkdownRequest(entry, url, request) {
985
1017
  }
986
1018
  return null;
987
1019
  }
1020
+ function normalizeDocsPublicPath(value, entry) {
1021
+ if (typeof value !== "string") return `/${normalizePathSegment(entry)}`;
1022
+ const cleaned = value.trim();
1023
+ if (cleaned === "" || cleaned === "/") return "";
1024
+ return `/${cleaned.replace(/^\/+|\/+$/g, "")}`;
1025
+ }
1026
+ function publicDocsRoute(publicPath, slugParts = []) {
1027
+ const slug = slugParts.join("/");
1028
+ if (!slug) return publicPath || "/";
1029
+ return publicPath ? `${publicPath}/${slug}` : `/${slug}`;
1030
+ }
1031
+ function toPublicDocsUrl(value, entry, publicPath) {
1032
+ const normalizedEntry = `/${normalizePathSegment(entry)}`;
1033
+ const hashIndex = value.indexOf("#");
1034
+ const hash = hashIndex >= 0 ? value.slice(hashIndex) : "";
1035
+ const withoutHash = hashIndex >= 0 ? value.slice(0, hashIndex) : value;
1036
+ const queryIndex = withoutHash.indexOf("?");
1037
+ const query = queryIndex >= 0 ? withoutHash.slice(queryIndex) : "";
1038
+ const pathname = normalizeUrlPath(queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash);
1039
+ if (publicPath === normalizedEntry) return value;
1040
+ if (pathname === normalizedEntry) return `${publicPath || "/"}${query}${hash}`;
1041
+ if (pathname.startsWith(`${normalizedEntry}/`)) {
1042
+ const suffix = pathname.slice(normalizedEntry.length + 1);
1043
+ return `${publicPath ? `${publicPath}/${suffix}` : `/${suffix}`}${query}${hash}`;
1044
+ }
1045
+ return value;
1046
+ }
1047
+ function withPublicDocsUrl(page, ctx) {
1048
+ const url = toPublicDocsUrl(page.url, ctx.entryPath, ctx.publicPath);
1049
+ return url === page.url ? page : {
1050
+ ...page,
1051
+ url
1052
+ };
1053
+ }
1054
+ function readDocsPath(root) {
1055
+ for (const ext of FILE_EXTS) {
1056
+ const configPath = path.join(root, `docs.config.${ext}`);
1057
+ if (!fs.existsSync(configPath)) continue;
1058
+ try {
1059
+ const match = fs.readFileSync(configPath, "utf-8").match(/docsPath\s*:\s*["']([^"']*)["']/);
1060
+ if (match) return match[1];
1061
+ } catch {}
1062
+ }
1063
+ }
1064
+ function resolvePublicMarkdownRequest(entry, docsPath, url, request) {
1065
+ if (docsPath === `/${normalizePathSegment(entry)}`) return null;
1066
+ const pathname = normalizeUrlPath(url.pathname);
1067
+ if (docsPath === "") {
1068
+ if (pathname === `/${normalizePathSegment(entry)}.md`) return {
1069
+ requestedPath: "",
1070
+ delivery: "md_route"
1071
+ };
1072
+ if (pathname !== "/docs.md" && pathname.endsWith(".md")) return {
1073
+ requestedPath: pathname.slice(1, -3),
1074
+ delivery: "md_route"
1075
+ };
1076
+ const hasSignatureAgent = hasDocsMarkdownSignatureAgent(request);
1077
+ if (acceptsMarkdown(request) || hasSignatureAgent) {
1078
+ const delivery = hasSignatureAgent ? "signature_agent" : "accept_header";
1079
+ return {
1080
+ requestedPath: pathname === "/" ? "" : pathname.slice(1),
1081
+ delivery
1082
+ };
1083
+ }
1084
+ return null;
1085
+ }
1086
+ if (pathname === `${docsPath}.md`) return {
1087
+ requestedPath: "",
1088
+ delivery: "md_route"
1089
+ };
1090
+ const slugPrefix = `${docsPath}/`;
1091
+ if (pathname.startsWith(slugPrefix) && pathname.endsWith(".md")) return {
1092
+ requestedPath: pathname.slice(slugPrefix.length, -3),
1093
+ delivery: "md_route"
1094
+ };
1095
+ const hasSignatureAgent = hasDocsMarkdownSignatureAgent(request);
1096
+ if (acceptsMarkdown(request) || hasSignatureAgent) {
1097
+ const delivery = hasSignatureAgent ? "signature_agent" : "accept_header";
1098
+ if (pathname === docsPath) return {
1099
+ requestedPath: "",
1100
+ delivery
1101
+ };
1102
+ if (pathname.startsWith(slugPrefix)) return {
1103
+ requestedPath: pathname.slice(slugPrefix.length),
1104
+ delivery
1105
+ };
1106
+ }
1107
+ return null;
1108
+ }
988
1109
  function renderMarkdownDocument(page, options = {}) {
989
1110
  if ("agentRawContent" in page && page.agentRawContent !== void 0) return page.agentRawContent;
990
1111
  const relatedLines = renderDocsRelatedMarkdownLines(page.related);
@@ -1771,6 +1892,7 @@ function generateLlmsTxt(indexes, options) {
1771
1892
  function createDocsAPI(options) {
1772
1893
  const root = options?.rootDir ?? process.cwd();
1773
1894
  const entry = options?.entry ?? readEntry(root);
1895
+ const docsPath = normalizeDocsPublicPath(options?.docsPath ?? readDocsPath(root), entry);
1774
1896
  const analytics = options?.analytics;
1775
1897
  const observability = options?.observability;
1776
1898
  const appDir = getNextAppDir(root);
@@ -1836,6 +1958,7 @@ function createDocsAPI(options) {
1836
1958
  const docsDirs = resolveDocsDirCandidates();
1837
1959
  return {
1838
1960
  entryPath: entry,
1961
+ publicPath: docsPath,
1839
1962
  docsDirs,
1840
1963
  changelogDirs: resolveChangelogDirCandidates(docsDirs)
1841
1964
  };
@@ -1844,6 +1967,7 @@ function createDocsAPI(options) {
1844
1967
  const docsDirs = resolveDocsDirCandidates(locale);
1845
1968
  return {
1846
1969
  entryPath: entry,
1970
+ publicPath: docsPath,
1847
1971
  locale,
1848
1972
  docsDirs,
1849
1973
  changelogDirs: resolveChangelogDirCandidates(docsDirs)
@@ -1859,13 +1983,13 @@ function createDocsAPI(options) {
1859
1983
  let next = [];
1860
1984
  for (const docsDir of ctx.docsDirs) {
1861
1985
  const excludedDirs = ctx.changelogDirs.filter((dir) => isWithinDir(dir, docsDir));
1862
- const docsPages = scanDocsDir(docsDir, ctx.entryPath, ctx.locale, excludedDirs);
1986
+ const docsPages = scanDocsDir(docsDir, ctx.entryPath, ctx.locale, excludedDirs, ctx.publicPath);
1863
1987
  if (docsPages.length === 0) continue;
1864
1988
  next = docsPages;
1865
1989
  break;
1866
1990
  }
1867
1991
  if (changelogConfig.enabled) {
1868
- const changelogPages = ctx.changelogDirs.flatMap((dir) => scanChangelogDir(dir, ctx.entryPath, changelogConfig.path, ctx.locale));
1992
+ const changelogPages = ctx.changelogDirs.flatMap((dir) => scanChangelogDir(dir, ctx.entryPath, changelogConfig.path, ctx.locale, ctx.publicPath));
1869
1993
  next = [...next, ...changelogPages];
1870
1994
  }
1871
1995
  indexesByLocale.set(key, next);
@@ -1885,16 +2009,21 @@ function createDocsAPI(options) {
1885
2009
  }
1886
2010
  async function getMarkdownDocument(ctx, requestedPath) {
1887
2011
  const normalizedRequest = normalizeRequestedMarkdownPath(ctx.entryPath, requestedPath);
2012
+ const normalizedPublicRequest = normalizePublicRequestedMarkdownPath(ctx, requestedPath);
1888
2013
  const normalizedEntry = `/${normalizePathSegment(ctx.entryPath)}`;
1889
2014
  const relativeSlug = normalizedRequest === normalizedEntry ? "" : normalizedRequest.slice(normalizedEntry.length).replace(/^\/+/, "");
1890
2015
  for (const docsDir of ctx.docsDirs) if (isHiddenFolderIndexPageDir(relativeSlug ? path.join(docsDir, ...relativeSlug.split("/")) : docsDir)) return null;
1891
2016
  for (const source of getMarkdownSources(ctx)) {
1892
2017
  const page = findDocsMcpPage(ctx.entryPath, await source.getPages(), requestedPath);
1893
- if (page) return renderMarkdownDocument(page, { llmsEnabled: llmsConfig.enabled });
2018
+ if (page) return renderMarkdownDocument(withPublicDocsUrl(page, ctx), { llmsEnabled: llmsConfig.enabled });
1894
2019
  }
1895
- const fallbackPage = getIndexes(ctx).find((page) => normalizeUrlPath(page.url) === normalizedRequest);
1896
- if (fallbackPage) return renderMarkdownDocument(fallbackPage, { llmsEnabled: llmsConfig.enabled });
1897
- for (const page of getIndexes(ctx)) if (normalizePathSegment(page.url.replace(/^\/+/, "").replace(`${ctx.entryPath}/`, "")) === normalizePathSegment(requestedPath.replace(/^\/+/, "").replace(/\.md$/i, ""))) return renderMarkdownDocument(page, { llmsEnabled: llmsConfig.enabled });
2020
+ const fallbackPage = getIndexes(ctx).find((page) => {
2021
+ const pageUrl = normalizeUrlPath(page.url);
2022
+ return pageUrl === normalizedRequest || pageUrl === normalizedPublicRequest;
2023
+ });
2024
+ if (fallbackPage) return renderMarkdownDocument(withPublicDocsUrl(fallbackPage, ctx), { llmsEnabled: llmsConfig.enabled });
2025
+ const requestedSlug = normalizePublicDocsSlug(ctx, normalizedPublicRequest);
2026
+ for (const page of getIndexes(ctx)) if (normalizePublicDocsSlug(ctx, page.url) === requestedSlug) return renderMarkdownDocument(withPublicDocsUrl(page, ctx), { llmsEnabled: llmsConfig.enabled });
1898
2027
  return null;
1899
2028
  }
1900
2029
  function getLlmsContent(ctx) {
@@ -2049,15 +2178,14 @@ function createDocsAPI(options) {
2049
2178
  manifest: readDocsSitemapManifest(root, sitemapConfig)
2050
2179
  });
2051
2180
  if (sitemapResponse) return sitemapResponse;
2052
- const markdownRequest = resolveMarkdownRequest(entry, url, request);
2181
+ const markdownRequest = resolveMarkdownRequest(entry, url, request) ?? resolvePublicMarkdownRequest(entry, docsPath, url, request);
2053
2182
  if (markdownRequest) {
2054
2183
  const document = await getMarkdownDocument(ctx, markdownRequest.requestedPath);
2055
2184
  const varyHeader = getDocsMarkdownVaryHeader(request);
2056
- const canonicalLinkHeader = getDocsMarkdownCanonicalLinkHeader({
2185
+ const canonicalLinkHeader = getPublicMarkdownCanonicalLinkHeader({
2057
2186
  origin: url.origin,
2058
- entry: ctx.entryPath,
2059
- requestedPath: markdownRequest.requestedPath,
2060
- locale: ctx.locale
2187
+ ctx,
2188
+ requestedPath: markdownRequest.requestedPath
2061
2189
  });
2062
2190
  if (!document) {
2063
2191
  await emitDocsAnalyticsEvent(analytics, {
@@ -65,6 +65,7 @@ function resolveDocsI18nConfig(i18n) {
65
65
  }
66
66
  function resolveDocsLocaleContext(config, locale) {
67
67
  const entryBase = config.entry ?? "docs";
68
+ const publicPath = normalizeDocsPublicPath(config.docsPath, entryBase);
68
69
  const i18n = resolveDocsI18nConfig(getDocsI18n(config));
69
70
  const contentDir = config.contentDir;
70
71
  function resolveContentDir(localeValue) {
@@ -77,15 +78,36 @@ function resolveDocsLocaleContext(config, locale) {
77
78
  }
78
79
  if (!i18n) return {
79
80
  entryPath: entryBase,
81
+ publicPath,
80
82
  docsDir: resolveContentDir()
81
83
  };
82
84
  const resolvedLocale = locale && i18n.locales.includes(locale) ? locale : i18n.defaultLocale;
83
85
  return {
84
86
  entryPath: entryBase,
87
+ publicPath,
85
88
  locale: resolvedLocale,
86
89
  docsDir: resolveContentDir(resolvedLocale)
87
90
  };
88
91
  }
92
+ function normalizeDocsPublicPath(value, entry) {
93
+ if (typeof value !== "string") return `/${entry.replace(/^\/+|\/+$/g, "") || "docs"}`;
94
+ const cleaned = value.trim();
95
+ if (cleaned === "" || cleaned === "/") return "";
96
+ return `/${cleaned.replace(/^\/+|\/+$/g, "")}`;
97
+ }
98
+ function publicDocsRoute(ctx, slugParts = []) {
99
+ const slug = slugParts.join("/");
100
+ if (!slug) return ctx.publicPath || "/";
101
+ return ctx.publicPath ? `${ctx.publicPath}/${slug}` : `/${slug}`;
102
+ }
103
+ function toPublicDocsPath(pathname, ctx) {
104
+ const normalizedEntry = `/${ctx.entryPath.replace(/^\/+|\/+$/g, "")}`;
105
+ const normalizedPath = pathname.replace(/\/+$/, "") || "/";
106
+ if (ctx.publicPath === normalizedEntry) return normalizedPath;
107
+ if (normalizedPath === normalizedEntry) return ctx.publicPath || "/";
108
+ if (normalizedPath.startsWith(`${normalizedEntry}/`)) return publicDocsRoute(ctx, normalizedPath.slice(normalizedEntry.length + 1).split("/").filter(Boolean));
109
+ return normalizedPath;
110
+ }
89
111
  function getExcludedDocsDirs(config, ctx) {
90
112
  const changelog = resolveChangelogConfig(config.changelog);
91
113
  if (!changelog.enabled) return [];
@@ -123,7 +145,7 @@ function buildChangelogTree(config, ctx, flat = false) {
123
145
  if (!changelog.enabled) return null;
124
146
  const entries = readChangelogTreeEntries(config, ctx);
125
147
  if (entries.length === 0) return null;
126
- const url = `/${ctx.entryPath}/${changelog.path}`;
148
+ const url = publicDocsRoute(ctx, [changelog.path]);
127
149
  const children = entries.map((entry) => ({
128
150
  type: "page",
129
151
  name: entry.title,
@@ -155,7 +177,7 @@ function buildTree(config, ctx, flat = false) {
155
177
  if (data.hidden !== true) rootChildren.push({
156
178
  type: "page",
157
179
  name: data.title ?? "Documentation",
158
- url: `/${ctx.entryPath}`,
180
+ url: publicDocsRoute(ctx),
159
181
  icon: resolveIcon(data.icon, icons)
160
182
  });
161
183
  }
@@ -167,7 +189,7 @@ function buildTree(config, ctx, flat = false) {
167
189
  if (!fs.existsSync(pagePath)) return null;
168
190
  const data = readFrontmatter(pagePath);
169
191
  const slug = [...baseSlug, name];
170
- const url = `/${ctx.entryPath}/${slug.join("/")}`;
192
+ const url = publicDocsRoute(ctx, slug);
171
193
  const icon = resolveIcon(data.icon, icons);
172
194
  const displayName = data.title ?? name.replace(/-/g, " ");
173
195
  const hasChildren = hasChildPages(full, excludedDirs);
@@ -318,7 +340,7 @@ function buildLastModifiedMap(config, ctx) {
318
340
  if (isExcludedDir(dir, excludedDirs)) return;
319
341
  const pagePath = path.join(dir, "page.mdx");
320
342
  if (fs.existsSync(pagePath)) {
321
- const url = slugParts.length === 0 ? `/${ctx.entryPath}` : `/${ctx.entryPath}/${slugParts.join("/")}`;
343
+ const url = publicDocsRoute(ctx, slugParts);
322
344
  map[url] = formatDate(fs.statSync(pagePath).mtime);
323
345
  }
324
346
  for (const name of fs.readdirSync(dir)) {
@@ -344,7 +366,7 @@ function buildDescriptionMap(config, ctx) {
344
366
  if (fs.existsSync(pagePath)) {
345
367
  const desc = readFrontmatter(pagePath).description;
346
368
  if (desc) {
347
- const url = slugParts.length === 0 ? `/${ctx.entryPath}` : `/${ctx.entryPath}/${slugParts.join("/")}`;
369
+ const url = publicDocsRoute(ctx, slugParts);
348
370
  map[url] = desc;
349
371
  }
350
372
  }
@@ -368,7 +390,7 @@ function buildReadingTimeMap(config, ctx, options) {
368
390
  const { data, content } = matter(fs.readFileSync(pagePath, "utf-8"));
369
391
  const minutes = resolvePageReadingTime(data, resolveDocsAgentMdxContent(content, "human"), options);
370
392
  if (typeof minutes === "number") {
371
- const url = slugParts.length === 0 ? `/${ctx.entryPath}` : `/${ctx.entryPath}/${slugParts.join("/")}`;
393
+ const url = publicDocsRoute(ctx, slugParts);
372
394
  map[url] = minutes;
373
395
  }
374
396
  }
@@ -394,7 +416,7 @@ function buildStructuredDataMap(config, ctx) {
394
416
  const pagePath = findDocsPageFile(dir);
395
417
  if (pagePath) {
396
418
  const { data } = matter(fs.readFileSync(pagePath, "utf-8"));
397
- const route = slugParts.length === 0 ? `/${ctx.entryPath}` : `/${ctx.entryPath}/${slugParts.join("/")}`;
419
+ const route = publicDocsRoute(ctx, slugParts);
398
420
  const title = typeof data.title === "string" ? data.title : slugParts.at(-1)?.replace(/-/g, " ") || "Documentation";
399
421
  const description = typeof data.description === "string" ? data.description : void 0;
400
422
  const stat = fs.statSync(pagePath);
@@ -610,9 +632,9 @@ function createDocsLayout(config, options) {
610
632
  const activeLocale = localeContext.locale ?? i18n?.defaultLocale;
611
633
  const docsApiUrl = withLangInUrl("/api/docs", activeLocale);
612
634
  const changelogConfig = resolveChangelogConfig(config.changelog);
613
- const changelogBasePath = changelogConfig.enabled ? `/${localeContext.entryPath}/${changelogConfig.path}` : void 0;
635
+ const changelogBasePath = changelogConfig.enabled ? publicDocsRoute(localeContext, [changelogConfig.path]) : void 0;
614
636
  const navTitle = config.nav?.title ?? "Docs";
615
- const navUrl = withLangInUrl(config.nav?.url ?? `/${localeContext.entryPath}`, activeLocale);
637
+ const navUrl = withLangInUrl(config.nav?.url ? toPublicDocsPath(config.nav.url, localeContext) : publicDocsRoute(localeContext), activeLocale);
616
638
  const themeSwitch = resolveThemeSwitch(config.themeToggle);
617
639
  const toggleConfig = typeof config.themeToggle === "object" ? config.themeToggle : void 0;
618
640
  const forcedTheme = themeSwitch.enabled === false && toggleConfig?.default && toggleConfig.default !== "system" ? toggleConfig.default : void 0;
@@ -757,6 +779,7 @@ function createDocsLayout(config, options) {
757
779
  breadcrumbEnabled,
758
780
  changelogBasePath,
759
781
  entry: localeContext.entryPath,
782
+ publicPath: localeContext.publicPath,
760
783
  locale: activeLocale,
761
784
  copyMarkdown: copyMarkdownEnabled,
762
785
  openDocs: openDocsEnabled,
@@ -16,6 +16,8 @@ interface DocsPageClientProps {
16
16
  changelogBasePath?: string;
17
17
  /** The docs entry folder name (e.g. "docs") — used to strip from breadcrumb */
18
18
  entry?: string;
19
+ /** Public docs route prefix. Empty string means docs render from the site root. */
20
+ publicPath?: string;
19
21
  /** Active locale (used for llms.txt links) */
20
22
  locale?: string;
21
23
  copyMarkdown?: boolean;
@@ -79,6 +81,7 @@ declare function DocsPageClient({
79
81
  breadcrumbEnabled,
80
82
  changelogBasePath,
81
83
  entry,
84
+ publicPath,
82
85
  locale,
83
86
  copyMarkdown,
84
87
  openDocs,
@@ -28,17 +28,17 @@ const agentLlmsDirectiveStyle = {
28
28
  * Path-based breadcrumb that shows only parent / current folder.
29
29
  * Skips the entry segment (e.g. "docs"). Parent is clickable.
30
30
  */
31
- function PathBreadcrumb({ pathname, entry, locale }) {
31
+ function PathBreadcrumb({ pathname, publicPath, locale }) {
32
32
  const router = useRouter();
33
33
  const segments = pathname.split("/").filter(Boolean);
34
- const entryParts = entry.split("/").filter(Boolean);
35
- const contentSegments = segments.slice(entryParts.length);
34
+ const publicParts = publicPath.split("/").filter(Boolean);
35
+ const contentSegments = publicParts.length > 0 && segments.slice(0, publicParts.length).join("/") === publicParts.join("/") ? segments.slice(publicParts.length) : segments;
36
36
  if (contentSegments.length < 2) return null;
37
37
  const parentSegment = contentSegments[contentSegments.length - 2];
38
38
  const currentSegment = contentSegments[contentSegments.length - 1];
39
39
  const parentLabel = parentSegment.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
40
40
  const currentLabel = currentSegment.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
41
- const localizedParentUrl = withLangInUrl("/" + [...segments.slice(0, entryParts.length), ...contentSegments.slice(0, -1)].join("/"), locale);
41
+ const localizedParentUrl = withLangInUrl("/" + [...publicParts, ...contentSegments.slice(0, -1)].filter(Boolean).join("/"), locale);
42
42
  return /* @__PURE__ */ jsxs("nav", {
43
43
  className: "fd-breadcrumb",
44
44
  "aria-label": "Breadcrumb",
@@ -102,6 +102,94 @@ function localizeInternalLinks(root, locale) {
102
102
  } catch {}
103
103
  }
104
104
  }
105
+ function normalizePublicDocsPath(value, entry) {
106
+ if (typeof value !== "string") return `/${entry.replace(/^\/+|\/+$/g, "") || "docs"}`;
107
+ const cleaned = value.trim();
108
+ if (cleaned === "" || cleaned === "/") return "";
109
+ return `/${cleaned.replace(/^\/+|\/+$/g, "")}`;
110
+ }
111
+ function toPublicDocsPath(pathname, entry, publicPath) {
112
+ const normalizedEntry = `/${entry.replace(/^\/+|\/+$/g, "") || "docs"}`;
113
+ const normalizedPath = pathname.replace(/\/+$/, "") || "/";
114
+ if (publicPath === normalizedEntry) return normalizedPath;
115
+ if (normalizedPath === normalizedEntry) return publicPath || "/";
116
+ if (normalizedPath.startsWith(`${normalizedEntry}/`)) {
117
+ const suffix = normalizedPath.slice(normalizedEntry.length + 1);
118
+ return publicPath ? `${publicPath}/${suffix}` : `/${suffix}`;
119
+ }
120
+ return normalizedPath;
121
+ }
122
+ function rewriteDocsPathLinks(root, entry, publicPath) {
123
+ const anchors = root.querySelectorAll("a[href]:not([data-fd-docspath=\"true\"])");
124
+ for (const anchor of anchors) {
125
+ const href = anchor.getAttribute("href");
126
+ if (!href || href.startsWith("#")) continue;
127
+ if (/^(mailto:|tel:|javascript:)/i.test(href)) continue;
128
+ try {
129
+ const url = new URL(href, window.location.origin);
130
+ if (url.origin !== window.location.origin) continue;
131
+ const nextPath = toPublicDocsPath(url.pathname, entry, publicPath);
132
+ if (nextPath === url.pathname) continue;
133
+ anchor.href = `${nextPath}${url.search}${url.hash}`;
134
+ anchor.dataset.fdDocspath = "true";
135
+ } catch {}
136
+ }
137
+ }
138
+ function normalizeCurrentPath(pathname) {
139
+ return pathname.replace(/\/+$/, "") || "/";
140
+ }
141
+ function syncDocsPathActiveLinks(root, entry, publicPath) {
142
+ const currentPath = normalizeCurrentPath(window.location.pathname);
143
+ const anchors = root.querySelectorAll("a[data-active][href]");
144
+ for (const anchor of anchors) {
145
+ const href = anchor.getAttribute("href");
146
+ if (!href || href.startsWith("#")) continue;
147
+ try {
148
+ const url = new URL(href, window.location.origin);
149
+ if (url.origin !== window.location.origin) continue;
150
+ if (!isDocsNavigationPath(url.pathname, entry, publicPath)) continue;
151
+ const publicHrefPath = normalizeCurrentPath(toPublicDocsPath(url.pathname, entry, publicPath));
152
+ anchor.dataset.active = publicHrefPath === currentPath ? "true" : "false";
153
+ } catch {}
154
+ }
155
+ }
156
+ function isFrameworkPath(pathname) {
157
+ return pathname.startsWith("/_next") || pathname.startsWith("/api") || pathname.startsWith("/.well-known") || pathname === "/mcp" || pathname === "/llms.txt" || pathname === "/llms-full.txt" || pathname === "/robots.txt" || pathname === "/sitemap.xml" || pathname === "/sitemap.md" || pathname === "/AGENTS.md" || pathname === "/AGENT.md" || pathname === "/skill.md" || /\/[^/]+\.[^/]+$/.test(pathname);
158
+ }
159
+ function isDocsNavigationPath(pathname, entry, publicPath) {
160
+ if (isFrameworkPath(pathname)) return false;
161
+ const normalizedEntry = `/${entry.replace(/^\/+|\/+$/g, "") || "docs"}`;
162
+ if (pathname === normalizedEntry || pathname.startsWith(`${normalizedEntry}/`)) return true;
163
+ if (publicPath === "") return pathname.startsWith("/");
164
+ return pathname === publicPath || pathname.startsWith(`${publicPath}/`);
165
+ }
166
+ function installDocsPathNavigationGuard(entry, publicPath) {
167
+ if (publicPath === `/${entry.replace(/^\/+|\/+$/g, "") || "docs"}`) return void 0;
168
+ function onClick(event) {
169
+ if (event.defaultPrevented) return;
170
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
171
+ const anchor = (event.target instanceof Element ? event.target : null)?.closest("a[href]");
172
+ if (!anchor) return;
173
+ if (anchor.target && anchor.target !== "_self") return;
174
+ if (anchor.hasAttribute("download")) return;
175
+ const href = anchor.getAttribute("href");
176
+ if (!href || href.startsWith("#")) return;
177
+ if (/^(mailto:|tel:|javascript:)/i.test(href)) return;
178
+ try {
179
+ const url = new URL(href, window.location.origin);
180
+ if (url.origin !== window.location.origin) return;
181
+ if (!isDocsNavigationPath(url.pathname, entry, publicPath)) return;
182
+ const nextPath = toPublicDocsPath(url.pathname, entry, publicPath);
183
+ if (nextPath === url.pathname) return;
184
+ const nextHref = `${nextPath}${url.search}${url.hash}`;
185
+ if (nextHref === `${window.location.pathname}${window.location.search}${window.location.hash}`) return;
186
+ event.preventDefault();
187
+ window.location.assign(nextHref);
188
+ } catch {}
189
+ }
190
+ document.addEventListener("click", onClick, true);
191
+ return () => document.removeEventListener("click", onClick, true);
192
+ }
105
193
  function decodeHashTarget(hash) {
106
194
  const value = hash.startsWith("#") ? hash.slice(1) : hash;
107
195
  try {
@@ -175,13 +263,14 @@ function TitleDecorations({ description, belowTitle }) {
175
263
  if (!description && !belowTitle) return null;
176
264
  return /* @__PURE__ */ jsx(Fragment$1, { children: Children.toArray([description, belowTitle].filter(Boolean)) });
177
265
  }
178
- 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, readingTimeMap, readingTime: readingTimeProp, structuredDataMap, structuredData: structuredDataProp, readingTimeEnabled = false, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackOnFeedback, analytics = false, children }) {
266
+ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, changelogBasePath, entry = "docs", publicPath, locale, copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, readingTimeMap, readingTime: readingTimeProp, structuredDataMap, structuredData: structuredDataProp, readingTimeEnabled = false, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackOnFeedback, analytics = false, children }) {
179
267
  const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
180
268
  const [toc, setToc] = useState([]);
181
269
  const [titlePortalHost, setTitlePortalHost] = useState(null);
182
270
  const [browserPath, setBrowserPath] = useState(null);
183
271
  const pathname = usePathname();
184
272
  const activeLocale = resolveClientLocale(useWindowSearchParams(), locale);
273
+ const resolvedPublicPath = normalizePublicDocsPath(publicPath, entry);
185
274
  const llmsLangQuery = activeLocale ? `?lang=${encodeURIComponent(activeLocale)}` : "";
186
275
  const pageDescription = description ?? descriptionMap?.[pathname.replace(/\/$/, "") || "/"];
187
276
  const normalizedPath = (browserPath ?? pathname).replace(/\/$/, "") || "/";
@@ -207,6 +296,9 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
207
296
  isChangelogRoute,
208
297
  normalizedPath
209
298
  ]);
299
+ useEffect(() => {
300
+ return installDocsPathNavigationGuard(entry, resolvedPublicPath);
301
+ }, [entry, resolvedPublicPath]);
210
302
  const resolvedReadingTime = !isChangelogRoute ? readingTimeProp !== void 0 ? readingTimeProp : readingTimeEnabled ? matchedReadingTime : void 0 : void 0;
211
303
  const effectiveTocEnabled = isChangelogRoute ? false : tocEnabled;
212
304
  const effectiveBreadcrumbEnabled = isChangelogRoute ? false : breadcrumbEnabled;
@@ -225,17 +317,22 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
225
317
  return () => cancelAnimationFrame(timer);
226
318
  }, [effectiveTocEnabled, pathname]);
227
319
  useEffect(() => {
228
- if (!activeLocale) return;
229
320
  const timer = requestAnimationFrame(() => {
230
- const container = document.getElementById("nd-page");
231
- if (!container) return;
232
- localizeInternalLinks(container, activeLocale);
321
+ const root = document.body;
322
+ if (!root) return;
323
+ rewriteDocsPathLinks(root, entry, resolvedPublicPath);
324
+ syncDocsPathActiveLinks(root, entry, resolvedPublicPath);
325
+ if (!activeLocale) return;
326
+ localizeInternalLinks(root, activeLocale);
233
327
  });
234
328
  return () => cancelAnimationFrame(timer);
235
329
  }, [
236
330
  activeLocale,
331
+ browserPath,
237
332
  children,
238
- pathname
333
+ entry,
334
+ pathname,
335
+ resolvedPublicPath
239
336
  ]);
240
337
  useEffect(() => {
241
338
  setBrowserPath(window.location.pathname);
@@ -383,7 +480,7 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
383
480
  children: [
384
481
  effectiveBreadcrumbEnabled && /* @__PURE__ */ jsx(PathBreadcrumb, {
385
482
  pathname,
386
- entry,
483
+ publicPath: resolvedPublicPath,
387
484
  locale: activeLocale
388
485
  }),
389
486
  showActionsAboveTitle && /* @__PURE__ */ jsxs("div", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.1.108",
3
+ "version": "0.1.109",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -139,7 +139,7 @@
139
139
  "tsdown": "^0.20.3",
140
140
  "typescript": "^5.9.3",
141
141
  "vitest": "^3.2.4",
142
- "@farming-labs/docs": "0.1.108"
142
+ "@farming-labs/docs": "0.1.109"
143
143
  },
144
144
  "peerDependencies": {
145
145
  "@farming-labs/docs": ">=0.0.1",