@marlinjai/clearify 2.0.0 → 2.1.0

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.
@@ -2,7 +2,7 @@ import {
2
2
  loadUserConfig,
3
3
  resolveConfig,
4
4
  resolveSections
5
- } from "./chunk-PGTNVDNF.js";
5
+ } from "./chunk-ZWF6VCSQ.js";
6
6
  import {
7
7
  scanDocs
8
8
  } from "./chunk-CTL4YL7C.js";
@@ -6,14 +6,18 @@ import {
6
6
  import {
7
7
  mermaidContentHash
8
8
  } from "./chunk-SCZZB7OE.js";
9
+ import {
10
+ generateLlmsFullTxt,
11
+ generateLlmsTxt
12
+ } from "./chunk-TVDWHKQF.js";
9
13
  import {
10
14
  clearifyPlugin
11
- } from "./chunk-RW4IKHY4.js";
15
+ } from "./chunk-Q5FUAK6J.js";
12
16
  import {
13
17
  loadUserConfig,
14
18
  resolveConfig,
15
19
  resolveSections
16
- } from "./chunk-PGTNVDNF.js";
20
+ } from "./chunk-ZWF6VCSQ.js";
17
21
  import {
18
22
  buildSectionData
19
23
  } from "./chunk-CTL4YL7C.js";
