Skip to content

Commit 4ae9189

Browse files
committed
feat: add llms.txt
1 parent 0ab080b commit 4ae9189

File tree

4 files changed

+399
-1
lines changed

4 files changed

+399
-1
lines changed

app/[lang]/llms.txt/route.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import fg from 'fast-glob';
2+
import matter from 'gray-matter';
3+
import * as fs from 'node:fs/promises';
4+
import path from 'node:path';
5+
import { remark } from 'remark';
6+
import remarkGfm from 'remark-gfm';
7+
import remarkStringify from 'remark-stringify';
8+
9+
export const revalidate = false;
10+
11+
// Regular expressions for cleaning up the content
12+
const IMPORT_REGEX = /import\s+?(?:(?:{[^}]*}|\*|\w+)\s+from\s+)?['"](.*?)['"];?\n?/g;
13+
const COMPONENT_USAGE_REGEX = /<[A-Z][a-zA-Z]*(?:\s+[^>]*)?(?:\/?>|>[^<]*<\/[A-Z][a-zA-Z]*>)/g;
14+
const NEXTRA_COMPONENT_REGEX = /<(?:Callout|Steps|Tabs|Tab|FileTree)[^>]*>[^<]*<\/(?:Callout|Steps|Tabs|Tab|FileTree)>/g;
15+
const MDX_EXPRESSION_REGEX = /{(?:[^{}]|{[^{}]*})*}/g;
16+
const EXPORT_REGEX = /export\s+(?:default\s+)?(?:const|let|var|function|class|interface|type)?\s+[a-zA-Z_$][0-9a-zA-Z_$]*[\s\S]*?(?:;|\n|$)/g;
17+
18+
export async function GET() {
19+
try {
20+
const files = await fg(['content/en/patterns/**/*.mdx']);
21+
22+
const scan = files.map(async (file) => {
23+
try {
24+
const fileContent = await fs.readFile(file);
25+
const { content, data } = matter(fileContent.toString());
26+
27+
// Get the filename without extension to use as fallback title
28+
const basename = path.basename(file, '.mdx');
29+
30+
// Extract category from file path
31+
const pathParts = path.dirname(file).split(path.sep);
32+
let category = 'general';
33+
if (pathParts.length > 3 && pathParts[3]) {
34+
category = pathParts[3];
35+
}
36+
37+
// Skip if the file is marked as hidden or draft
38+
if (data.draft || data.hidden) {
39+
return null;
40+
}
41+
42+
// Use filename as title if no title in frontmatter, and convert to Title Case
43+
const title = data.title || basename.split('-')
44+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
45+
.join(' ');
46+
47+
const processed = await processContent(content);
48+
return `File: ${file}
49+
# ${category.toUpperCase()}: ${title}
50+
51+
${data.description || ''}
52+
53+
${processed}`;
54+
} catch (error) {
55+
console.error(`Error processing file ${file}:`, error);
56+
return null;
57+
}
58+
});
59+
60+
const scanned = (await Promise.all(scan)).filter(Boolean);
61+
62+
if (!scanned.length) {
63+
return new Response('No content found', { status: 404 });
64+
}
65+
66+
return new Response(scanned.join('\n\n'));
67+
} catch (error) {
68+
console.error('Error generating LLM content:', error);
69+
return new Response('Internal Server Error', { status: 500 });
70+
}
71+
}
72+
73+
async function processContent(content: string): Promise<string> {
74+
try {
75+
// Multi-step cleanup to handle different MDX constructs
76+
let cleanContent = content
77+
// Remove imports first
78+
.replace(IMPORT_REGEX, '')
79+
// Remove exports
80+
.replace(EXPORT_REGEX, '')
81+
// Remove Nextra components with their content
82+
.replace(NEXTRA_COMPONENT_REGEX, '')
83+
// Remove other React components
84+
.replace(COMPONENT_USAGE_REGEX, '')
85+
// Remove MDX expressions
86+
.replace(MDX_EXPRESSION_REGEX, '')
87+
// Clean up multiple newlines
88+
.replace(/\n{3,}/g, '\n\n')
89+
// Remove empty JSX expressions
90+
.replace(/{[\s]*}/g, '')
91+
// Clean up any remaining JSX-like syntax
92+
.replace(/<>[\s\S]*?<\/>/g, '')
93+
.replace(/{\s*\/\*[\s\S]*?\*\/\s*}/g, '')
94+
.trim();
95+
96+
// Simple markdown processing without MDX
97+
const file = await remark()
98+
.use(remarkGfm)
99+
.use(remarkStringify)
100+
.process(cleanContent);
101+
102+
return String(file);
103+
} catch (error) {
104+
console.error('Error processing content:', error);
105+
// If processing fails, return a basic cleaned version
106+
return content
107+
.replace(IMPORT_REGEX, '')
108+
.replace(COMPONENT_USAGE_REGEX, '')
109+
.replace(MDX_EXPRESSION_REGEX, '')
110+
.trim();
111+
}
112+
}

middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ export { middleware } from 'nextra/locales'
33
export const config = {
44
// Matcher ignoring `/_next/` and `/api/`
55
matcher: [
6-
'/((?!api/mdx|api/email|api/patterns/random|api/og|_next/static|_next/image|favicon.ico|robots.txt|og/opengraph-image.png|covers|twitter-image|sitemap.xml|6ba7b811-9dad-11d1-80b4.txt|43mg4ybv6sxxanu24g7dngawd9up5w93.txt|apple-icon.png|manifest|_pagefind|examples).*)'
6+
'/((?!api/mdx|api/email|api/patterns/random|api/og|_next/static|_next/image|llms.txt|favicon.ico|robots.txt|og/opengraph-image.png|covers|twitter-image|sitemap.xml|6ba7b811-9dad-11d1-80b4.txt|43mg4ybv6sxxanu24g7dngawd9up5w93.txt|apple-icon.png|manifest|_pagefind|examples).*)'
77
]
88
}

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@
3333
"@mdn/browser-compat-data": "^5.6.37",
3434
"@radix-ui/react-slot": "^1.1.2",
3535
"@sentry/nextjs": "^8.54.0",
36+
"@types/chalk": "^2.2.4",
3637
"@types/dagre": "^0.7.52",
38+
"chalk": "^5.4.1",
3739
"class-variance-authority": "^0.7.1",
3840
"clsx": "^2.1.1",
3941
"dagre": "^0.8.5",
42+
"fast-glob": "^3.3.3",
4043
"gray-matter": "^4.0.3",
4144
"html-to-image": "^1.11.11",
4245
"lucide-react": "^0.475.0",
@@ -45,13 +48,17 @@
4548
"next-plausible": "^3.12.4",
4649
"nextra": "4.2.5",
4750
"nextra-theme-docs": "4.2.5",
51+
"openai": "^4.83.0",
4852
"react": "19.0.0",
4953
"react-dom": "19.0.0",
5054
"react-intersection-observer": "^9.15.1",
5155
"react-markdown": "^9.0.3",
5256
"react-resizable-panels": "^2.1.7",
5357
"reactflow": "^11.11.4",
58+
"remark": "^15.0.1",
5459
"remark-gfm": "^4.0.0",
60+
"remark-mdx": "^3.1.0",
61+
"remark-stringify": "^11.0.0",
5562
"remove-markdown": "^0.6.0",
5663
"require-in-the-middle": "^7.5.1",
5764
"simple-icons": "^14.6.0",
@@ -69,11 +76,13 @@
6976
"@types/node": "22.13.1",
7077
"@types/react": "19.0.8",
7178
"cross-env": "^7.0.3",
79+
"dotenv": "^16.4.7",
7280
"eslint": "^9.20.0",
7381
"eslint-config-next": "15.1.6",
7482
"pagefind": "^1.3.0",
7583
"plop": "^4.0.1",
7684
"tailwindcss": "4.0.5",
85+
"ts-node": "^10.9.2",
7786
"tsx": "^4.19.2",
7887
"typescript": "^5.7.3"
7988
}

0 commit comments

Comments
 (0)