format files

pull/4692/head
Okinea Dev 5 months ago
parent 673db7d4b6
commit 5c83259896
No known key found for this signature in database
GPG Key ID: 07944BC5E01E7B43

@ -122,7 +122,7 @@ export const shared = defineConfig({
), ),
firebase: 'logos:firebase' firebase: 'logos:firebase'
} }
}), })
] ]
}, },
llms: { llms: {

@ -243,7 +243,7 @@ export async function resolveSiteData(
scrollOffset: userConfig.scrollOffset ?? 134, scrollOffset: userConfig.scrollOffset ?? 134,
cleanUrls: !!userConfig.cleanUrls, cleanUrls: !!userConfig.cleanUrls,
contentProps: userConfig.contentProps, contentProps: userConfig.contentProps,
llms: userConfig.llms ?? false, llms: userConfig.llms ?? false
} }
} }

@ -6,10 +6,10 @@ import matter from 'gray-matter'
import type { DefaultTheme } from 'vitepress' import type { DefaultTheme } from 'vitepress'
import { defaultLLMsTxtTemplate } from '../constants' import { defaultLLMsTxtTemplate } from '../constants'
import type { import type {
LinksExtension, LinksExtension,
LlmstxtSettings, LlmstxtSettings,
PreparedFile, PreparedFile,
VitePressConfig, VitePressConfig
} from '../types' } from '../types'
import { generateTOC } from './toc' import { generateTOC } from './toc'
import { expandTemplate, extractTitle, generateMetadata } from './utils' import { expandTemplate, extractTitle, generateMetadata } from './utils'
@ -18,32 +18,32 @@ import { expandTemplate, extractTitle, generateMetadata } from './utils'
* Options for generating the `llms.txt` file. * Options for generating the `llms.txt` file.
*/ */
export interface GenerateLLMsTxtOptions { export interface GenerateLLMsTxtOptions {
/** Path to the main documentation file `index.md`.*/ /** Path to the main documentation file `index.md`.*/
indexMd: string indexMd: string
/** The source directory for the files. */ /** The source directory for the files. */
srcDir: VitePressConfig['vitepress']['srcDir'] srcDir: VitePressConfig['vitepress']['srcDir']
/** Template to use for generating `llms.txt`. */ /** Template to use for generating `llms.txt`. */
LLMsTxtTemplate?: LlmstxtSettings['customLLMsTxtTemplate'] LLMsTxtTemplate?: LlmstxtSettings['customLLMsTxtTemplate']
/** Template variables for `customLLMsTxtTemplate`. */ /** Template variables for `customLLMsTxtTemplate`. */
templateVariables?: LlmstxtSettings['customTemplateVariables'] templateVariables?: LlmstxtSettings['customTemplateVariables']
/** The VitePress configuration. */ /** The VitePress configuration. */
vitepressConfig?: VitePressConfig['vitepress']['userConfig'] vitepressConfig?: VitePressConfig['vitepress']['userConfig']
/** The base domain for the generated links. */ /** The base domain for the generated links. */
domain?: LlmstxtSettings['domain'] domain?: LlmstxtSettings['domain']
/** The link extension for generated links. */ /** The link extension for generated links. */
linksExtension?: LinksExtension linksExtension?: LinksExtension
/** Whether to use clean URLs (without the extension). */ /** Whether to use clean URLs (without the extension). */
cleanUrls?: VitePressConfig['cleanUrls'] cleanUrls?: VitePressConfig['cleanUrls']
/** Optional sidebar configuration for organizing the TOC. */ /** Optional sidebar configuration for organizing the TOC. */
sidebar?: DefaultTheme.Sidebar sidebar?: DefaultTheme.Sidebar
} }
/** /**
@ -69,77 +69,77 @@ export interface GenerateLLMsTxtOptions {
* @see https://llmstxt.org/#format * @see https://llmstxt.org/#format
*/ */
export async function generateLLMsTxt( export async function generateLLMsTxt(
preparedFiles: PreparedFile[], preparedFiles: PreparedFile[],
options: GenerateLLMsTxtOptions, options: GenerateLLMsTxtOptions
): Promise<string> { ): Promise<string> {
const { const {
indexMd, indexMd,
srcDir, srcDir,
LLMsTxtTemplate = defaultLLMsTxtTemplate, LLMsTxtTemplate = defaultLLMsTxtTemplate,
templateVariables = {}, templateVariables = {},
vitepressConfig, vitepressConfig,
domain, domain,
sidebar, sidebar,
linksExtension, linksExtension,
cleanUrls, cleanUrls
} = options } = options
// @ts-expect-error // @ts-expect-error
matter.clearCache() matter.clearCache()
const indexMdContent = await fs.readFile(indexMd, 'utf-8') const indexMdContent = await fs.readFile(indexMd, 'utf-8')
const indexMdFile = matter(indexMdContent as string) const indexMdFile = matter(indexMdContent as string)
templateVariables.title ??= templateVariables.title ??=
indexMdFile.data?.hero?.name || indexMdFile.data?.hero?.name ||
indexMdFile.data?.title || indexMdFile.data?.title ||
vitepressConfig?.title || vitepressConfig?.title ||
vitepressConfig?.titleTemplate || vitepressConfig?.titleTemplate ||
extractTitle(indexMdFile) || extractTitle(indexMdFile) ||
'LLMs Documentation' 'LLMs Documentation'
templateVariables.description ??= templateVariables.description ??=
indexMdFile.data?.hero?.text || indexMdFile.data?.hero?.text ||
vitepressConfig?.description || vitepressConfig?.description ||
indexMdFile?.data?.description || indexMdFile?.data?.description ||
indexMdFile.data?.titleTemplate indexMdFile.data?.titleTemplate
if (templateVariables.description) { if (templateVariables.description) {
templateVariables.description = `> ${templateVariables.description}` templateVariables.description = `> ${templateVariables.description}`
} }
templateVariables.details ??= templateVariables.details ??=
indexMdFile.data?.hero?.tagline || indexMdFile.data?.hero?.tagline ||
indexMdFile.data?.tagline || indexMdFile.data?.tagline ||
(!templateVariables.description && (!templateVariables.description &&
'This file contains links to all documentation sections.') 'This file contains links to all documentation sections.')
templateVariables.toc ??= await generateTOC(preparedFiles, { templateVariables.toc ??= await generateTOC(preparedFiles, {
srcDir, srcDir,
domain, domain,
sidebarConfig: sidebar || vitepressConfig?.themeConfig?.sidebar, sidebarConfig: sidebar || vitepressConfig?.themeConfig?.sidebar,
linksExtension, linksExtension,
cleanUrls, cleanUrls
}) })
return expandTemplate(LLMsTxtTemplate, templateVariables) return expandTemplate(LLMsTxtTemplate, templateVariables)
} }
/** /**
* Options for generating the `llms-full.txt` file. * Options for generating the `llms-full.txt` file.
*/ */
export interface GenerateLLMsFullTxtOptions { export interface GenerateLLMsFullTxtOptions {
/** The source directory for the files. */ /** The source directory for the files. */
srcDir: VitePressConfig['vitepress']['srcDir'] srcDir: VitePressConfig['vitepress']['srcDir']
/** The base domain for the generated links. */ /** The base domain for the generated links. */
domain?: LlmstxtSettings['domain'] domain?: LlmstxtSettings['domain']
/** The link extension for generated links. */ /** The link extension for generated links. */
linksExtension?: LinksExtension linksExtension?: LinksExtension
/** Whether to use clean URLs (without the extension). */ /** Whether to use clean URLs (without the extension). */
cleanUrls?: VitePressConfig['cleanUrls'] cleanUrls?: VitePressConfig['cleanUrls']
} }
/** /**
@ -150,26 +150,26 @@ export interface GenerateLLMsFullTxtOptions {
* @returns A string representing the full content of the LLMs.txt file. * @returns A string representing the full content of the LLMs.txt file.
*/ */
export function generateLLMsFullTxt( export function generateLLMsFullTxt(
preparedFiles: PreparedFile[], preparedFiles: PreparedFile[],
options: GenerateLLMsFullTxtOptions, options: GenerateLLMsFullTxtOptions
) { ) {
const { srcDir, domain, linksExtension, cleanUrls } = options const { srcDir, domain, linksExtension, cleanUrls } = options
const llmsFullTxtContent = preparedFiles const llmsFullTxtContent = preparedFiles
.map((preparedFile) => { .map((preparedFile) => {
const relativePath = path.relative(srcDir, preparedFile.path) const relativePath = path.relative(srcDir, preparedFile.path)
return matter.stringify( return matter.stringify(
preparedFile.file.content, preparedFile.file.content,
generateMetadata(preparedFile.file, { generateMetadata(preparedFile.file, {
domain, domain,
filePath: relativePath, filePath: relativePath,
linksExtension, linksExtension,
cleanUrls, cleanUrls
}), })
) )
}) })
.join('\n---\n\n') .join('\n---\n\n')
return llmsFullTxtContent return llmsFullTxtContent
} }