@@ -72,6 +76,29 @@ function generateSitemap(routes, siteUrl) {
72
76
  ${urls.join("\n")}
73
77
  </urlset>`;
74
78
  }
79
+ function formatBytes(bytes) {
80
+ if (bytes < 1024) return `${bytes} B`;
81
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
82
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
83
+ }
84
+ function collectLlmsDocs(sections) {
85
+ const out = [];
86
+ for (const sd of sections) {
87
+ for (const doc of sd.docs) {
88
+ const title = doc.frontmatter.title ?? "Untitled";
89
+ const optional = (doc.frontmatter.order ?? 0) >= 9e3;
90
+ const markdown = existsSync(doc.filePath) ? readFileSync(doc.filePath, "utf-8") : "";
91
+ out.push({
92
+ title,
93
+ routePath: doc.routePath,
94
+ sectionLabel: sd.section.label,
95
+ markdown,
96
+ optional
97
+ });
98
+ }
99
+ }
100
+ return out;
101
+ }
75
102
  function generateRobotsTxt(siteUrl) {
76
103
  const lines = ["User-agent: *", "Allow: /"];
77
104
  if (siteUrl) {
@@ -312,6 +339,19 @@ async function buildSite() {
312
339
  const sitemap = generateSitemap(sitemapRoutes, config.siteUrl);
313
340
  writeFileSync(resolve(outDir, "sitemap.xml"), sitemap);
314
341
  console.log(" Generated sitemap.xml");
342
+ if (config.generateLlmsTxt !== false) {
343
+ const llmsDocs = collectLlmsDocs(sectionDataList);
344
+ const llmsTxt = generateLlmsTxt(config, llmsDocs);
345
+ const llmsFullTxt = generateLlmsFullTxt(config, llmsDocs);
346
+ const llmsTxtPath = resolve(outDir, "llms.txt");
347
+ const llmsFullTxtPath = resolve(outDir, "llms-full.txt");
348
+ writeFileSync(llmsTxtPath, llmsTxt);
349
+ writeFileSync(llmsFullTxtPath, llmsFullTxt);
350
+ const llmsTxtSize = Buffer.byteLength(llmsTxt, "utf-8");
351
+ const llmsFullSize = Buffer.byteLength(llmsFullTxt, "utf-8");
352
+ console.log(` Generated ${llmsTxtPath} (${formatBytes(llmsTxtSize)})`);
353
+ console.log(` Generated ${llmsFullTxtPath} (${formatBytes(llmsFullSize)})`);
354
+ }
315
355
  rmSync(ssrOutDir, { recursive: true, force: true });
316
356
  console.log(" Cleaned up SSR build artifacts.");
317
357
  if (mermaidStrategy === "build") {
@@ -10,7 +10,7 @@ import {
10
10
  resolveSections,
11
11
  scanHubProjects,
12
12
  writeDataConfig
13
- } from "./chunk-PGTNVDNF.js";
13
+ } from "./chunk-ZWF6VCSQ.js";
14
14
  import {
15
15
  buildSectionData
16
16
  } from "./chunk-CTL4YL7C.js";
@@ -0,0 +1,134 @@
1
+ // src/core/llms-txt.ts
2
+ import { readFileSync } from "fs";
3
+ import matter from "gray-matter";
4
+ function normalizeBase(siteUrl) {
5
+ if (!siteUrl) return "";
6
+ return siteUrl.replace(/\/$/, "");
7
+ }
8
+ function toAbsoluteUrl(base, routePath) {
9
+ if (!base) return routePath;
10
+ if (!routePath.startsWith("/")) return `${base}/${routePath}`;
11
+ return `${base}${routePath}`;
12
+ }
13
+ function stripFrontmatterAndH1(markdown) {
14
+ const { content } = matter(markdown);
15
+ return content.replace(/^\s*#\s+[^\n]*\n+/, "").trimStart();
16
+ }
17
+ function firstParagraphSummary(markdown) {
18
+ const parsed = matter(markdown);
19
+ const fromFrontmatter = typeof parsed.data.summary === "string" && parsed.data.summary.trim() || typeof parsed.data.description === "string" && parsed.data.description.trim() || "";
20
+ const body = parsed.content;
21
+ const lines = body.split(/\r?\n/);
22
+ let inFence = false;
23
+ const paragraph = [];
24
+ for (const rawLine of lines) {
25
+ const line = rawLine.trimEnd();
26
+ if (/^```/.test(line)) {
27
+ inFence = !inFence;
28
+ if (paragraph.length > 0) break;
29
+ continue;
30
+ }
31
+ if (inFence) continue;
32
+ if (!line.trim()) {
33
+ if (paragraph.length > 0) break;
34
+ continue;
35
+ }
36
+ if (/^(#{1,6}\s|>\s|[-*+]\s|\d+\.\s|\|.*\|\s*$|<)/.test(line)) {
37
+ if (paragraph.length > 0) break;
38
+ continue;
39
+ }
40
+ paragraph.push(line.trim());
41
+ }
42
+ const text = paragraph.join(" ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/([*_]{1,3})([^*_]+)\1/g, "$2").replace(/\s+/g, " ").trim();
43
+ return text || fromFrontmatter;
44
+ }
45
+ function uniqueOrdered(items) {
46
+ const seen = /* @__PURE__ */ new Set();
47
+ const out = [];
48
+ for (const item of items) {
49
+ if (seen.has(item)) continue;
50
+ seen.add(item);
51
+ out.push(item);
52
+ }
53
+ return out;
54
+ }
55
+ function buildHeader(config) {
56
+ const name = config.name.trim();
57
+ const tagline = config.hubProject?.description && config.hubProject.description.trim() || `Documentation for ${name}.`;
58
+ return `# ${name}
59
+
60
+ > ${tagline}
61
+
62
+ `;
63
+ }
64
+ function generateLlmsTxt(config, docs) {
65
+ const base = normalizeBase(config.siteUrl);
66
+ let out = buildHeader(config);
67
+ const mainDocs = docs.filter((d) => !d.optional);
68
+ const optionalDocs = docs.filter((d) => d.optional);
69
+ const sectionLabels = uniqueOrdered(mainDocs.map((d) => d.sectionLabel));
70
+ for (const label of sectionLabels) {
71
+ const sectionDocs = mainDocs.filter((d) => d.sectionLabel === label);
72
+ if (sectionDocs.length === 0) continue;
73
+ out += `## ${label}
74
+
75
+ `;
76
+ for (const doc of sectionDocs) {
77
+ const url = toAbsoluteUrl(base, doc.routePath);
78
+ const summary = firstParagraphSummary(doc.markdown);
79
+ if (summary) {
80
+ out += `- [${doc.title}](${url}): ${summary}
81
+ `;
82
+ } else {
83
+ out += `- [${doc.title}](${url})
84
+ `;
85
+ }
86
+ }
87
+ out += "\n";
88
+ }
89
+ if (optionalDocs.length > 0) {
90
+ out += `## Optional
91
+
92
+ `;
93
+ for (const doc of optionalDocs) {
94
+ const url = toAbsoluteUrl(base, doc.routePath);
95
+ const summary = firstParagraphSummary(doc.markdown);
96
+ if (summary) {
97
+ out += `- [${doc.title}](${url}): ${summary}
98
+ `;
99
+ } else {
100
+ out += `- [${doc.title}](${url})
101
+ `;
102
+ }
103
+ }
104
+ out += "\n";
105
+ }
106
+ return out.trimEnd() + "\n";
107
+ }
108
+ function generateLlmsFullTxt(config, docs) {
109
+ const base = normalizeBase(config.siteUrl);
110
+ let out = buildHeader(config);
111
+ for (const doc of docs) {
112
+ const url = toAbsoluteUrl(base, doc.routePath);
113
+ const body = stripFrontmatterAndH1(doc.markdown);
114
+ out += `## ${doc.title}
115
+
116
+ `;
117
+ out += `Source: ${url}
118
+
119
+ `;
120
+ if (body) {
121
+ out += body.trimEnd() + "\n\n";
122
+ }
123
+ }
124
+ return out.trimEnd() + "\n";
125
+ }
126
+ function readMarkdownFile(filePath) {
127
+ return readFileSync(filePath, "utf-8");
128
+ }
129
+
130
+ export {
131
+ generateLlmsTxt,
132
+ generateLlmsFullTxt,
133
+ readMarkdownFile
134
+ };
@@ -117,7 +117,8 @@ var ClearifyConfigSchema = z.object({
117
117
  hubProject: HubProjectPartialSchema,
118
118
  customCss: z.string().optional(),
119
119
  headTags: z.array(z.string()).optional(),
120
- includeReadme: z.boolean().default(false)
120
+ includeReadme: z.boolean().default(false),
121
+ generateLlmsTxt: z.boolean().default(true)
121
122
  });
