|
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