Skip to content

Commit f4b9177

Browse files
committed
merge main
2 parents 65852ac + 0d2357d commit f4b9177

File tree

4 files changed

+186
-0
lines changed

4 files changed

+186
-0
lines changed

astro.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'
44
import vue from '@astrojs/vue'
55
import tutorialkit from '@tutorialkit/astro'
66
import { defineConfig } from 'astro/config'
7+
import { llmsPlugin } from 'vite-plugin-llmstxt'
78

89
// Read package version at build time
910
const __filename = fileURLToPath(import.meta.url)
@@ -19,6 +20,7 @@ export default defineConfig({
1920
define: {
2021
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
2122
},
23+
plugins: [llmsPlugin({ preset: 'tutorialkit', outputDir: 'dist' })],
2224
},
2325
integrations: [
2426
tutorialkit({

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"pathe": "^2.0.3",
4747
"prettier-plugin-astro": "^0.14.1",
4848
"typescript": "^5.4.5",
49+
"vite-plugin-llmstxt": "^0.0.2",
4950
"wrangler": "^3.96.0"
5051
}
5152
}

pnpm-lock.yaml

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/plugins/llms-plugin.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2+
import { join } from 'node:path'
3+
import fg from 'fast-glob'
4+
5+
interface TutorialInfo {
6+
slug: string
7+
title: string
8+
dir: string
9+
lessons: LessonInfo[]
10+
}
11+
12+
interface LessonInfo {
13+
number: number
14+
title: string
15+
contentPath: string
16+
solutionDir: string | null
17+
}
18+
19+
async function scanTutorials(): Promise<TutorialInfo[]> {
20+
const tutorialDirs = await fg('src/content/tutorial/*-*/', { onlyDirectories: true })
21+
22+
const tutorials: TutorialInfo[] = []
23+
24+
for (const dir of tutorialDirs) {
25+
try {
26+
const metaPath = join(dir, 'meta.md')
27+
const metaContent = readFileSync(metaPath, 'utf-8')
28+
29+
const titleMatch = metaContent.match(/title:\s*(.+)/)
30+
const title = titleMatch ? titleMatch[1].trim() : 'Untitled'
31+
32+
const slug = dir.split('/').pop()!.replace(/^\d+-/, '')
33+
34+
const lessonDirs = await fg(`${dir}/*-*/`, { onlyDirectories: true })
35+
const lessons: LessonInfo[] = []
36+
37+
for (const lessonDir of lessonDirs) {
38+
const lessonName = lessonDir.split('/').pop()!
39+
const numberMatch = lessonName.match(/^(\d+)-/)
40+
const number = numberMatch ? Number.parseInt(numberMatch[1]) : 0
41+
42+
const contentPath = join(lessonDir, 'content.md')
43+
const solutionDir = join(lessonDir, '_solution')
44+
45+
const lessonContent = readFileSync(contentPath, 'utf-8')
46+
const lessonTitleMatch = lessonContent.match(/title:\s*(.+)/)
47+
const lessonTitle = lessonTitleMatch ? lessonTitleMatch[1].trim() : lessonName
48+
49+
lessons.push({
50+
number,
51+
title: lessonTitle,
52+
contentPath,
53+
solutionDir: await fg(`${solutionDir}/*`).then(files => files.length > 0 ? solutionDir : null),
54+
})
55+
}
56+
57+
lessons.sort((a, b) => a.number - b.number)
58+
59+
tutorials.push({ slug, title, dir, lessons })
60+
}
61+
catch (error) {
62+
console.warn(`[llms-plugin] ⚠️ Skipping ${dir}: ${error}`)
63+
continue
64+
}
65+
}
66+
67+
tutorials.sort((a, b) => a.dir.localeCompare(b.dir))
68+
69+
return tutorials
70+
}
71+
72+
async function generateTutorialFile(tutorial: TutorialInfo): Promise<string> {
73+
let content = `# ${tutorial.title}\n\n`
74+
75+
for (const lesson of tutorial.lessons) {
76+
content += `## Lesson ${lesson.number}: ${lesson.title}\n\n`
77+
78+
const lessonContent = readFileSync(lesson.contentPath, 'utf-8')
79+
const contentWithoutFrontmatter = lessonContent.replace(/^---\n[\s\S]*?\n---\n/, '')
80+
content += `${contentWithoutFrontmatter}\n\n`
81+
82+
if (lesson.solutionDir) {
83+
content += `### Solution Code\n\n`
84+
85+
const solutionFiles = (await fg(`${lesson.solutionDir}/*`)).sort()
86+
for (const file of solutionFiles) {
87+
const fileName = file.split('/').pop()!
88+
const fileContent = readFileSync(file, 'utf-8')
89+
const ext = fileName.split('.').pop()
90+
91+
content += `#### ${fileName}\n\`\`\`${ext}\n${fileContent}\n\`\`\`\n\n`
92+
}
93+
}
94+
95+
content += `---\n\n`
96+
}
97+
98+
return content
99+
}
100+
101+
function generateRootIndex(tutorials: TutorialInfo[]): string {
102+
let content = `# Nimiq Tutorials\n\n`
103+
content += `Learn to build on Nimiq blockchain through interactive tutorials.\n\n`
104+
content += `## Available Tutorials\n\n`
105+
106+
for (const tutorial of tutorials) {
107+
content += `- [${tutorial.title}](/tutorial/${tutorial.slug}.txt): ${tutorial.title}\n`
108+
}
109+
110+
content += `\n## Complete Documentation\n\n`
111+
content += `- [All Tutorials](/llms-full.txt): Full combined documentation\n`
112+
113+
return content
114+
}
115+
116+
async function generateAllFiles(outputDir = 'dist') {
117+
try {
118+
const tutorials = await scanTutorials()
119+
const tutorialDir = join(outputDir, 'tutorial')
120+
mkdirSync(tutorialDir, { recursive: true })
121+
122+
// Cache content to avoid regenerating for llms-full.txt
123+
const tutorialContents: string[] = []
124+
for (const tutorial of tutorials) {
125+
const content = await generateTutorialFile(tutorial)
126+
tutorialContents.push(content)
127+
const outputPath = join(tutorialDir, `${tutorial.slug}.txt`)
128+
writeFileSync(outputPath, content, 'utf-8')
129+
}
130+
131+
const rootIndex = generateRootIndex(tutorials)
132+
writeFileSync(join(outputDir, 'llms.txt'), rootIndex, 'utf-8')
133+
134+
const fullContent = tutorialContents.join(`\n\n${'='.repeat(80)}\n\n`)
135+
writeFileSync(join(outputDir, 'llms-full.txt'), fullContent, 'utf-8')
136+
137+
return tutorials.length
138+
}
139+
catch (error) {
140+
console.error('[llms-plugin] ❌ Error generating files:', error)
141+
throw error
142+
}
143+
}
144+
145+
export function llmsPlugin() {
146+
let config: any
147+
148+
return {
149+
name: 'llms-plugin',
150+
151+
configResolved(resolvedConfig: any) {
152+
config = resolvedConfig
153+
},
154+
155+
async buildEnd() {
156+
console.log('[llms-plugin] Generating llms.txt files...')
157+
const outputDir = config.build?.outDir || 'dist'
158+
const count = await generateAllFiles(outputDir)
159+
console.log(`[llms-plugin] ✅ Generated ${count} tutorial files`)
160+
},
161+
162+
configureServer(server: any) {
163+
server.watcher.on('change', async (path: string) => {
164+
if (path.includes('src/content/tutorial') && (path.endsWith('.md') || path.includes('/_solution/'))) {
165+
console.log('[llms-plugin] Tutorial content changed, regenerating...')
166+
await generateAllFiles('dist')
167+
console.log('[llms-plugin] ✅ Regenerated llms.txt files')
168+
}
169+
})
170+
},
171+
}
172+
}

0 commit comments

Comments
 (0)