122
123
  var ClearifyDataSchema = z.object({
123
124
  name: z.string().optional(),
@@ -265,6 +266,11 @@ function resolveConfig(userConfig = {}, root) {
265
266
  if (!userConfig.name && root) {
266
267
  parsed.name = detectProjectName(root);
267
268
  }
269
+ if (parsed.generateLlmsTxt && !parsed.siteUrl) {
270
+ throw new Error(
271
+ "clearify config: `generateLlmsTxt` is enabled (default) but `siteUrl` is not set. Set `siteUrl` to the production URL of your docs site, or set `generateLlmsTxt: false` to disable."
272
+ );
273
+ }
268
274
  return parsed;
269
275
  }
270
276
  function slugify(str) {
@@ -8,7 +8,7 @@ import {
8
8
  resolveSections,
9
9
  scanHubProjects,
10
10
  writeDataConfig
11
- } from "../chunk-PGTNVDNF.js";
11
+ } from "../chunk-ZWF6VCSQ.js";
12
12
  import "../chunk-AJBCHKBP.js";
13
13
  import {
14
14
  defineConfig
@@ -0,0 +1,54 @@
1
+ import { ClearifyConfig } from '../types/index.js';
2
+
3
+ /**
4
+ * A doc entry as consumed by the llms.txt generators. Produced from the
5
+ * scanned section data by callers (e.g. the build pipeline) so these
6
+ * functions stay pure string builders.
7
+ */
8
+ interface LlmsDocEntry {
9
+ /** Page title (from frontmatter or derived). */
10
+ title: string;
11
+ /** Route path on the site, e.g. `/getting-started`. */
12
+ routePath: string;
13
+ /** Section label the page belongs to, e.g. `Documentation`. */
14
+ sectionLabel: string;
15
+ /** Raw markdown source (frontmatter optional, handled by the generator). */
16
+ markdown: string;
17
+ /**
18
+ * Optional flag. When true the page goes under the `## Optional` bucket at
19
+ * the end of llms.txt rather than the main section list. Use for auxiliary
20
+ * content like a changelog or roadmap.
21
+ */
22
+ optional?: boolean;
23
+ }
24
+ /**
25
+ * Generate the `llms.txt` index file content.
26
+ *
27
+ * Format:
28
+ * # {name}
29
+ *
30
+ * > {tagline}
31
+ *
32
+ * ## {Section label}
33
+ *
34
+ * - [Title](https://site/path): first-paragraph summary
35
+ * ...
36
+ *
37
+ * ## Optional
38
+ * - [Title](https://site/path): summary
39
+ */
40
+ declare function generateLlmsTxt(config: ClearifyConfig, docs: LlmsDocEntry[]): string;
41
+ /**
42
+ * Generate the `llms-full.txt` bundle file content. Concatenates the entire
43
+ * markdown body of every doc under an H2 with a `Source:` line, so an LLM
44
+ * with no browsing can ingest the whole site from one file.
45
+ */
46
+ declare function generateLlmsFullTxt(config: ClearifyConfig, docs: LlmsDocEntry[]): string;
47
+ /**
48
+ * Read a markdown file from disk. Returned as-is, including frontmatter.
49
+ * Exists here so callers in the build pipeline stay thin and the unit tests
50
+ * can stub in-memory markdown without touching the filesystem.
51
+ */
52
+ declare function readMarkdownFile(filePath: string): string;
53
+
54
+ export { type LlmsDocEntry, generateLlmsFullTxt, generateLlmsTxt, readMarkdownFile };
@@ -0,0 +1,10 @@
1
+ import {
2
+ generateLlmsFullTxt,
3
+ generateLlmsTxt,
4
+ readMarkdownFile
5
+ } from "../chunk-TVDWHKQF.js";
6
+ export {
7
+ generateLlmsFullTxt,
8
+ generateLlmsTxt,
9
+ readMarkdownFile
10
+ };
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  buildSite
3
- } from "../chunk-BCEGDIR6.js";
3
+ } from "../chunk-PZA4UAFG.js";
4
4
  import "../chunk-PRTER35L.js";
5
5
  import "../chunk-SCZZB7OE.js";
6
- import "../chunk-RW4IKHY4.js";
6
+ import "../chunk-TVDWHKQF.js";
7
+ import "../chunk-Q5FUAK6J.js";
7
8
  import "../chunk-B2Q23JW3.js";
8
- import "../chunk-PGTNVDNF.js";
9
+ import "../chunk-ZWF6VCSQ.js";
9
10
  import "../chunk-AJBCHKBP.js";
10
11
  import "../chunk-V7LLYIRO.js";
11
12
  import "../chunk-CTL4YL7C.js";
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  checkFrontmatter,
3
3
  checkLinks
4
- } from "../chunk-CK2SLBAG.js";
5
- import "../chunk-PGTNVDNF.js";
4
+ } from "../chunk-GZE6QLJT.js";
5
+ import "../chunk-ZWF6VCSQ.js";
6
6
  import "../chunk-AJBCHKBP.js";
