Skip to content

Commit f85a18d

Browse files
authored
Add versioning to llms.txt (#3495)
* create versioned llms.txt files * Refactor LLM file generation for versioned docs Reworks the script to loop through all documentation versions and generate LLM files for each, removing redundant code and simplifying file output logic. The latest version now generates both llms.txt and llms-full.txt, while older versions generate llms-{version}.txt. Cleans up unused variables and functions, and improves maintainability. * Update generate-llm-files.ts * Update generate-llm-files.ts * run prettier * Refactor llms.txt file generation logic Replaces the buildLlmsTxtContent function with a more concise implementation using template constants. Extracts file writing logic into dedicated writeLlmsTxtFile and writeLlmsFullTxtFile functions for better modularity and clarity.
1 parent 25ceae1 commit f85a18d

File tree

2 files changed

+107
-66
lines changed

2 files changed

+107
-66
lines changed

web/.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,4 @@ yarn-error.log*
2121
.vercel
2222

2323
# LLM files
24-
llms.txt
25-
llms-full.txt
24+
llms*.txt

web/scripts/generate-llm-files.ts

Lines changed: 106 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,22 @@ import fs from "fs/promises";
77
import { globSync } from "glob";
88
import path from "path";
99

10-
import docsSidebarConfig from "../sidebars.js";
10+
import versionsJson from "../versions.json";
1111

1212
const SITE_ROOT = process.cwd();
1313
const STATIC_DIR = path.join(SITE_ROOT, "static/");
14-
const DOCS_DIR = path.join(SITE_ROOT, "docs/");
14+
const VERSIONED_DOCS_DIR = path.join(SITE_ROOT, "versioned_docs/");
15+
const VERSIONED_SIDEBARS_DIR = path.join(SITE_ROOT, "versioned_sidebars/");
1516
const BLOG_DIR = path.join(SITE_ROOT, "blog/");
1617
const GITHUB_RAW_BASE_URL =
1718
"https://raw.githubusercontent.com/wasp-lang/wasp/refs/heads/release/web/"; // Use the release branch
1819
const WASP_BASE_URL = "https://wasp.sh/";
19-
const LLM_FULL_FILENAME = "llms-full.txt";
20-
const LLM_OVERVIEW_FILENAME = "llms.txt";
21-
const LLMS_TXT_FILE_PATH = path.join(STATIC_DIR, LLM_OVERVIEW_FILENAME);
22-
const LLMS_FULL_TXT_FILE_PATH = path.join(STATIC_DIR, LLM_FULL_FILENAME);
2320
const CATEGORIES_TO_IGNORE = ["Miscellaneous"];
2421

25-
const OVERVIEW_INTRO_CONTENT = `
26-
# Wasp
27-
Wasp is a full-stack framework with batteries included for React, Node.js, and Prisma.
22+
const LLMS_TXT_INTRO = `# Wasp
23+
Wasp is a full-stack framework with batteries included for React, Node.js, and Prisma.`;
2824

29-
## Individual documentation sections and guides:
30-
`;
31-
const OVERVIEW_MISC_SECTION_CONTENT = `
32-
## Miscellaneous
25+
const LLMS_TXT_MISC = `## Miscellaneous
3326
- [Wasp Developer Discord](https://discord.com/invite/rzdnErX)
3427
- [Open SaaS -- Wasp's free, open-source SaaS boilerplate starter](https://opensaas.sh)
3528
`;
@@ -38,31 +31,68 @@ generateFiles();
3831

3932
/**
4033
* Main function to generate the LLM-friendly doc files.
41-
* It orchestrates the process by fetching and processing documentation and blog files,
42-
* and then writing the final output to the static directory.
34+
* Loops through all versioned docs and generates llms.txt files for each.
35+
* The latest version also gets llms-full.txt generated.
4336
*/
4437
async function generateFiles() {
4538
console.log("Starting LLM file generation...");
4639

47-
if (!Array.isArray(docsSidebarConfig.docs)) {
48-
throw new Error(
49-
"Sidebar configuration for docs is not an array. Please check the sidebars.ts file.",
50-
);
40+
const blogSectionContent = await processBlogFiles();
41+
const latestVersion = versionsJson[0];
42+
43+
for (const version of versionsJson) {
44+
const isLatest = version === latestVersion;
45+
console.log(`Processing version ${version}...`);
46+
47+
const docsDir = path.join(VERSIONED_DOCS_DIR, `version-${version}`);
48+
const sidebarItems = await loadVersionedSidebar(version);
49+
50+
const { overviewDocsSection, llmsFullTxtContent } =
51+
await processDocumentationFiles(sidebarItems, docsDir, version, {
52+
generateLlmsFullTxt: isLatest,
53+
});
54+
55+
const filename = isLatest ? "llms.txt" : `llms-${version}.txt`;
56+
await writeLlmsTxtFile(filename, overviewDocsSection, blogSectionContent);
57+
58+
if (isLatest) {
59+
await writeLlmsFullTxtFile(llmsFullTxtContent);
60+
}
5161
}
5262

53-
const { overviewDocsSection, fullConcatContent } =
54-
await processDocumentationFiles(docsSidebarConfig.docs);
55-
const blogSectionContent = await processBlogFiles();
63+
console.log("🎉 LLM files generation completed successfully.");
64+
}
5665

57-
const llmsTxtContent =
58-
OVERVIEW_INTRO_CONTENT +
59-
overviewDocsSection +
60-
blogSectionContent +
61-
OVERVIEW_MISC_SECTION_CONTENT;
62-
const llmsFullTxtContent = fullConcatContent;
66+
async function writeLlmsTxtFile(
67+
filename: string,
68+
overviewDocsSection: string,
69+
blogSectionContent: string,
70+
): Promise<void> {
71+
const content = buildLlmsTxtContent(overviewDocsSection, blogSectionContent);
72+
const outputPath = path.join(STATIC_DIR, filename);
73+
await fs.writeFile(outputPath, content, "utf8");
74+
console.log(` Generated: ${filename}`);
75+
}
76+
77+
async function writeLlmsFullTxtFile(content: string): Promise<void> {
78+
const outputPath = path.join(STATIC_DIR, "llms-full.txt");
79+
await fs.writeFile(outputPath, content.trim(), "utf8");
80+
console.log(` Generated: llms-full.txt`);
81+
}
6382

64-
await writeOutputFiles(llmsTxtContent, llmsFullTxtContent);
65-
console.log("🎉 LLM file generation complete.");
83+
/**
84+
* Assembles the overview content for llms.txt from its component sections.
85+
*/
86+
function buildLlmsTxtContent(
87+
overviewDocsSection: string,
88+
blogSectionContent: string,
89+
): string {
90+
return [
91+
LLMS_TXT_INTRO,
92+
overviewDocsSection,
93+
blogSectionContent,
94+
LLMS_TXT_MISC,
95+
].join("\n\n");
6696
}
6797

6898
/**
@@ -72,9 +102,12 @@ async function generateFiles() {
72102
*/
73103
async function processDocumentationFiles(
74104
docsSidebarItems: SidebarItemConfig[],
75-
): Promise<{ overviewDocsSection: string; fullConcatContent: string }> {
76-
let overviewDocsSection = "";
77-
let fullConcatContent = "";
105+
docsDir: string,
106+
version: string,
107+
{ generateLlmsFullTxt = false } = {},
108+
): Promise<{ overviewDocsSection: string; llmsFullTxtContent: string }> {
109+
let overviewDocsSection = `## Documentation Raw Text URLs -- Version ${version}:\n`;
110+
let llmsFullTxtContent = "";
78111

79112
const orderedDocIds = flattenSidebarItemsToDocIds(docsSidebarItems);
80113
console.log(
@@ -84,9 +117,13 @@ async function processDocumentationFiles(
84117
const sidebarOverviewStructure =
85118
getDocsSidebarCategoryStructure(docsSidebarItems);
86119

87-
const docIdToPathMap = buildDocIdToPathMap(DOCS_DIR);
120+
const docIdToPathMap = buildDocIdToPathMap(docsDir);
88121

89-
const docInfoMap = await populateDocInfoMap(orderedDocIds, docIdToPathMap);
122+
const docInfoMap = await populateDocInfoMap(
123+
orderedDocIds,
124+
docIdToPathMap,
125+
docsDir,
126+
);
90127

91128
for (const category of sidebarOverviewStructure) {
92129
overviewDocsSection += `${category.categoryLabel}\n`;
@@ -104,23 +141,24 @@ async function processDocumentationFiles(
104141
}
105142
}
106143

107-
// Build fullConcatContent using sidebar structure with proper heading hierarchy
108-
for (const category of sidebarOverviewStructure) {
109-
// Add category header as H1 and separator
110-
fullConcatContent += `# ${category.categoryLabel}\n\n`;
111-
112-
for (const docId of category.docIds) {
113-
if (docInfoMap.has(docId)) {
114-
const info = docInfoMap.get(docId);
115-
// Add document title as H2
116-
fullConcatContent += `## ${info.title}\n\n${info.processedBody}\n\n`;
144+
if (generateLlmsFullTxt) {
145+
for (const category of sidebarOverviewStructure) {
146+
// Add category header as H1 and separator
147+
llmsFullTxtContent += `# ${category.categoryLabel}\n\n`;
148+
149+
for (const docId of category.docIds) {
150+
if (docInfoMap.has(docId)) {
151+
const info = docInfoMap.get(docId);
152+
// Add document title as H2
153+
llmsFullTxtContent += `## ${info.title}\n\n${info.processedBody}\n\n`;
154+
}
117155
}
118-
}
119156

120-
// Add category separator
121-
fullConcatContent += `------\n\n`;
157+
// Add category separator
158+
llmsFullTxtContent += `------\n\n`;
159+
}
122160
}
123-
return { overviewDocsSection, fullConcatContent };
161+
return { overviewDocsSection, llmsFullTxtContent };
124162
}
125163

126164
/**
@@ -162,13 +200,14 @@ type DocDetails = {
162200
async function populateDocInfoMap(
163201
orderedDocIds: string[],
164202
docIdToPathMap: Map<string, string>,
203+
docsDir: string,
165204
): Promise<Map<string, DocDetails>> {
166205
const docInfoMap = new Map<string, DocDetails>();
167206
for (const docId of orderedDocIds) {
168207
const relativeDocPath = docIdToPathMap.get(docId);
169208

170209
if (relativeDocPath) {
171-
const absolutePath = path.join(DOCS_DIR, relativeDocPath);
210+
const absolutePath = path.join(docsDir, relativeDocPath);
172211
try {
173212
const rawContent = await fs.readFile(absolutePath, "utf8");
174213
const { attributes, body } = fm(rawContent);
@@ -412,23 +451,26 @@ function constructBlogUrl(filename: string): string {
412451
}
413452

414453
/**
415-
* Writes the LLM-friendly content to their respective output files.
454+
* Loads and parses a versioned sidebar configuration file.
455+
* Returns the docs array from the sidebar config.
416456
*/
417-
async function writeOutputFiles(
418-
llmsTxtContent: string,
419-
llmsFullTxtContent: string,
420-
): Promise<void> {
421-
console.log("Writing output files to static/ ...");
457+
async function loadVersionedSidebar(
458+
version: string,
459+
): Promise<SidebarItemConfig[]> {
460+
const sidebarPath = path.join(
461+
VERSIONED_SIDEBARS_DIR,
462+
`version-${version}-sidebars.json`,
463+
);
464+
const sidebarContent = await fs.readFile(sidebarPath, "utf8");
465+
const sidebarConfig = JSON.parse(sidebarContent);
422466

423-
await fs.writeFile(LLMS_TXT_FILE_PATH, llmsTxtContent.trim(), "utf8");
424-
console.log(`Generated overview file: ${LLMS_TXT_FILE_PATH}`);
467+
if (!Array.isArray(sidebarConfig.docs)) {
468+
throw new Error(
469+
`Versioned sidebar configuration for ${version} does not have a docs array.`,
470+
);
471+
}
425472

426-
await fs.writeFile(
427-
LLMS_FULL_TXT_FILE_PATH,
428-
llmsFullTxtContent.trim(),
429-
"utf8",
430-
);
431-
console.log(`Generated full concatenated file: ${LLMS_FULL_TXT_FILE_PATH}`);
473+
return sidebarConfig.docs;
432474
}
433475

434476
/**

0 commit comments

Comments
 (0)