@ -8,36 +8,36 @@ const logPrefix = pc.blue('llmstxt') + pc.dim(' » ')
/** Logger object with standardized logging methods. */ /** Logger object with standardized logging methods. */
const log = { const log = {
/** /**
* Logs an informational message to the console. * Logs an informational message to the console.
* *
* @param message - The message to log. * @param message - The message to log.
*/ */
info: (message: string) => console.log(`${logPrefix} ${message}`), info: (message: string) => console.log(`${logPrefix} ${message}`),
/** /**
* Logs a success message to the console. * Logs a success message to the console.
* *
* @param message - The message to log. * @param message - The message to log.
*/ */
success: (message: string) => success: (message: string) =>
console.log(`${logPrefix}${pc.green('✓')} ${message}`), console.log(`${logPrefix}${pc.green('✓')} ${message}`),
/** /**
* Logs a warning message to the console. * Logs a warning message to the console.
* *
* @param message - The message to log. * @param message - The message to log.
*/ */
warn: (message: string) => warn: (message: string) =>
console.warn(`${logPrefix}${pc.yellow('⚠')} ${pc.yellow(message)}`), console.warn(`${logPrefix}${pc.yellow('⚠')} ${pc.yellow(message)}`),
/** /**
* Logs an error message to the console. * Logs an error message to the console.
* *
* @param message - The message to log. * @param message - The message to log.
*/ */
error: (message: string) => error: (message: string) =>
console.error(`${logPrefix}${pc.red('✗')} ${pc.red(message)}`), console.error(`${logPrefix}${pc.red('✗')} ${pc.red(message)}`)
} }
export default log export default log

