Vite plugin to generate llms.txt files from tutorial content with composable architecture.
- 🧩 Composable: Mix and match scanners, processors, and formatters
- 🎯 Multiple Presets: TutorialKit and VitePress out of the box
- 🔧 Extensible: Create custom scanners, processors, and formatters
- ⚡ Fast: Powered by mdream for HTML→Markdown conversion
- 🎨 Flexible: Template system with variable substitution
- 📁 Smart Filtering: Glob patterns and predefined exclusions
- 🌲 Nested Output: Experimental depth support for multi-level llms.txt
pnpm add vite-plugin-llmstxt mdream// astro.config.ts
import { llmsPlugin } from 'vite-plugin-llmstxt'
export default defineConfig({
vite: {
plugins: [llmsPlugin()]
}
})import { llmsPlugin } from 'vite-plugin-llmstxt'
export default defineConfig({
vite: {
plugins: [
llmsPlugin({
preset: 'vitepress',
contentDir: 'docs',
stripHTML: true,
domain: 'https://example.com'
})
]
}
})The plugin uses a composable architecture with three main components:
- Scanner: Discovers content files
- Processor: Transforms markdown content
- Formatter: Generates output files
Presets combine scanner + processors + formatter + defaults:
llmsPlugin({ preset: 'tutorialkit' }) // or 'vitepress' or 'auto'Override any piece:
import { DocsFormatter, llmsPlugin, MdreamProcessor } from 'vite-plugin-llmstxt'
llmsPlugin({
preset: 'tutorialkit',
// Append custom processors
processors: [
new MdreamProcessor({ stripHTML: true }),
],
// Replace formatter
formatter: new DocsFormatter(),
// Template customization
template: '# {title}\n\n{toc}',
templateVars: { customVar: 'value' },
// Output control
domain: 'https://example.com',
generateIndex: true,
generateFull: true,
generateIndividual: true,
// Filtering
ignoreFiles: ['**/draft/**'],
excludeBlog: true,
excludeTeam: true,
})interface LLMPluginOptions {
// Preset (provides defaults)
preset?: 'tutorialkit' | 'vitepress' | 'auto' | Preset
// Override components
scanner?: Scanner
processors?: Processor[] | { mode: 'replace' | 'append', list: Processor[] }
formatter?: Formatter
// Content discovery
contentDir?: string
outputDir?: string
workDir?: string
// Filtering
ignoreFiles?: string[]
excludeUnnecessaryFiles?: boolean
excludeIndexPage?: boolean
excludeBlog?: boolean
excludeTeam?: boolean
// Processing
stripHTML?: boolean
injectLLMHint?: boolean
handleContentTags?: boolean
// Output
domain?: string
generateIndex?: boolean
generateFull?: boolean
generateIndividual?: boolean
// Template
template?: string
templateVars?: Record<string, string>
title?: string
description?: string
details?: string
// Advanced
experimental?: {
depth?: number // nested llms.txt generation
}
}import { TutorialKitScanner, VitePressScanner } from 'vite-plugin-llmstxt'
// Custom scanner
class MyScanner implements Scanner {
async scan(contentDir: string): Promise<RawFile[]> {
// Your content discovery logic
return []
}
watchPatterns(): string[] {
return ['**/*.md']
}
}Built-in processors:
MdreamProcessor: HTML stripping via mdreamFrontmatterProcessor: Extract/inject YAML frontmatterContentTagsProcessor: Handle<llm-only>and<llm-exclude>tagsSnippetsProcessor: Include external code snippetsHintsProcessor: Inject LLM hintsImageUrlsProcessor: Map image URLs
import { MdreamProcessor } from 'vite-plugin-llmstxt'
// Custom processor
class MyProcessor implements Processor {
name = 'my-processor'
async process(content: string, ctx: ProcessContext): Promise<string> {
// Transform content
return content.toUpperCase()
}
}import { DocsFormatter, TutorialFormatter } from 'vite-plugin-llmstxt'
// Custom formatter
class MyFormatter implements Formatter {
formatIndex(files: PreparedFile[], opts: FormatOptions): string {
return files.map(f => `- ${f.title}`).join('\n')
}
formatFull(files: PreparedFile[], opts: FormatOptions): string {
return files.map(f => f.content).join('\n---\n')
}
formatIndividual(file: PreparedFile, opts: FormatOptions): string {
return `# ${file.title}\n\n${file.content}`
}
}public/
├── llms.txt # Index with links
├── llms-full.txt # All content concatenated
└── tutorial/
├── intro.txt
├── basics.txt
└── ...
Use special tags to control LLM visibility:
<!-- Visible only in LLM output -->
<llm-only>
Technical details for LLMs only
</llm-only>
<!-- Hidden from LLM output -->
<llm-exclude>
Marketing content or human-only instructions
</llm-exclude>Customize the index format:
llmsPlugin({
template: `# {title}
> {description}
{details}
## Documentation
{toc}`,
templateVars: {
title: 'My Project',
description: 'Awesome docs',
details: 'Built with ❤️',
},
})Available variables:
{title}- Project title{description}- Description{details}- Additional details{toc}- Auto-generated table of contents- Custom variables via
templateVars
Generate nested llms.txt at multiple directory levels:
llmsPlugin({
experimental: {
depth: 2 // generates llms.txt in root + subdirectories
}
})Output:
dist/
├── llms.txt
├── llms-full.txt
├── guide/
│ ├── llms.txt
│ └── llms-full.txt
└── api/
├── llms.txt
└── llms-full.txt
// Append (default)
llmsPlugin({
processors: [new MyProcessor()]
})
// Replace
llmsPlugin({
processors: {
mode: 'replace',
list: [new MyProcessor()]
}
})pnpm install
pnpm build
pnpm test
pnpm typecheckMIT