diff --git a/frontend/apps/docs/app/docs/llms-full.txt/route.ts b/frontend/apps/docs/app/docs/llms-full.txt/route.ts index b9a4a7427..9d3825aa8 100644 --- a/frontend/apps/docs/app/docs/llms-full.txt/route.ts +++ b/frontend/apps/docs/app/docs/llms-full.txt/route.ts @@ -9,14 +9,13 @@ const getRawContents = (dirPath: string): string[] => { const results: string[] = [] let rootIndexContent: string | null = null - const list = fs.readdirSync(dirPath) + const list = fs.readdirSync(dirPath, { withFileTypes: true }) for (const file of list) { - const filePath = path.resolve(dirPath, file) - const stat = fs.statSync(filePath) + const filePath = path.resolve(dirPath, file.name) - if (stat?.isDirectory()) { + if (file.isDirectory()) { results.push(...getRawContents(filePath)) - } else if (file.endsWith('.mdx')) { + } else if (file.name.endsWith('.mdx')) { const content = fs.readFileSync(filePath, 'utf8') if ( filePath === path.resolve(dirPath, 'index.mdx') && @@ -41,8 +40,7 @@ export function GET() { path.resolve(process.cwd(), 'content/docs'), ) - const content = ` -# Liam ERD + const content = `# Liam ERD ${rawContents.join('\n\n')} ` diff --git a/frontend/apps/docs/app/docs/llms.txt/route.ts b/frontend/apps/docs/app/docs/llms.txt/route.ts new file mode 100644 index 000000000..a873edc32 --- /dev/null +++ b/frontend/apps/docs/app/docs/llms.txt/route.ts @@ -0,0 +1,111 @@ +import fs from 'node:fs' +import path from 'node:path' +import { NextResponse } from 'next/server' + +// This is a static route, so it will be rendered at build time +export const dynamic = 'force-static' + +interface MDXFile { + path: string // relative + title: string + url: string + description?: string +} + +interface Frontmatter { + title: string + description?: string +} + +const extractFrontmatter = (content: string): Frontmatter | null => { + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/) + if (!frontmatterMatch) return null + + const frontmatter = frontmatterMatch[1] + const titleMatch = frontmatter.match(/^title:\s*(.+)$/m) + const descriptionMatch = frontmatter.match(/^description:\s*(.+)$/m) + + if (!titleMatch) return null + + return { + title: titleMatch[1], + description: descriptionMatch?.[1], + } +} + +const getContents = (dirPath: string): MDXFile[] => { + const results: MDXFile[] = [] + const list = fs.readdirSync(dirPath, { withFileTypes: true }) + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3002' + + for (const file of list) { + const filePath = path.resolve(dirPath, file.name) + + if (file.isDirectory()) { + results.push(...getContents(filePath)) + } else if (file.name.endsWith('.mdx')) { + const content = fs.readFileSync(filePath, 'utf8') + const frontmatter = extractFrontmatter(content) + + if (!frontmatter) { + continue + } + + const relativePath = path + .relative(path.resolve(process.cwd(), 'content/docs'), filePath) + .replace(/(\/?index)?\.mdx$/, '') + const url = new URL(`docs/${relativePath}`, baseUrl).toString() + + results.push({ + path: relativePath, + title: frontmatter.title, + url, + ...(frontmatter.description && { + description: frontmatter.description, + }), + }) + } + } + + return results.sort((a, b) => + a.path.localeCompare(b.path, 'en', { numeric: true }), + ) +} + +/** + * Returns an indentation string based on the number of slashes in the path. + * + * @param path - A string that may contain slashes (/) + * @returns A string of spaces: 4 spaces per slash in the path + */ +const getIndent = (path: string): string => { + const count = (path.match(/\//g) || []).length + return ' '.repeat(count * 4) +} + +export function GET() { + const contents = getContents(path.resolve(process.cwd(), 'content/docs')) + + const llmsText = `# Liam ERD + +## Docs + +${contents + .map((file) => { + const base = `${getIndent(file.path)}- [${file.title}](${file.url})` + return file.description ? `${base}: ${file.description}` : base + }) + .join('\n')} + +## Optional + +- [Website](https://liambx.com/) +- [Open Source](https://github.com/liam-hq/liam) +` + + return new NextResponse(llmsText, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + }, + }) +}