185 lines
5.1 KiB
TypeScript
185 lines
5.1 KiB
TypeScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import { cache } from '@/utils/cache';
|
|
|
|
export interface BlogPost {
|
|
slug: string;
|
|
title: string;
|
|
date: string;
|
|
excerpt: string;
|
|
coverImage: string;
|
|
category?: string;
|
|
tags?: string[];
|
|
content?: string;
|
|
}
|
|
|
|
const BLOG_POSTS_DIR = path.join(process.cwd(), 'blog-posts');
|
|
|
|
// Category mapping based on filename patterns
|
|
const categoryMap: Record<string, string> = {
|
|
'agentic': 'KI-Agenten',
|
|
'ai-agent': 'KI-Agenten',
|
|
'deepseek': 'KI-Entwicklung',
|
|
'openai': 'KI-Entwicklung',
|
|
'gpt': 'KI-Entwicklung',
|
|
'claude': 'KI-Entwicklung',
|
|
'langchain': 'KI-Entwicklung',
|
|
'langgraph': 'KI-Entwicklung',
|
|
'rag': 'KI-Entwicklung',
|
|
'llm': 'KI-Entwicklung',
|
|
'voice': 'Voice AI',
|
|
'vapi': 'Voice AI',
|
|
'speech': 'Voice AI',
|
|
'elevenlabs': 'Voice AI',
|
|
'whisper': 'Voice AI',
|
|
'n8n': 'Automatisierung',
|
|
'make': 'Automatisierung',
|
|
'zapier': 'Automatisierung',
|
|
'automation': 'Automatisierung',
|
|
'workflow': 'Automatisierung',
|
|
'scraping': 'Web Scraping',
|
|
'playwright': 'Web Scraping',
|
|
'puppeteer': 'Web Scraping',
|
|
'nextjs': 'Web Development',
|
|
'react': 'Web Development',
|
|
'typescript': 'Web Development',
|
|
'api': 'Backend',
|
|
'database': 'Backend',
|
|
'postgres': 'Backend',
|
|
'prisma': 'Backend',
|
|
'supabase': 'Backend',
|
|
'saas': 'SaaS',
|
|
'stripe': 'SaaS',
|
|
'subscription': 'SaaS',
|
|
'seo': 'SEO',
|
|
'performance': 'Performance',
|
|
'iot': 'IoT',
|
|
'raspberry': 'IoT',
|
|
'mqtt': 'IoT',
|
|
'home-assistant': 'Smart Home',
|
|
};
|
|
|
|
function detectCategory(filename: string, content: string): string {
|
|
const lowerFilename = filename.toLowerCase();
|
|
const lowerContent = content.toLowerCase().slice(0, 500);
|
|
|
|
for (const [keyword, category] of Object.entries(categoryMap)) {
|
|
if (lowerFilename.includes(keyword) || lowerContent.includes(keyword)) {
|
|
return category;
|
|
}
|
|
}
|
|
return 'Technologie';
|
|
}
|
|
|
|
function parseMarkdownPost(filename: string, content: string): BlogPost | null {
|
|
try {
|
|
// Extract title from first H1
|
|
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
const title = titleMatch ? titleMatch[1].trim() : filename.replace(/\.md$/, '');
|
|
|
|
// Extract meta description
|
|
const metaMatch = content.match(/\*\*Meta-Description:\*\*\s*(.+)/i);
|
|
const metaDescription = metaMatch ? metaMatch[1].trim() : '';
|
|
|
|
// Extract keywords
|
|
const keywordsMatch = content.match(/\*\*Keywords?:\*\*\s*(.+)/i);
|
|
const keywordsStr = keywordsMatch ? keywordsMatch[1].trim() : '';
|
|
const tags = keywordsStr ? keywordsStr.split(',').map(k => k.trim()).slice(0, 6) : [];
|
|
|
|
// Extract excerpt from introduction or meta description
|
|
let excerpt = metaDescription;
|
|
if (!excerpt) {
|
|
const introMatch = content.match(/## Einführung\s*\n\n([\s\S]+?)(?:\n\n|$)/);
|
|
if (introMatch) {
|
|
excerpt = introMatch[1].trim().slice(0, 200) + '...';
|
|
}
|
|
}
|
|
|
|
// Generate slug from filename (remove number prefix)
|
|
const slug = filename
|
|
.replace(/\.md$/, '')
|
|
.replace(/^\d+-/, '');
|
|
|
|
// Extract date from filename number (approximate)
|
|
const numMatch = filename.match(/^(\d+)-/);
|
|
const postNum = numMatch ? parseInt(numMatch[1], 10) : 1;
|
|
// Generate dates starting from 2026-01-19 going backwards
|
|
const baseDate = new Date('2026-01-19');
|
|
baseDate.setDate(baseDate.getDate() - (postNum - 1));
|
|
const date = baseDate.toISOString().split('T')[0];
|
|
|
|
// Detect category
|
|
const category = detectCategory(filename, content);
|
|
|
|
// Cover image path
|
|
const coverImage = `/images/posts/${slug}/cover.jpg`;
|
|
|
|
return {
|
|
slug,
|
|
title,
|
|
date,
|
|
excerpt: excerpt || title,
|
|
coverImage,
|
|
category,
|
|
tags,
|
|
content,
|
|
};
|
|
} catch (error) {
|
|
console.error(`Error parsing ${filename}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function getAllBlogPosts(): BlogPost[] {
|
|
const cacheKey = 'blog:all-posts';
|
|
|
|
// Check cache first
|
|
const cachedPosts = cache.get<BlogPost[]>(cacheKey);
|
|
if (cachedPosts) {
|
|
return cachedPosts;
|
|
}
|
|
|
|
if (!fs.existsSync(BLOG_POSTS_DIR)) {
|
|
console.warn('Blog posts directory not found:', BLOG_POSTS_DIR);
|
|
return [];
|
|
}
|
|
|
|
const files = fs.readdirSync(BLOG_POSTS_DIR)
|
|
.filter(file => file.endsWith('.md'))
|
|
.sort();
|
|
|
|
const posts: BlogPost[] = [];
|
|
|
|
for (const file of files) {
|
|
try {
|
|
const filePath = path.join(BLOG_POSTS_DIR, file);
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const post = parseMarkdownPost(file, content);
|
|
if (post) {
|
|
posts.push(post);
|
|
}
|
|
} catch (error) {
|
|
// Skip files that fail to parse
|
|
console.warn(`Failed to parse blog post: ${file}`, error);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Sort by date descending (newest first)
|
|
const sortedPosts = posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
|
|
// Cache the results
|
|
cache.set(cacheKey, sortedPosts);
|
|
|
|
return sortedPosts;
|
|
}
|
|
|
|
export function getBlogPostBySlug(slug: string): BlogPost | null {
|
|
const posts = getAllBlogPosts();
|
|
return posts.find(post => post.slug === slug) || null;
|
|
}
|
|
|
|
export function getBlogPostSlugs(): string[] {
|
|
return getAllBlogPosts().map(post => post.slug);
|
|
}
|