|
1 | | -import fg from 'fast-glob'; |
2 | 1 | import matter from 'gray-matter'; |
3 | | -import * as fs from 'node:fs/promises'; |
| 2 | +import { NextResponse } from 'next/server'; |
| 3 | +import fs from 'node:fs'; |
4 | 4 | import path from 'node:path'; |
5 | | -import { remark } from 'remark'; |
6 | | -import remarkGfm from 'remark-gfm'; |
7 | | -import remarkStringify from 'remark-stringify'; |
8 | 5 |
|
9 | | -export const revalidate = false; |
| 6 | +const contentDirectory = path.join(process.cwd(), 'content/en/patterns'); |
10 | 7 |
|
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; |
| 8 | +interface Pattern { |
| 9 | + category: string; |
| 10 | + title: string; |
| 11 | + summary: string; |
| 12 | + status: string; |
| 13 | + slug: string; |
| 14 | +} |
17 | 15 |
|
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 | | - }); |
| 16 | +interface PatternsByCategory { |
| 17 | + [category: string]: Pattern[]; |
| 18 | +} |
59 | 19 |
|
60 | | - const scanned = (await Promise.all(scan)).filter(Boolean); |
| 20 | +function slugify(text: string): string { |
| 21 | + return text |
| 22 | + .toLowerCase() |
| 23 | + .replace(/[^a-z0-9]+/g, '-') |
| 24 | + .replace(/(^-|-$)/g, ''); |
| 25 | +} |
61 | 26 |
|
62 | | - if (!scanned.length) { |
63 | | - return new Response('No content found', { status: 404 }); |
64 | | - } |
| 27 | +function getAllPatterns(): PatternsByCategory { |
| 28 | + // Get all directories under patterns |
| 29 | + const categories = fs.readdirSync(contentDirectory); |
65 | 30 |
|
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 }); |
| 31 | + const allPatterns: PatternsByCategory = {}; |
| 32 | + |
| 33 | + for (const category of categories) { |
| 34 | + const categoryPath = path.join(contentDirectory, category); |
| 35 | + |
| 36 | + // Skip if not a directory |
| 37 | + if (!fs.statSync(categoryPath).isDirectory()) continue; |
| 38 | + |
| 39 | + // Read all MDX files in the category |
| 40 | + const files = fs.readdirSync(categoryPath) |
| 41 | + .filter(file => file.endsWith('.mdx')); |
| 42 | + |
| 43 | + const categoryPatterns = files.map(file => { |
| 44 | + const fullPath = path.join(categoryPath, file); |
| 45 | + const fileContents = fs.readFileSync(fullPath, 'utf8'); |
| 46 | + const { data } = matter(fileContents); |
| 47 | + const slug = file.replace('.mdx', ''); |
| 48 | + |
| 49 | + return { |
| 50 | + category, |
| 51 | + title: data.title || slug, |
| 52 | + summary: data.summary || '', |
| 53 | + status: data.status || 'coming soon', |
| 54 | + slug |
| 55 | + }; |
| 56 | + }); |
| 57 | + |
| 58 | + allPatterns[category] = categoryPatterns; |
70 | 59 | } |
| 60 | + |
| 61 | + return allPatterns; |
71 | 62 | } |
72 | 63 |
|
73 | | -async function processContent(content: string): Promise<string> { |
| 64 | +export async function GET( |
| 65 | + request: Request, |
| 66 | + { params }: { params: { lang: string } } |
| 67 | +) { |
74 | 68 | 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); |
| 69 | + const patterns = getAllPatterns(); |
| 70 | + |
| 71 | + // Get base URL and await params |
| 72 | + const [baseUrl, { lang }] = await Promise.all([ |
| 73 | + Promise.resolve( |
| 74 | + process.env.NODE_ENV === 'development' |
| 75 | + ? 'http://localhost:3000' |
| 76 | + : `https://${process.env.NEXT_PUBLIC_VERCEL_URL || 'localhost:3000'}` |
| 77 | + ), |
| 78 | + params |
| 79 | + ]); |
| 80 | + |
| 81 | + // Generate the text content |
| 82 | + let content = `# UX Patterns for Developers |
| 83 | +
|
| 84 | +## Overview |
| 85 | +This is an automatically generated overview of all UX patterns documented in this project. |
| 86 | +
|
| 87 | +## Pattern Categories\n`; |
| 88 | + |
| 89 | + // Add patterns by category |
| 90 | + for (const [category, categoryPatterns] of Object.entries(patterns)) { |
| 91 | + content += `\n### ${category.charAt(0).toUpperCase() + category.slice(1)}\n`; |
| 92 | + for (const pattern of categoryPatterns) { |
| 93 | + const patternUrl = `${baseUrl}/${lang}/patterns/${category}/${pattern.slug}`; |
| 94 | + content += `- [${pattern.title}](${patternUrl})${pattern.summary ? `: ${pattern.summary}` : ''} [${pattern.status}]\n`; |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + content += `\n## Additional Resources |
| 99 | +- [Blog posts and articles about UX patterns](${baseUrl}/${lang}/blog) |
| 100 | +- [Comprehensive glossary of UX terms](${baseUrl}/${lang}/glossary) |
| 101 | +
|
| 102 | +## Technical Implementation |
| 103 | +- Built with Next.js and TypeScript |
| 104 | +- MDX-based pattern documentation |
| 105 | +- Accessibility-first approach |
| 106 | +- Comprehensive testing guidelines`; |
| 107 | + |
| 108 | + return new NextResponse(content, { |
| 109 | + headers: { |
| 110 | + 'Content-Type': 'text/plain', |
| 111 | + }, |
| 112 | + }); |
103 | 113 | } 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(); |
| 114 | + console.error('Error generating content:', error); |
| 115 | + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); |
111 | 116 | } |
112 | 117 | } |
0 commit comments