@ -1,10 +1,10 @@
import path from 'node:path' import path from 'node:path'
import type { DefaultTheme } from 'vitepress' import type { DefaultTheme } from 'vitepress'
import type { import type {
LinksExtension, LinksExtension,
LlmstxtSettings, LlmstxtSettings,
PreparedFile, PreparedFile,
VitePressConfig, VitePressConfig
} from '../types' } from '../types'
import { generateLink, stripExtPosix } from './utils' import { generateLink, stripExtPosix } from './utils'
@ -19,14 +19,14 @@ import { generateLink, stripExtPosix } from './utils'
* @returns The formatted TOC entry as a Markdown list item. * @returns The formatted TOC entry as a Markdown list item.
*/ */
export const generateTOCLink = ( export const generateTOCLink = (
file: PreparedFile, file: PreparedFile,
domain: LlmstxtSettings['domain'], domain: LlmstxtSettings['domain'],
relativePath: string, relativePath: string,
extension?: LinksExtension, extension?: LinksExtension,
cleanUrls: VitePressConfig['cleanUrls'] = false, cleanUrls: VitePressConfig['cleanUrls'] = false
) => { ) => {
const description: string = file.file.data.description const description: string = file.file.data.description
return `- [${file.title}](${generateLink(stripExtPosix(relativePath), domain, extension ?? '.md', cleanUrls)})${description ? `: ${description.trim()}` : ''}\n` return `- [${file.title}](${generateLink(stripExtPosix(relativePath), domain, extension ?? '.md', cleanUrls)})${description ? `: ${description.trim()}` : ''}\n`
} }
/** /**
@ -36,23 +36,23 @@ export const generateTOCLink = (
* @returns Array of paths collected from the sidebar items. * @returns Array of paths collected from the sidebar items.
*/ */
function collectPathsFromSidebarItems( function collectPathsFromSidebarItems(
items: DefaultTheme.SidebarItem[], items: DefaultTheme.SidebarItem[]
): string[] { ): string[] {
const paths: string[] = [] const paths: string[] = []
for (const item of items) { for (const item of items) {
// Add the current item's path if it exists // Add the current item's path if it exists
if (item.link) { if (item.link) {
paths.push(item.link) paths.push(item.link)
} }
// Recursively add paths from nested items // Recursively add paths from nested items
if (item.items && Array.isArray(item.items)) { if (item.items && Array.isArray(item.items)) {
paths.push(...collectPathsFromSidebarItems(item.items)) paths.push(...collectPathsFromSidebarItems(item.items))
} }
} }
return paths return paths
} }
/** /**
@ -62,13 +62,13 @@ function collectPathsFromSidebarItems(
* @returns Normalized link path for consistent comparison. * @returns Normalized link path for consistent comparison.
*/ */
export function normalizeLinkPath(link: string): string { export function normalizeLinkPath(link: string): string {
const normalizedPath = stripExtPosix(link) const normalizedPath = stripExtPosix(link)
if (path.basename(normalizedPath) === 'index') { if (path.basename(normalizedPath) === 'index') {
return path.dirname(normalizedPath) return path.dirname(normalizedPath)
} }
return normalizedPath return normalizedPath
} }
/** /**
@ -79,13 +79,13 @@ export function normalizeLinkPath(link: string): string {
* @returns True if paths match, false otherwise * @returns True if paths match, false otherwise
*/ */
export function isPathMatch(filePath: string, sidebarPath: string): boolean { export function isPathMatch(filePath: string, sidebarPath: string): boolean {
const normalizedFilePath = normalizeLinkPath(filePath) const normalizedFilePath = normalizeLinkPath(filePath)
const normalizedSidebarPath = normalizeLinkPath(sidebarPath) const normalizedSidebarPath = normalizeLinkPath(sidebarPath)
return ( return (
normalizedFilePath === normalizedSidebarPath || normalizedFilePath === normalizedSidebarPath ||
normalizedFilePath === `${normalizedSidebarPath}.md` normalizedFilePath === `${normalizedSidebarPath}.md`
) )
} }
/** /**
@ -100,86 +100,86 @@ export function isPathMatch(filePath: string, sidebarPath: string): boolean {
* @returns A string representing the formatted section of the TOC * @returns A string representing the formatted section of the TOC
*/ */
async function processSidebarSection( async function processSidebarSection(
section: DefaultTheme.SidebarItem, section: DefaultTheme.SidebarItem,
preparedFiles: PreparedFile[], preparedFiles: PreparedFile[],
srcDir: VitePressConfig['vitepress']['srcDir'], srcDir: VitePressConfig['vitepress']['srcDir'],
domain?: LlmstxtSettings['domain'], domain?: LlmstxtSettings['domain'],
linksExtension?: LinksExtension, linksExtension?: LinksExtension,
cleanUrls?: VitePressConfig['cleanUrls'], cleanUrls?: VitePressConfig['cleanUrls'],
depth = 3, depth = 3
): Promise<string> { ): Promise<string> {
let sectionTOC = '' let sectionTOC = ''
// Add section header only if it has text and is not just a link container // Add section header only if it has text and is not just a link container
if (section.text) { if (section.text) {
sectionTOC += `${'#'.repeat(depth)} ${section.text}\n\n` sectionTOC += `${'#'.repeat(depth)} ${section.text}\n\n`
} }
// Process items in this section // Process items in this section
if (section.items && Array.isArray(section.items)) { if (section.items && Array.isArray(section.items)) {
const linkItems: string[] = [] const linkItems: string[] = []
const nestedSections: string[] = [] const nestedSections: string[] = []
// First pass: separate link items and nested sections // First pass: separate link items and nested sections
await Promise.all( await Promise.all(
section.items.map(async (item) => { section.items.map(async (item) => {
// Process nested sections // Process nested sections
if (item.items && item.items.length > 0) { if (item.items && item.items.length > 0) {
const processedSection = await processSidebarSection( const processedSection = await processSidebarSection(
item, item,
preparedFiles, preparedFiles,
srcDir, srcDir,
domain, domain,
linksExtension, linksExtension,
cleanUrls, cleanUrls,
// Increase depth for nested sections to maintain proper heading levels // Increase depth for nested sections to maintain proper heading levels
depth + 1, depth + 1
) )
nestedSections.push(processedSection) nestedSections.push(processedSection)
} }
// Process link items // Process link items
else if (item.link) { else if (item.link) {
// Normalize the link for matching // Normalize the link for matching
const normalizedItemLink = normalizeLinkPath(item.link) const normalizedItemLink = normalizeLinkPath(item.link)
const matchingFile = preparedFiles.find((file) => { const matchingFile = preparedFiles.find((file) => {
const relativePath = `/${stripExtPosix(path.relative(srcDir, file.path))}` const relativePath = `/${stripExtPosix(path.relative(srcDir, file.path))}`
return isPathMatch(relativePath, normalizedItemLink) return isPathMatch(relativePath, normalizedItemLink)
}) })
if (matchingFile) { if (matchingFile) {
const relativePath = path.relative(srcDir, matchingFile.path) const relativePath = path.relative(srcDir, matchingFile.path)
linkItems.push( linkItems.push(
generateTOCLink( generateTOCLink(
matchingFile, matchingFile,
domain, domain,
relativePath, relativePath,
linksExtension, linksExtension,
cleanUrls, cleanUrls
), )
) )
} }
} }
}), })
) )
// Add link items if any // Add link items if any
if (linkItems.length > 0) { if (linkItems.length > 0) {
sectionTOC += linkItems.join('') sectionTOC += linkItems.join('')
} }
// Add a blank line before nested sections if we have link items // Add a blank line before nested sections if we have link items
if (linkItems.length > 0 && nestedSections.length > 0) { if (linkItems.length > 0 && nestedSections.length > 0) {
sectionTOC += '\n' sectionTOC += '\n'
} }
// Add nested sections with appropriate spacing // Add nested sections with appropriate spacing
if (nestedSections.length > 0) { if (nestedSections.length > 0) {
sectionTOC += nestedSections.join('\n') sectionTOC += nestedSections.join('\n')
} }
} }
return sectionTOC return sectionTOC
} }
/** /**
@ -189,46 +189,46 @@ async function processSidebarSection(
* @returns An array of sidebar items. * @returns An array of sidebar items.
*/ */
function flattenSidebarConfig( function flattenSidebarConfig(
sidebarConfig: DefaultTheme.Sidebar, sidebarConfig: DefaultTheme.Sidebar
): DefaultTheme.SidebarItem[] { ): DefaultTheme.SidebarItem[] {
// If it's already an array, return as is // If it's already an array, return as is
if (Array.isArray(sidebarConfig)) { if (Array.isArray(sidebarConfig)) {
return sidebarConfig return sidebarConfig
} }
// If it's an object with path keys, flatten it // If it's an object with path keys, flatten it
if (typeof sidebarConfig === 'object') { if (typeof sidebarConfig === 'object') {
return Object.values(sidebarConfig).flat() return Object.values(sidebarConfig).flat()
} }
// If it's neither, return an empty array // If it's neither, return an empty array
return [] return []
} }
/** /**
* Options for generating a Table of Contents (TOC). * Options for generating a Table of Contents (TOC).
*/ */
export interface GenerateTOCOptions { export interface GenerateTOCOptions {
/** /**
* The VitePress source directory. * The VitePress source directory.
*/ */
srcDir: VitePressConfig['vitepress']['srcDir'] srcDir: VitePressConfig['vitepress']['srcDir']
/** /**
* Optional domain to prefix URLs with. * Optional domain to prefix URLs with.
*/ */
domain?: LlmstxtSettings['domain'] domain?: LlmstxtSettings['domain']
/** /**
* Optional VitePress sidebar configuration. * Optional VitePress sidebar configuration.
*/ */
sidebarConfig?: DefaultTheme.Sidebar sidebarConfig?: DefaultTheme.Sidebar
/** The link extension for generated links. */ /** The link extension for generated links. */
linksExtension?: LinksExtension linksExtension?: LinksExtension
/** Whether to use clean URLs (without the extension). */ /** Whether to use clean URLs (without the extension). */
cleanUrls?: VitePressConfig['cleanUrls'] cleanUrls?: VitePressConfig['cleanUrls']
} }
/** /**
@ -244,66 +244,66 @@ export interface GenerateTOCOptions {
* @returns A string representing the formatted Table of Contents. * @returns A string representing the formatted Table of Contents.
*/ */
export async function generateTOC( export async function generateTOC(
preparedFiles: PreparedFile[], preparedFiles: PreparedFile[],
options: GenerateTOCOptions, options: GenerateTOCOptions
): Promise<string> { ): Promise<string> {
const { srcDir, domain, sidebarConfig, linksExtension, cleanUrls } = options const { srcDir, domain, sidebarConfig, linksExtension, cleanUrls } = options
let tableOfContent = '' let tableOfContent = ''
let filesToProcess = preparedFiles let filesToProcess = preparedFiles
// If sidebar configuration exists // If sidebar configuration exists
if (sidebarConfig) { if (sidebarConfig) {
// Flatten sidebar config if it's an object with path keys // Flatten sidebar config if it's an object with path keys
const flattenedSidebarConfig = flattenSidebarConfig(sidebarConfig) const flattenedSidebarConfig = flattenSidebarConfig(sidebarConfig)
// Process each top-level section in the flattened sidebar // Process each top-level section in the flattened sidebar
if (flattenedSidebarConfig.length > 0) { if (flattenedSidebarConfig.length > 0) {
for (const section of flattenedSidebarConfig) { for (const section of flattenedSidebarConfig) {
tableOfContent += await processSidebarSection( tableOfContent += await processSidebarSection(
section, section,
filesToProcess, filesToProcess,
srcDir, srcDir,
domain, domain,
linksExtension, linksExtension,
cleanUrls, cleanUrls
) )
// tableOfContent = `${tableOfContent.trimEnd()}\n\n` // tableOfContent = `${tableOfContent.trimEnd()}\n\n`
tableOfContent += '\n' tableOfContent += '\n'
} }
// Find files that didn't match any section // Find files that didn't match any section
const allSidebarPaths = collectPathsFromSidebarItems( const allSidebarPaths = collectPathsFromSidebarItems(
flattenedSidebarConfig, flattenedSidebarConfig
) )
const unsortedFiles = filesToProcess.filter((file) => { const unsortedFiles = filesToProcess.filter((file) => {
const relativePath = `/${stripExtPosix(path.relative(srcDir, file.path))}` const relativePath = `/${stripExtPosix(path.relative(srcDir, file.path))}`
return !allSidebarPaths.some((sidebarPath) => return !allSidebarPaths.some((sidebarPath) =>
isPathMatch(relativePath, sidebarPath), isPathMatch(relativePath, sidebarPath)
) )
}) })
// Add files that didn't match any section // Add files that didn't match any section
if (unsortedFiles.length > 0) { if (unsortedFiles.length > 0) {
tableOfContent += '### Other\n\n' tableOfContent += '### Other\n\n'
filesToProcess = unsortedFiles filesToProcess = unsortedFiles
} }
} }
} }
const tocEntries: string[] = [] const tocEntries: string[] = []
await Promise.all( await Promise.all(
filesToProcess.map(async (file) => { filesToProcess.map(async (file) => {
const relativePath = path.relative(srcDir, file.path) const relativePath = path.relative(srcDir, file.path)
tocEntries.push( tocEntries.push(
generateTOCLink(file, domain, relativePath, linksExtension, cleanUrls), generateTOCLink(file, domain, relativePath, linksExtension, cleanUrls)
) )
}), })
) )
tableOfContent += tocEntries.join('') tableOfContent += tocEntries.join('')
return tableOfContent return tableOfContent
} }

