Skip to content

Commit 1ae470a

Browse files
committed
Generate llms.txt file
1 parent 7668881 commit 1ae470a

2 files changed

Lines changed: 134 additions & 2 deletions

File tree

plugins/generate-llms-txt.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import fs from 'fs/promises';
2+
import fsSync from 'fs';
3+
import path from 'path';
4+
import { fileURLToPath } from 'url';
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
/**
10+
* Extract title and description from frontmatter
11+
* @param {string} content
12+
* @returns {{title: string, description: string, body: string}}
13+
*/
14+
function parseFrontmatter(content) {
15+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
16+
const match = content.match(frontmatterRegex);
17+
18+
if (!match) {
19+
return { title: '', description: '', body: content };
20+
}
21+
22+
const frontmatter = match[1];
23+
const body = match[2];
24+
25+
const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
26+
const descriptionMatch = frontmatter.match(/^description:\s*(.+)$/m);
27+
28+
return {
29+
title: titleMatch ? titleMatch[1].trim() : '',
30+
description: descriptionMatch ? descriptionMatch[1].trim() : '',
31+
body: body.trim()
32+
};
33+
}
34+
35+
/**
36+
* Read all markdown files from the guide directory
37+
* @param {string} guideDir
38+
* @returns {Promise<Array<{filename: string, content: string}>>}
39+
*/
40+
async function readMarkdownFiles(guideDir) {
41+
const files = await fs.readdir(guideDir);
42+
const markdownFiles = files.filter(file => file.endsWith('.md'));
43+
44+
return Promise.all(
45+
markdownFiles.map(async filename => {
46+
const filePath = path.join(guideDir, filename);
47+
const content = await fs.readFile(filePath, 'utf-8');
48+
return { filename, content };
49+
})
50+
);
51+
}
52+
53+
/**
54+
* Generate the llms.txt content
55+
* @param {Array<{filename: string, content: string}>} files
56+
* @returns {string}
57+
*/
58+
function generateLlmsTxt(files) {
59+
const header = `# Preact Documentation
60+
61+
This file contains comprehensive documentation for Preact v10, a fast 3kB alternative to React with the same modern API.
62+
63+
Generated on: ${new Date().toISOString()}
64+
Source: https://github.com/preactjs/preact-www
65+
66+
## Overview
67+
68+
Preact is a fast, lightweight alternative to React that provides the same modern API in a much smaller package. This documentation covers all aspects of Preact v10, including components, hooks, server-side rendering, TypeScript support, and more.
69+
70+
---
71+
72+
`;
73+
74+
let content = header;
75+
76+
files.sort((a, b) => a.filename.localeCompare(b.filename));
77+
78+
files.forEach(({ filename, content: fileContent }) => {
79+
const { title, description, body } = parseFrontmatter(fileContent);
80+
81+
content += `## ${title || filename.replace('.md', '')}\n\n`;
82+
83+
if (description) {
84+
content += `**Description:** ${description}\n\n`;
85+
}
86+
87+
content += `**Source:** content/en/guide/v10/${filename}\n\n`;
88+
content += `${body}\n\n`;
89+
content += `---\n\n`;
90+
});
91+
92+
return content;
93+
}
94+
95+
/**
96+
* Vite plugin to generate llms.txt from Preact documentation
97+
* @param {Object} options
98+
* @param {string} [options.guideDir] - Path to the guide directory
99+
* @param {string} [options.outputFile] - Path to the output file
100+
* @returns {import('vite').Plugin}
101+
*/
102+
export default function generateLlmsTxtPlugin(options = {}) {
103+
const guideDir =
104+
options.guideDir ||
105+
path.join(__dirname, '..', 'content', 'en', 'guide', 'v10');
106+
107+
return {
108+
name: 'generate-llms-txt',
109+
async buildStart() {
110+
try {
111+
if (!fsSync.existsSync(guideDir)) {
112+
this.warn(`Guide directory not found: ${guideDir}`);
113+
return;
114+
}
115+
116+
const files = await readMarkdownFiles(guideDir);
117+
const llmsTxtContent = generateLlmsTxt(files);
118+
119+
this.emitFile({
120+
type: 'asset',
121+
fileName: 'llms.txt',
122+
source: llmsTxtContent
123+
});
124+
} catch (error) {
125+
this.error(`Error generating llms.txt: ${error.message}`);
126+
}
127+
}
128+
};
129+
}

vite.config.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { netlifyPlugin } from './plugins/netlify.js';
88
import { spaFallbackMiddlewarePlugin } from './plugins/spa-fallback-middleware.js';
99
import { htmlRoutingMiddlewarePlugin } from './plugins/html-routing-middleware.js';
1010
import { rssFeedPlugin } from './plugins/rss-feed.js';
11+
import generateLlmsTxtPlugin from './plugins/generate-llms-txt.js';
1112

1213
// TODO: Should we do this for all routes, rely on discovery a bit less?
1314
import { tutorialRoutes } from './src/lib/route-utils.js';
@@ -44,7 +45,8 @@ export default defineConfig({
4445
{
4546
src: './content/**/*.md',
4647
dest: './',
47-
rename: (_name, _fileExtension, fullPath) => path.basename(fullPath).replace(/\.md$/, '.json'),
48+
rename: (_name, _fileExtension, fullPath) =>
49+
path.basename(fullPath).replace(/\.md$/, '.json'),
4850
transform: precompileMarkdown
4951
}
5052
],
@@ -74,6 +76,7 @@ export default defineConfig({
7476
netlifyPlugin(),
7577
spaFallbackMiddlewarePlugin(),
7678
htmlRoutingMiddlewarePlugin(),
77-
rssFeedPlugin()
79+
rssFeedPlugin(),
80+
generateLlmsTxtPlugin()
7881
]
7982
});

0 commit comments

Comments
 (0)