feat: Add professional portfolio images and Hero redesign
- Add 15 optimized WebP images for portfolio (21-53KB each) - Redesign Hero section with full-width background image - Add SSR support for Hero component - Update About section with new portrait image - Update Contact page with professional headshot - Add images to Services/Leistungen page - Add image optimization script (sharp) - Update translations for gallery section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
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(.+?)(?:\n\n|$)/s);
|
||||
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[] {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by date descending (newest first)
|
||||
return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user