@ -12,8 +12,8 @@ import type { LinksExtension, LlmstxtSettings, VitePressConfig } from '../types'
* @returns An object containing the directory and file name. * @returns An object containing the directory and file name.
*/ */
export const splitDirAndFile = (filepath: string) => ({ export const splitDirAndFile = (filepath: string) => ({
dir: path.dirname(filepath), dir: path.dirname(filepath),
file: path.basename(filepath), file: path.basename(filepath)
}) })
/** /**
@ -23,9 +23,9 @@ export const splitDirAndFile = (filepath: string) => ({
* @returns The filename without the extension. * @returns The filename without the extension.
*/ */
export const stripExt = (filepath: string) => { export const stripExt = (filepath: string) => {
const { dir, file } = splitDirAndFile(filepath) const { dir, file } = splitDirAndFile(filepath)
return path.join(dir, path.basename(file, path.extname(file))) return path.join(dir, path.basename(file, path.extname(file)))
} }
/** /**
@ -35,9 +35,9 @@ export const stripExt = (filepath: string) => {
* @returns The filename without the extension in POSIX format. * @returns The filename without the extension in POSIX format.
*/ */
export const stripExtPosix = (filepath: string) => { export const stripExtPosix = (filepath: string) => {
const { dir, file } = splitDirAndFile(filepath) const { dir, file } = splitDirAndFile(filepath)
return path.posix.join(dir, path.basename(file, path.extname(file))) return path.posix.join(dir, path.basename(file, path.extname(file)))
} }
/** /**
@ -47,13 +47,13 @@ export const stripExtPosix = (filepath: string) => {
* @returns The extracted title, or `undefined` if no title is found. * @returns The extracted title, or `undefined` if no title is found.
*/ */
export function extractTitle(file: GrayMatterFile<Input>): string { export function extractTitle(file: GrayMatterFile<Input>): string {
const titleFromFrontmatter = file.data?.title || file.data?.titleTemplate const titleFromFrontmatter = file.data?.title || file.data?.titleTemplate
let titleFromMarkdown: string | undefined let titleFromMarkdown: string | undefined
if (!titleFromFrontmatter) { if (!titleFromFrontmatter) {
titleFromMarkdown = markdownTitle(file.content) titleFromMarkdown = markdownTitle(file.content)
} }
return titleFromFrontmatter || titleFromMarkdown return titleFromFrontmatter || titleFromMarkdown
} }
/** /**
@ -69,7 +69,7 @@ export function extractTitle(file: GrayMatterFile<Input>): string {
* ``` * ```
*/ */
const templateVariable = (key: string) => const templateVariable = (key: string) =>
new RegExp(`(\\n\\s*\\n)?\\{${key}\\}`, 'gi') new RegExp(`(\\n\\s*\\n)?\\{${key}\\}`, 'gi')
/** /**
* Replaces occurrences of a template variable `{variable}` in a given content string with a provided value. * Replaces occurrences of a template variable `{variable}` in a given content string with a provided value.
@ -89,15 +89,15 @@ const templateVariable = (key: string) =>
* ``` * ```
*/ */
export function replaceTemplateVariable( export function replaceTemplateVariable(
content: string, content: string,
variable: string, variable: string,
value: string | undefined, value: string | undefined,
fallback?: string, fallback?: string
) { ) {
return content.replace(templateVariable(variable), (_, prefix) => { return content.replace(templateVariable(variable), (_, prefix) => {
const val = value?.length ? value : fallback?.length ? fallback : '' const val = value?.length ? value : fallback?.length ? fallback : ''
return val ? `${prefix ? '\n\n' : ''}${val}` : '' return val ? `${prefix ? '\n\n' : ''}${val}` : ''
}) })
} }
/** /**
@ -116,13 +116,13 @@ export function replaceTemplateVariable(
* ``` * ```
*/ */
export const expandTemplate = ( export const expandTemplate = (
template: string, template: string,
variables: Record<string, string | undefined>, variables: Record<string, string | undefined>
) => { ) => {
return Object.entries(variables).reduce( return Object.entries(variables).reduce(
(result, [key, value]) => replaceTemplateVariable(result, key, value), (result, [key, value]) => replaceTemplateVariable(result, key, value),
template, template
) )
} }
/** /**
@ -134,32 +134,32 @@ export const expandTemplate = (
* @returns The generated link * @returns The generated link
*/ */
export const generateLink = ( export const generateLink = (
path: string, path: string,
domain?: string, domain?: string,
extension?: LinksExtension, extension?: LinksExtension,
cleanUrls?: VitePressConfig['cleanUrls'], cleanUrls?: VitePressConfig['cleanUrls']
) => ) =>
expandTemplate('{domain}/{path}{extension}', { expandTemplate('{domain}/{path}{extension}', {
domain: domain || '', domain: domain || '',
path, path,
extension: cleanUrls ? '' : extension, extension: cleanUrls ? '' : extension
}) })
/** /**
* Options for generating metadata for markdown files. * Options for generating metadata for markdown files.
*/ */
export interface GenerateMetadataOptions { export interface GenerateMetadataOptions {
/** Optional domain name to prepend to the URL. */ /** Optional domain name to prepend to the URL. */
domain?: LlmstxtSettings['domain'] domain?: LlmstxtSettings['domain']
/** Path to the file relative to the content root. */ /** Path to the file relative to the content root. */
filePath: string filePath: string
/** The link extension for generated links. */ /** The link extension for generated links. */
linksExtension?: LinksExtension linksExtension?: LinksExtension
/** Whether to use clean URLs (without the extension). */ /** Whether to use clean URLs (without the extension). */
cleanUrls?: VitePressConfig['cleanUrls'] cleanUrls?: VitePressConfig['cleanUrls']
} }
/** /**
@ -174,24 +174,24 @@ export interface GenerateMetadataOptions {
* // Returns { url: 'https://example.com/docs/guide.md', description: 'A guide' } * // Returns { url: 'https://example.com/docs/guide.md', description: 'A guide' }
*/ */
export function generateMetadata<GrayMatter extends GrayMatterFile<Input>>( export function generateMetadata<GrayMatter extends GrayMatterFile<Input>>(
sourceFile: GrayMatter, sourceFile: GrayMatter,
options: GenerateMetadataOptions, options: GenerateMetadataOptions
) { ) {
const { domain, filePath, linksExtension, cleanUrls } = options const { domain, filePath, linksExtension, cleanUrls } = options
const frontmatterMetadata: Record<string, string> = {} const frontmatterMetadata: Record<string, string> = {}
frontmatterMetadata.url = generateLink( frontmatterMetadata.url = generateLink(
stripExtPosix(filePath), stripExtPosix(filePath),
domain, domain,
linksExtension ?? '.md', linksExtension ?? '.md',
cleanUrls, cleanUrls
) )
if (sourceFile.data?.description?.length) { if (sourceFile.data?.description?.length) {
frontmatterMetadata.description = sourceFile.data?.description frontmatterMetadata.description = sourceFile.data?.description
} }
return frontmatterMetadata return frontmatterMetadata
} }
/** /**
@ -204,4 +204,4 @@ export function generateMetadata<GrayMatter extends GrayMatterFile<Input>>(
* @returns A human-readable size string (e.g., "1.2 KB", "500 B"). * @returns A human-readable size string (e.g., "1.2 KB", "500 B").
*/ */
export const getHumanReadableSizeOf = (string: string) => export const getHumanReadableSizeOf = (string: string) =>
byteSize(new Blob([string]).size).toString() byteSize(new Blob([string]).size).toString()

