Skip to content

Commit 9b182db

Browse files
a2ucursoragent
andcommitted
Add llms.txt generation script for LLM-friendly documentation
- Add scripts/generate-llms-txt.js to generate llms.txt and llms-full.txt - Integrate script into docs:build pipeline - Generates structured documentation index for LLM consumption Co-authored-by: Cursor <[email protected]>
1 parent 294dfff commit 9b182db

2 files changed

Lines changed: 207 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"docs:dev": "vuepress dev docs",
8-
"docs:build": "vuepress build docs",
8+
"docs:build": "vuepress build docs && node scripts/generate-llms-txt.js",
99
"dev": "vite",
1010
"build": "vue-tsc && vite build",
1111
"preview": "vite preview"

scripts/generate-llms-txt.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { promises as fs } from "fs";
2+
import path from "path";
3+
import { fileURLToPath } from "url";
4+
5+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
6+
const repoRoot = path.resolve(__dirname, "..");
7+
const docsDir = path.join(repoRoot, "docs");
8+
const distDir = path.join(docsDir, ".vuepress", "dist");
9+
const documentsPath = path.join(
10+
docsDir,
11+
".vuepress",
12+
"config-client",
13+
"documents.ts"
14+
);
15+
const sidebarPath = path.join(
16+
docsDir,
17+
".vuepress",
18+
"config-client",
19+
"sidebar.ts"
20+
);
21+
22+
const baseUrl = "https://docs.imunify360.com";
23+
const siteTitle = "Imunify360 Documentation";
24+
const siteSummary =
25+
"Imunify360 is a security solution for Linux web servers based on machine learning technology. It provides advanced firewall, intrusion detection, malware scanning, patch management, and proactive defense against zero-day attacks.";
26+
27+
const normalizeRouteKey = (route) => (route.endsWith("/") ? route : `${route}/`);
28+
29+
const parseExportDefault = (content) => {
30+
const withoutExport = content.replace(/^\s*export\s+default\s+/m, "").trim();
31+
const withoutSemicolon = withoutExport.replace(/;?\s*$/, "");
32+
return new Function(`return (${withoutSemicolon})`)();
33+
};
34+
35+
const routeToDocPath = (route) => {
36+
const cleaned = route.replace(/^\//, "");
37+
if (route.endsWith(".md")) {
38+
return path.join(docsDir, cleaned);
39+
}
40+
const withSlash = route.endsWith("/") ? route : `${route}/`;
41+
return path.join(docsDir, withSlash.replace(/^\//, ""), "README.md");
42+
};
43+
44+
const docPathToRoute = (docPath) => {
45+
const rel = path.relative(docsDir, docPath);
46+
if (rel === "README.md") {
47+
return "/";
48+
}
49+
if (path.basename(rel) === "README.md") {
50+
return `/${path.posix.dirname(rel).replace(/\\/g, "/")}/`;
51+
}
52+
return `/${rel.replace(/\\/g, "/")}`;
53+
};
54+
55+
const routeToMdUrl = (route) => {
56+
if (route.endsWith(".md")) {
57+
return `${baseUrl}${route}`;
58+
}
59+
const withSlash = route.endsWith("/") ? route : `${route}/`;
60+
return `${baseUrl}${withSlash}index.md`;
61+
};
62+
63+
const getMarkdownTitle = async (filePath) => {
64+
const content = await fs.readFile(filePath, "utf8");
65+
const lines = content.split(/\r?\n/);
66+
let startIndex = 0;
67+
if (lines[0] === "---") {
68+
const endIndex = lines.findIndex((line, i) => i > 0 && line === "---");
69+
if (endIndex !== -1) {
70+
startIndex = endIndex + 1;
71+
}
72+
}
73+
for (let i = startIndex; i < lines.length; i += 1) {
74+
const line = lines[i].trim();
75+
if (line.startsWith("# ")) {
76+
return line.replace(/^#\s+/, "").trim();
77+
}
78+
}
79+
return path.basename(filePath, path.extname(filePath));
80+
};
81+
82+
const stripFrontmatter = (content) => {
83+
const lines = content.split(/\r?\n/);
84+
if (lines[0] !== "---") {
85+
return content;
86+
}
87+
const endIndex = lines.findIndex((line, i) => i > 0 && line === "---");
88+
if (endIndex === -1) {
89+
return content;
90+
}
91+
return lines.slice(endIndex + 1).join("\n").trimStart();
92+
};
93+
94+
const readMarkdownContent = async (filePath) => {
95+
const content = await fs.readFile(filePath, "utf8");
96+
return stripFrontmatter(content);
97+
};
98+
99+
const collectMarkdownFiles = async (dir, results = []) => {
100+
const entries = await fs.readdir(dir, { withFileTypes: true });
101+
for (const entry of entries) {
102+
if (entry.name === ".vuepress") {
103+
continue;
104+
}
105+
const fullPath = path.join(dir, entry.name);
106+
if (entry.isDirectory()) {
107+
await collectMarkdownFiles(fullPath, results);
108+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
109+
results.push(fullPath);
110+
}
111+
}
112+
return results;
113+
};
114+
115+
const writeMarkdownCopies = async (markdownFiles) => {
116+
await fs.mkdir(distDir, { recursive: true });
117+
await Promise.all(
118+
markdownFiles.map(async (filePath) => {
119+
const rel = path.relative(docsDir, filePath);
120+
const dirName = path.dirname(rel);
121+
const baseName = path.basename(rel);
122+
const outputName = baseName === "README.md" ? "index.md" : baseName;
123+
const outputDir = dirName === "." ? distDir : path.join(distDir, dirName);
124+
const outputPath = path.join(outputDir, outputName);
125+
await fs.mkdir(outputDir, { recursive: true });
126+
const content = await fs.readFile(filePath, "utf8");
127+
await fs.writeFile(outputPath, content);
128+
})
129+
);
130+
};
131+
132+
const buildLlmsTxt = async () => {
133+
const [documentsRaw, sidebarRaw] = await Promise.all([
134+
fs.readFile(documentsPath, "utf8"),
135+
fs.readFile(sidebarPath, "utf8"),
136+
]);
137+
const documents = parseExportDefault(documentsRaw);
138+
const sidebar = parseExportDefault(sidebarRaw);
139+
const normalizedSidebar = new Map(
140+
Object.entries(sidebar).map(([key, value]) => [
141+
normalizeRouteKey(key),
142+
value,
143+
])
144+
);
145+
146+
const markdownFiles = await collectMarkdownFiles(docsDir);
147+
await writeMarkdownCopies(markdownFiles);
148+
149+
const lines = [`# ${siteTitle}`, "", `> ${siteSummary}`, ""];
150+
const fullLines = [`# ${siteTitle}`, "", `> ${siteSummary}`, ""];
151+
152+
for (const doc of documents) {
153+
const heading = doc.title;
154+
const docRoute = normalizeRouteKey(doc.link);
155+
const sidebarEntry = normalizedSidebar.get(docRoute) || [];
156+
const sidebarRoutes = sidebarEntry.flatMap((entry) => entry.children || []);
157+
158+
const childRoutes = new Set(sidebarRoutes);
159+
const docDir = path.join(docsDir, docRoute.replace(/^\//, ""));
160+
const extraRoutes = markdownFiles
161+
.filter((filePath) => filePath.startsWith(docDir))
162+
.map((filePath) => docPathToRoute(filePath))
163+
.filter((route) => route !== "/" && !childRoutes.has(route));
164+
165+
const orderedRoutes = [
166+
...sidebarRoutes,
167+
...extraRoutes.sort((a, b) => a.localeCompare(b)),
168+
];
169+
170+
lines.push(`## ${heading}`, "");
171+
fullLines.push(`## ${heading}`, "");
172+
173+
for (const route of orderedRoutes) {
174+
const docPath = routeToDocPath(route);
175+
try {
176+
await fs.access(docPath);
177+
} catch {
178+
continue;
179+
}
180+
const title = await getMarkdownTitle(docPath);
181+
lines.push(`- [${title}](${routeToMdUrl(route)})`);
182+
183+
const content = await readMarkdownContent(docPath);
184+
fullLines.push(`### ${title}`, "", content.trimEnd(), "");
185+
}
186+
187+
lines.push("");
188+
fullLines.push("");
189+
}
190+
191+
const outputPath = path.join(distDir, "llms.txt");
192+
lines.push(
193+
"---",
194+
"",
195+
`For more comprehensive documentation, see [llms-full.txt](${baseUrl}/llms-full.txt)`
196+
);
197+
await fs.writeFile(outputPath, lines.join("\n").trimEnd() + "\n");
198+
199+
const fullOutputPath = path.join(distDir, "llms-full.txt");
200+
await fs.writeFile(fullOutputPath, fullLines.join("\n").trimEnd() + "\n");
201+
};
202+
203+
buildLlmsTxt().catch((error) => {
204+
console.error(error);
205+
process.exitCode = 1;
206+
});

0 commit comments

Comments
 (0)