7
7
  import "../chunk-V7LLYIRO.js";
8
8
  import "../chunk-CTL4YL7C.js";
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  buildSite
3
- } from "../chunk-BCEGDIR6.js";
3
+ } from "../chunk-PZA4UAFG.js";
4
4
  import {
5
5
  remarkMermaidToComponent
6
6
  } from "../chunk-PRTER35L.js";
7
7
  import "../chunk-SCZZB7OE.js";
8
+ import "../chunk-TVDWHKQF.js";
8
9
  import {
9
10
  clearifyPlugin
10
- } from "../chunk-RW4IKHY4.js";
11
+ } from "../chunk-Q5FUAK6J.js";
11
12
  import "../chunk-B2Q23JW3.js";
12
13
  import {
13
14
  init
@@ -15,11 +16,11 @@ import {
15
16
  import {
16
17
  checkFrontmatter,
17
18
  checkLinks
18
- } from "../chunk-CK2SLBAG.js";
19
+ } from "../chunk-GZE6QLJT.js";
19
20
  import {
20
21
  loadUserConfig,
21
22
  resolveConfig
22
- } from "../chunk-PGTNVDNF.js";
23
+ } from "../chunk-ZWF6VCSQ.js";
23
24
  import "../chunk-AJBCHKBP.js";
24
25
  import "../chunk-V7LLYIRO.js";
25
26
  import "../chunk-CTL4YL7C.js";
@@ -62,6 +62,12 @@ interface ClearifyConfig {
62
62
  customCss?: string;
63
63
  headTags?: string[];
64
64
  includeReadme?: boolean;
65
+ /**
66
+ * When true (default), `clearify build` writes `llms.txt` and
67
+ * `llms-full.txt` into the output directory for AI agents and crawlers.
68
+ * Requires `siteUrl` to be set. Set to `false` to skip generation.
69
+ */
70
+ generateLlmsTxt?: boolean;
65
71
  }
66
72
  interface NavigationItem {
67
73
  label: string;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  clearifyPlugin
3
- } from "../chunk-RW4IKHY4.js";
3
+ } from "../chunk-Q5FUAK6J.js";
4
4
  import "../chunk-B2Q23JW3.js";
5
- import "../chunk-PGTNVDNF.js";
5
+ import "../chunk-ZWF6VCSQ.js";
6
6
  import "../chunk-AJBCHKBP.js";
7
7
  import "../chunk-V7LLYIRO.js";
8
8
  import "../chunk-CTL4YL7C.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marlinjai/clearify",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "An open-source documentation site generator. Turn markdown into beautiful docs.",
5
5
  "type": "module",
6
6
  "bin": {