@ -17,17 +17,17 @@ import { defaultLLMsTxtTemplate } from './constants'
import { generateLLMsFullTxt, generateLLMsTxt } from './helpers/index' import { generateLLMsFullTxt, generateLLMsTxt } from './helpers/index'
import log from './helpers/logger' import log from './helpers/logger'
import { import {
expandTemplate, expandTemplate,
extractTitle, extractTitle,
generateMetadata, generateMetadata,
getHumanReadableSizeOf, getHumanReadableSizeOf,
stripExt, stripExt
} from './helpers/utils' } from './helpers/utils'
import type { import type {
CustomTemplateVariables, CustomTemplateVariables,
LlmstxtSettings, LlmstxtSettings,
PreparedFile, PreparedFile,
VitePressConfig, VitePressConfig
} from './types' } from './types'
const PLUGIN_NAME = 'llmstxt' const PLUGIN_NAME = 'llmstxt'
@ -42,324 +42,324 @@ const PLUGIN_NAME = 'llmstxt'
* @see https://llmstxt.org/ * @see https://llmstxt.org/
*/ */
export default function llmstxt(userSettings: LlmstxtSettings = {}): Plugin { export default function llmstxt(userSettings: LlmstxtSettings = {}): Plugin {
// Create a settings object with defaults explicitly merged // Create a settings object with defaults explicitly merged
const settings: Omit<LlmstxtSettings, 'workDir'> & { workDir: string } = { const settings: Omit<LlmstxtSettings, 'workDir'> & { workDir: string } = {
generateLLMsTxt: true, generateLLMsTxt: true,
generateLLMsFullTxt: true, generateLLMsFullTxt: true,
generateLLMFriendlyDocsForEachPage: true, generateLLMFriendlyDocsForEachPage: true,
ignoreFiles: [], ignoreFiles: [],
workDir: undefined as unknown as string, workDir: undefined as unknown as string,
stripHTML: true, stripHTML: true,
...userSettings, ...userSettings
} }
// Store the resolved Vite config // Store the resolved Vite config
let config: VitePressConfig let config: VitePressConfig
// Set to store all markdown file paths // Set to store all markdown file paths
const mdFiles: Set<string> = new Set() const mdFiles: Set<string> = new Set()
// Flag to identify which build we're in // Flag to identify which build we're in
let isSsrBuild = false let isSsrBuild = false
return { return {
name: PLUGIN_NAME, name: PLUGIN_NAME,
/** Resolves the Vite configuration and sets up the working directory. */ /** Resolves the Vite configuration and sets up the working directory. */
configResolved(resolvedConfig) { configResolved(resolvedConfig) {
config = resolvedConfig as VitePressConfig config = resolvedConfig as VitePressConfig
if (settings.workDir) { if (settings.workDir) {
settings.workDir = path.resolve( settings.workDir = path.resolve(
config.vitepress.srcDir, config.vitepress.srcDir,
settings.workDir as string, settings.workDir as string
) )
} else { } else {
settings.workDir = config.vitepress.srcDir settings.workDir = config.vitepress.srcDir
} }
// Detect if this is the SSR build // Detect if this is the SSR build
isSsrBuild = !!resolvedConfig.build?.ssr isSsrBuild = !!resolvedConfig.build?.ssr
log.info( log.info(
`${pc.bold(PLUGIN_NAME)} initialized ${isSsrBuild ? pc.dim('(SSR build)') : pc.dim('(client build)')} with workDir: ${pc.cyan(settings.workDir as string)}`, `${pc.bold(PLUGIN_NAME)} initialized ${isSsrBuild ? pc.dim('(SSR build)') : pc.dim('(client build)')} with workDir: ${pc.cyan(settings.workDir as string)}`
) )
}, },
/** Configures the development server to handle `llms.txt` and markdown files for LLMs. */ /** Configures the development server to handle `llms.txt` and markdown files for LLMs. */
async configureServer(server: ViteDevServer) { async configureServer(server: ViteDevServer) {
log.info('Dev server configured for serving plain text docs for LLMs') log.info('Dev server configured for serving plain text docs for LLMs')
server.middlewares.use(async (req, res, next) => { server.middlewares.use(async (req, res, next) => {
if (req.url?.endsWith('.md') || req.url?.endsWith('.txt')) { if (req.url?.endsWith('.md') || req.url?.endsWith('.txt')) {
try { try {
// Try to read and serve the markdown file // Try to read and serve the markdown file
const filePath = path.resolve( const filePath = path.resolve(
config.vitepress?.outDir ?? 'dist', config.vitepress?.outDir ?? 'dist',
`${stripExt(req.url)}.md`, `${stripExt(req.url)}.md`
) )
const content = await fs.readFile(filePath, 'utf-8') const content = await fs.readFile(filePath, 'utf-8')
res.setHeader('Content-Type', 'text/plain; charset=utf-8') res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end(content) res.end(content)
return return
} catch (e) { } catch (e) {
// If file doesn't exist or can't be read, continue to next middleware // If file doesn't exist or can't be read, continue to next middleware
log.warn(`Failed to return ${pc.cyan(req.url)}: File not found`) log.warn(`Failed to return ${pc.cyan(req.url)}: File not found`)
next() next()
} }
} }
// Pass to next middleware if not handled // Pass to next middleware if not handled
next() next()
}) })
}, },
/** /**
* Resets the collection of markdown files when the build starts. * Resets the collection of markdown files when the build starts.
* This ensures we don't include stale data from previous builds. * This ensures we don't include stale data from previous builds.
*/ */
buildStart() { buildStart() {
mdFiles.clear() mdFiles.clear()
log.info('Build started, file collection cleared') log.info('Build started, file collection cleared')
}, },
/** /**
* Processes each file that Vite transforms and collects markdown files. * Processes each file that Vite transforms and collects markdown files.
* *
* @param _ - The file content (not used). * @param _ - The file content (not used).
* @param id - The file identifier (path). * @param id - The file identifier (path).
* @returns null if the file is processed, otherwise returns the original content. * @returns null if the file is processed, otherwise returns the original content.
*/ */
async transform(_, id: string) { async transform(_, id: string) {
if (!id.endsWith('.md')) { if (!id.endsWith('.md')) {
return null return null
} }
// Skip files outside workDir if it's configured // Skip files outside workDir if it's configured
if (!id.startsWith(settings.workDir as string)) { if (!id.startsWith(settings.workDir as string)) {
return null return null
} }
if (settings.ignoreFiles?.length) { if (settings.ignoreFiles?.length) {
const shouldIgnore = await Promise.all( const shouldIgnore = await Promise.all(
settings.ignoreFiles.map(async (pattern) => { settings.ignoreFiles.map(async (pattern) => {
if (typeof pattern === 'string') { if (typeof pattern === 'string') {
return await Promise.resolve( return await Promise.resolve(
minimatch( minimatch(
path.relative(settings.workDir as string, id), path.relative(settings.workDir as string, id),
pattern, pattern
), )
) )
} }
return false return false
}), })
) )
if (shouldIgnore.some((result) => result === true)) { if (shouldIgnore.some((result) => result === true)) {
return null return null
} }
} }
// Add markdown file path to our collection // Add markdown file path to our collection
mdFiles.add(id) mdFiles.add(id)
// Return null to avoid modifying the file // Return null to avoid modifying the file
return null return null
}, },
/** /**
* Runs only in the client build (not SSR) after completion. * Runs only in the client build (not SSR) after completion.
* This ensures the processing happens exactly once. * This ensures the processing happens exactly once.
*/ */
async generateBundle() { async generateBundle() {
// Skip processing during SSR build // Skip processing during SSR build
if (isSsrBuild) { if (isSsrBuild) {
log.info('Skipping LLMs docs generation in SSR build') log.info('Skipping LLMs docs generation in SSR build')
return return
} }
const outDir = config.vitepress?.outDir ?? 'dist' const outDir = config.vitepress?.outDir ?? 'dist'
// Create output directory if it doesn't exist // Create output directory if it doesn't exist
try { try {
await fs.access(outDir) await fs.access(outDir)
} catch { } catch {
log.info(`Creating output directory: ${pc.cyan(outDir)}`) log.info(`Creating output directory: ${pc.cyan(outDir)}`)
await fs.mkdir(outDir, { recursive: true }) await fs.mkdir(outDir, { recursive: true })
} }
const mdFilesList = Array.from(mdFiles) const mdFilesList = Array.from(mdFiles)
const fileCount = mdFilesList.length const fileCount = mdFilesList.length
// Skip if no files found // Skip if no files found
if (fileCount === 0) { if (fileCount === 0) {
log.warn( log.warn(
`No markdown files found to process. Check your \`${pc.bold('workDir')}\` and \`${pc.bold('ignoreFiles')}\` settings.`, `No markdown files found to process. Check your \`${pc.bold('workDir')}\` and \`${pc.bold('ignoreFiles')}\` settings.`
) )
return return
} }
log.info( log.info(
`Processing ${pc.bold(fileCount.toString())} markdown files from ${pc.cyan(settings.workDir)}`, `Processing ${pc.bold(fileCount.toString())} markdown files from ${pc.cyan(settings.workDir)}`
) )
const preparedFiles: PreparedFile[] = await Promise.all( const preparedFiles: PreparedFile[] = await Promise.all(
mdFilesList.map(async (file) => { mdFilesList.map(async (file) => {
const content = await fs.readFile(file, 'utf-8') const content = await fs.readFile(file, 'utf-8')
let mdFile: matter.GrayMatterFile<Input> let mdFile: matter.GrayMatterFile<Input>
if (settings.stripHTML) { if (settings.stripHTML) {
const cleanedMarkdown = await remark() const cleanedMarkdown = await remark()
.use(remarkFrontmatter) .use(remarkFrontmatter)
.use(() => { .use(() => {
// Strip HTML tags // Strip HTML tags
return (tree) => { return (tree) => {
remove(tree, { type: 'html' }) remove(tree, { type: 'html' })
return tree return tree
} }
}) })
.process(content) .process(content)
mdFile = matter(String(cleanedMarkdown)) mdFile = matter(String(cleanedMarkdown))
} else { } else {
mdFile = matter(content) mdFile = matter(content)
} }
// Extract title from frontmatter or use the first heading // Extract title from frontmatter or use the first heading
const title = extractTitle(mdFile)?.trim() || 'Untitled' const title = extractTitle(mdFile)?.trim() || 'Untitled'
const filePath = const filePath =
path.basename(file) === 'index.md' && path.basename(file) === 'index.md' &&
path.dirname(file) !== settings.workDir path.dirname(file) !== settings.workDir
? `${path.dirname(file)}.md` ? `${path.dirname(file)}.md`
: file : file
return { path: filePath, title, file: mdFile } return { path: filePath, title, file: mdFile }
}), })
) )
if (settings.generateLLMFriendlyDocsForEachPage) { if (settings.generateLLMFriendlyDocsForEachPage) {
await Promise.all( await Promise.all(
preparedFiles.map(async (file) => { preparedFiles.map(async (file) => {
const relativePath = path.relative(settings.workDir, file.path) const relativePath = path.relative(settings.workDir, file.path)
try { try {
const mdFile = file.file const mdFile = file.file
const targetPath = path.resolve(outDir, relativePath) const targetPath = path.resolve(outDir, relativePath)
// Ensure target directory exists (async version) // Ensure target directory exists (async version)
await fs.mkdir(path.dirname(targetPath), { await fs.mkdir(path.dirname(targetPath), {
recursive: true, recursive: true
}) })
// Copy file to output directory (async version) // Copy file to output directory (async version)
await fs.writeFile( await fs.writeFile(
targetPath, targetPath,
matter.stringify( matter.stringify(
mdFile.content, mdFile.content,
generateMetadata(mdFile, { generateMetadata(mdFile, {
domain: settings.domain, domain: settings.domain,
filePath: relativePath, filePath: relativePath
}), })
), )
) )
log.success(`Processed ${pc.cyan(relativePath)}`) log.success(`Processed ${pc.cyan(relativePath)}`)
} catch (error) { } catch (error) {
log.error( log.error(
// @ts-ignore // @ts-ignore
`Failed to process ${pc.cyan(relativePath)}: ${error.message}`, `Failed to process ${pc.cyan(relativePath)}: ${error.message}`
) )
} }
}), })
) )
} }
// Sort files by title for better organization // Sort files by title for better organization
preparedFiles.sort((a, b) => a.title.localeCompare(b.title)) preparedFiles.sort((a, b) => a.title.localeCompare(b.title))
const tasks: Promise<void>[] = [] const tasks: Promise<void>[] = []
// Generate llms.txt - table of contents with links // Generate llms.txt - table of contents with links
if (settings.generateLLMsTxt) { if (settings.generateLLMsTxt) {
const llmsTxtPath = path.resolve(outDir, 'llms.txt') const llmsTxtPath = path.resolve(outDir, 'llms.txt')
const templateVariables: CustomTemplateVariables = { const templateVariables: CustomTemplateVariables = {
title: settings.title, title: settings.title,
description: settings.description, description: settings.description,
details: settings.details, details: settings.details,
toc: settings.toc, toc: settings.toc,
...settings.customTemplateVariables, ...settings.customTemplateVariables
} }
tasks.push( tasks.push(
(async () => { (async () => {
log.info(`Generating ${pc.cyan('llms.txt')}...`) log.info(`Generating ${pc.cyan('llms.txt')}...`)
const llmsTxt = await generateLLMsTxt(preparedFiles, { const llmsTxt = await generateLLMsTxt(preparedFiles, {
indexMd: path.resolve(settings.workDir as string, 'index.md'), indexMd: path.resolve(settings.workDir as string, 'index.md'),
srcDir: settings.workDir as string, srcDir: settings.workDir as string,
LLMsTxtTemplate: LLMsTxtTemplate:
settings.customLLMsTxtTemplate || defaultLLMsTxtTemplate, settings.customLLMsTxtTemplate || defaultLLMsTxtTemplate,
templateVariables, templateVariables,
vitepressConfig: config?.vitepress?.userConfig, vitepressConfig: config?.vitepress?.userConfig,
domain: settings.domain, domain: settings.domain,
sidebar: settings.sidebar, sidebar: settings.sidebar,
linksExtension: !settings.generateLLMFriendlyDocsForEachPage linksExtension: !settings.generateLLMFriendlyDocsForEachPage
? '.html' ? '.html'
: undefined, : undefined,
cleanUrls: config.cleanUrls, cleanUrls: config.cleanUrls
}) })
await fs.writeFile(llmsTxtPath, llmsTxt, 'utf-8') await fs.writeFile(llmsTxtPath, llmsTxt, 'utf-8')
log.success( log.success(
expandTemplate( expandTemplate(
'Generated {file} (~{tokens} tokens, {size}) with {fileCount} documentation links', 'Generated {file} (~{tokens} tokens, {size}) with {fileCount} documentation links',
{ {
file: pc.cyan('llms.txt'), file: pc.cyan('llms.txt'),
tokens: pc.bold(millify(approximateTokenSize(llmsTxt))), tokens: pc.bold(millify(approximateTokenSize(llmsTxt))),
size: pc.bold(getHumanReadableSizeOf(llmsTxt)), size: pc.bold(getHumanReadableSizeOf(llmsTxt)),
fileCount: pc.bold(fileCount.toString()), fileCount: pc.bold(fileCount.toString())
}, }
), )
) )
})(), })()
) )
} }
// Generate llms-full.txt - all content in one file // Generate llms-full.txt - all content in one file
if (settings.generateLLMsFullTxt) { if (settings.generateLLMsFullTxt) {
const llmsFullTxtPath = path.resolve(outDir, 'llms-full.txt') const llmsFullTxtPath = path.resolve(outDir, 'llms-full.txt')
tasks.push( tasks.push(
(async () => { (async () => {
log.info( log.info(
`Generating full documentation bundle (${pc.cyan('llms-full.txt')})...`, `Generating full documentation bundle (${pc.cyan('llms-full.txt')})...`
) )
const llmsFullTxt = generateLLMsFullTxt(preparedFiles, { const llmsFullTxt = generateLLMsFullTxt(preparedFiles, {
srcDir: settings.workDir as string, srcDir: settings.workDir as string,
domain: settings.domain, domain: settings.domain,
linksExtension: !settings.generateLLMFriendlyDocsForEachPage linksExtension: !settings.generateLLMFriendlyDocsForEachPage
? '.html' ? '.html'
: undefined, : undefined,
cleanUrls: config.cleanUrls, cleanUrls: config.cleanUrls
}) })
// Write content to llms-full.txt // Write content to llms-full.txt
await fs.writeFile(llmsFullTxtPath, llmsFullTxt, 'utf-8') await fs.writeFile(llmsFullTxtPath, llmsFullTxt, 'utf-8')
log.success( log.success(
expandTemplate( expandTemplate(
'Generated {file} (~{tokens} tokens, {size}) with {fileCount} markdown files', 'Generated {file} (~{tokens} tokens, {size}) with {fileCount} markdown files',
{ {
file: pc.cyan('llms-full.txt'), file: pc.cyan('llms-full.txt'),
tokens: pc.bold(millify(approximateTokenSize(llmsFullTxt))), tokens: pc.bold(millify(approximateTokenSize(llmsFullTxt))),
size: pc.bold(getHumanReadableSizeOf(llmsFullTxt)), size: pc.bold(getHumanReadableSizeOf(llmsFullTxt)),
fileCount: pc.bold(fileCount.toString()), fileCount: pc.bold(fileCount.toString())
}, }
), )
) )
})(), })()
) )
} }
if (tasks.length) { if (tasks.length) {
await Promise.all(tasks) await Promise.all(tasks)
} }
}, }
} }
} }

