Files
Portfolio/src/lib/blog.ts
T
2026-01-25 19:25:21 +01:00

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);
}