@ -3,240 +3,240 @@ import type { ResolvedConfig } from 'vite'
import type { DefaultTheme, SiteConfig, UserConfig } from 'vitepress' import type { DefaultTheme, SiteConfig, UserConfig } from 'vitepress'
interface TemplateVariables { interface TemplateVariables {
/** /**
* The title extracted from the frontmatter or the first h1 heading in the main document (`index.md`). * The title extracted from the frontmatter or the first h1 heading in the main document (`index.md`).
* *
* @example 'Awesome tool' * @example 'Awesome tool'
*/ */
title?: string title?: string
/** /**
* The description. * The description.
* *
* @example 'Blazing fast build tool' * @example 'Blazing fast build tool'
*/ */
description?: string description?: string
/** /**
* The details. * The details.
* *
* @example 'A multi-user version of the notebook designed for companies, classrooms and research labs' * @example 'A multi-user version of the notebook designed for companies, classrooms and research labs'
*/ */
details?: string details?: string
/** /**
* An automatically generated **T**able **O**f **C**ontents. * An automatically generated **T**able **O**f **C**ontents.
* *
* @example * @example
* ```markdown * ```markdown
* - [Title](/foo.md): Lorem ipsum dolor sit amet, consectetur adipiscing elit. * - [Title](/foo.md): Lorem ipsum dolor sit amet, consectetur adipiscing elit.
* - [Title 2](/bar/baz.md): Cras vel nibh id ipsum pharetra efficitur. * - [Title 2](/bar/baz.md): Cras vel nibh id ipsum pharetra efficitur.
* ``` * ```
*/ */
toc?: string toc?: string
} }
interface CustomTemplateVariables extends TemplateVariables { interface CustomTemplateVariables extends TemplateVariables {
/** Any custom variable */ /** Any custom variable */
[key: string]: string | undefined [key: string]: string | undefined
} }
export interface LlmstxtSettings extends TemplateVariables { export interface LlmstxtSettings extends TemplateVariables {
/** /**
* The domain that will be appended to the beginning of URLs in `llms.txt` and in the context of other files * The domain that will be appended to the beginning of URLs in `llms.txt` and in the context of other files
* *
* Domain attachment is not yet agreed upon (since it depends on the AI whether it can resolve the relative paths that are currently there), but if you want you can add it * Domain attachment is not yet agreed upon (since it depends on the AI whether it can resolve the relative paths that are currently there), but if you want you can add it
* *
* **Note**: Domain cannot end with `/`. * **Note**: Domain cannot end with `/`.
* *
* Without a {@link LlmstxtSettings.domain | `domain`}: * Without a {@link LlmstxtSettings.domain | `domain`}:
* ```markdown * ```markdown
* - [Title](/foo/bar.md) * - [Title](/foo/bar.md)
* ``` * ```
* *
* With a {@link LlmstxtSettings.domain | `domain`}: * With a {@link LlmstxtSettings.domain | `domain`}:
* ```markdown * ```markdown
* - [Title](https://example.com/foo/bar.md) * - [Title](https://example.com/foo/bar.md)
* ``` * ```
* *
* @example * @example
* ```typescript * ```typescript
* llmstxt({ domain: 'https://example.com' }) * llmstxt({ domain: 'https://example.com' })
* ``` * ```
*/ */
domain?: string domain?: string
/** /**
* Indicates whether to generate the `llms.txt` file, which contains a list of sections with corresponding links. * Indicates whether to generate the `llms.txt` file, which contains a list of sections with corresponding links.
* *
* @default true * @default true
*/ */
generateLLMsTxt?: boolean generateLLMsTxt?: boolean
/** /**
* Determines whether to generate the `llms-full.txt` which contains all the documentation in one file. * Determines whether to generate the `llms-full.txt` which contains all the documentation in one file.
* *
* @default true * @default true
*/ */
generateLLMsFullTxt?: boolean generateLLMsFullTxt?: boolean
/** /**
* Determines whether to generate an LLM-friendly version of the documentation for each page on the website. * Determines whether to generate an LLM-friendly version of the documentation for each page on the website.
* *
* @default true * @default true
*/ */
generateLLMFriendlyDocsForEachPage?: boolean generateLLMFriendlyDocsForEachPage?: boolean
/** /**
* Whether to strip HTML tags from Markdown files * Whether to strip HTML tags from Markdown files
* *
* @default true * @default true
*/ */
stripHTML?: boolean stripHTML?: boolean
/** /**
* The directory from which files will be processed. * The directory from which files will be processed.
* *
* This is useful for configuring the plugin to generate documentation for LLMs in a specific language. * This is useful for configuring the plugin to generate documentation for LLMs in a specific language.
* *
* @example * @example
* ```typescript * ```typescript
* llmstxt({ * llmstxt({
* // Generate documentation for LLMs from English documentation only * // Generate documentation for LLMs from English documentation only
* workDir: 'en' * workDir: 'en'
* }) * })
* ``` * ```
* *
* @default vitepress.srcDir * @default vitepress.srcDir
*/ */
workDir?: string workDir?: string
/** /**
* An array of file path patterns to be ignored during processing. * An array of file path patterns to be ignored during processing.
* *
* This is useful for excluding certain files from LLMs, such as those not related to documentation (e.g., sponsors, team, etc.). * This is useful for excluding certain files from LLMs, such as those not related to documentation (e.g., sponsors, team, etc.).
* *
* @example * @example
* ```typescript * ```typescript
* llmstxt({ * llmstxt({
* ignoreFiles: [ * ignoreFiles: [
* 'about/team/*', * 'about/team/*',
* 'sponsor/*' * 'sponsor/*'
* // ... * // ...
* ] * ]
* }) * })
* ``` * ```
* *
* @default [] * @default []
*/ */
ignoreFiles?: string[] ignoreFiles?: string[]
/** /**
* A custom template for the `llms.txt` file, allowing for a personalized order of elements. * A custom template for the `llms.txt` file, allowing for a personalized order of elements.
* *
* Available template elements include: * Available template elements include:
* *
* - `{title}`: The title extracted from the frontmatter or the first h1 heading in the main document (`index.md`). * - `{title}`: The title extracted from the frontmatter or the first h1 heading in the main document (`index.md`).
* - `{description}`: The description. * - `{description}`: The description.
* - `{details}`: The details. * - `{details}`: The details.
* - `{toc}`: An automatically generated **T**able **O**f **C**ontents. * - `{toc}`: An automatically generated **T**able **O**f **C**ontents.
* *
* You can also add custom variables using the {@link LlmstxtSettings.customTemplateVariables | `customTemplateVariables`} parameter * You can also add custom variables using the {@link LlmstxtSettings.customTemplateVariables | `customTemplateVariables`} parameter
* *
* @default * @default
* ```markdown * ```markdown
* # {title} * # {title}
* *
* > {description} * > {description}
* *
* {details} * {details}
* *
* ## Table of Contents * ## Table of Contents
* *
* {toc} * {toc}
* ``` * ```
*/ */
customLLMsTxtTemplate?: string customLLMsTxtTemplate?: string
/** /**
* Custom variables for {@link LlmstxtSettings.customLLMsTxtTemplate | `customLLMsTxtTemplate`}. * Custom variables for {@link LlmstxtSettings.customLLMsTxtTemplate | `customLLMsTxtTemplate`}.
* *
* With this option you can edit or add variables to the template. * With this option you can edit or add variables to the template.
* *
* You can change the title in `llms.txt` without having to change the template: * You can change the title in `llms.txt` without having to change the template:
* *
* @example * @example
* ```typescript * ```typescript
* llmstxt({ * llmstxt({
* customTemplateVariables: { * customTemplateVariables: {
* title: 'Very custom title', * title: 'Very custom title',
* } * }
* }) * })
* ``` * ```
* *
* You can also combine this with a custom template: * You can also combine this with a custom template:
* *
* @example * @example
* ```typescript * ```typescript
* llmstxt({ * llmstxt({
* customLLMsTxtTemplate: '# {title}\n\n{foo}', * customLLMsTxtTemplate: '# {title}\n\n{foo}',
* customTemplateVariables: { * customTemplateVariables: {
* foo: 'Very custom title', * foo: 'Very custom title',
* } * }
* }) * })
* ``` * ```
*/ */
customTemplateVariables?: CustomTemplateVariables customTemplateVariables?: CustomTemplateVariables
/** /**
* VitePress {@link DefaultTheme.Sidebar | Sidebar} * VitePress {@link DefaultTheme.Sidebar | Sidebar}
* *
* Here you can insert your {@link DefaultTheme.Sidebar | `sidebar`} if it is not in the VitePress configuration * Here you can insert your {@link DefaultTheme.Sidebar | `sidebar`} if it is not in the VitePress configuration
* *
* Usually this parameter is used in rare cases * Usually this parameter is used in rare cases
*/ */
sidebar?: DefaultTheme.Sidebar sidebar?: DefaultTheme.Sidebar
} }
/** /**
* Represents a prepared file, including its title and path. * Represents a prepared file, including its title and path.
*/ */
export type PreparedFile = { export type PreparedFile = {
/** /**
* The title of the file. * The title of the file.
* *
* @example 'Guide' * @example 'Guide'
*/ */
title: string title: string
/** /**
* The absolute path to the file. * The absolute path to the file.
* *
* @example 'guide/getting-started.md' * @example 'guide/getting-started.md'
*/ */
path: string path: string
/** /**
* The prepared file itself. * The prepared file itself.
* *
* @example * @example
* ```typescript * ```typescript
* { * {
* data: { * data: {
* title: 'Guide' * title: 'Guide'
* }, * },
* content: 'Content goes here' * content: 'Content goes here'
* orig: '---\ntitle: Guide\n---\n\nContent goes here' * orig: '---\ntitle: Guide\n---\n\nContent goes here'
* } * }
* ``` * ```
*/ */
file: GrayMatterFile<Input> file: GrayMatterFile<Input>
} }
interface VitePressConfig interface VitePressConfig
extends Omit<UserConfig, keyof ResolvedConfig>, extends Omit<UserConfig, keyof ResolvedConfig>,
ResolvedConfig { ResolvedConfig {
vitepress: SiteConfig vitepress: SiteConfig
} }
/** Represents the link extension options for generated links. */ /** Represents the link extension options for generated links. */

Loading…
Cancel
Save