(
+ fn: (data: T) => void,
+ intervalMs: number
+): (data: T) => void {
+ let lastData: T | null = null;
+ let scheduled = false;
+
+ return (data: T) => {
+ lastData = data;
+
+ if (!scheduled) {
+ scheduled = true;
+ setTimeout(() => {
+ fn(lastData!);
+ scheduled = false;
+ }, intervalMs);
+ }
+ };
+}
+
+// Verwendung: Max 10 Updates pro Sekunde
+const throttledUpdate = throttle((data) => {
+ setData(data);
+}, 100);
+```
+
+### 2. Delta Updates
+
+```typescript
+// Nur Änderungen senden
+function createDelta(previous: any, current: any): any {
+ const delta: any = {};
+
+ for (const key of Object.keys(current)) {
+ if (JSON.stringify(previous[key]) !== JSON.stringify(current[key])) {
+ delta[key] = current[key];
+ }
+ }
+
+ return Object.keys(delta).length > 0 ? delta : null;
+}
+```
+
+### 3. Web Worker für Datenverarbeitung
+
+```typescript
+// worker.ts
+self.onmessage = (event) => {
+ const { type, data } = event.data;
+
+ if (type === 'process-metrics') {
+ // Schwere Berechnung im Worker
+ const processed = processHeavyData(data);
+ self.postMessage({ type: 'metrics-processed', data: processed });
+ }
+};
+```
+
+---
+
+## Fazit
+
+Real-Time Dashboards mit WebSockets bieten:
+
+1. **Instant Updates**: Daten erscheinen sofort
+2. **Weniger Load**: Keine unnötigen Requests
+3. **Bessere UX**: Flüssige Animationen möglich
+4. **Skalierbarkeit**: Redis Pub/Sub für Multi-Server
+
+Für datenintensive Dashboards ist WebSocket-Push der Standard 2026.
+
+---
+
+## Bildprompts
+
+1. "Dashboard with live updating charts and metrics, data flowing in real-time, modern analytics interface"
+2. "WebSocket connection stream feeding into dashboard, visualization of data flow"
+3. "Multiple dashboard widgets updating simultaneously, business analytics, clean dark theme"
+
+---
+
+## Quellen
+
+- [WebSocket API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
+- [Redis Pub/Sub](https://redis.io/docs/manual/pubsub/)
+- [React Real-Time Charts](https://recharts.org/)
+- [Canvas API Performance](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas)
diff --git a/blog-posts/27-sse-vs-websocket.md b/blog-posts/27-sse-vs-websocket.md
new file mode 100644
index 0000000..8735489
--- /dev/null
+++ b/blog-posts/27-sse-vs-websocket.md
@@ -0,0 +1,470 @@
+# Server-Sent Events vs. WebSockets: Die richtige Wahl für Real-Time
+
+**Meta-Description:** Detaillierter Vergleich von SSE und WebSockets. Unidirektionale vs. bidirektionale Kommunikation, Use Cases und Performance-Analyse.
+
+**Keywords:** Server-Sent Events, WebSocket, SSE, Real-Time, Streaming, EventSource, HTTP Streaming, Push Notifications
+
+---
+
+## Einführung
+
+Beide Technologien ermöglichen Server-Push – aber sie lösen unterschiedliche Probleme. SSE für **einfaches Streaming**, WebSockets für **bidirektionale Kommunikation**.
+
+---
+
+## Quick Comparison
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ SSE vs. WEBSOCKET │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Server-Sent Events (SSE) WebSocket │
+│ ───────────────────────── ───────────────────── │
+│ │
+│ Direction: Direction: │
+│ Server → Client (only) Server ↔ Client (both) │
+│ │
+│ Protocol: Protocol: │
+│ HTTP/1.1 or HTTP/2 ws:// or wss:// │
+│ │
+│ Reconnection: Reconnection: │
+│ Built-in automatic Manual implementation │
+│ │
+│ Browser Support: Browser Support: │
+│ All modern (no IE) All modern + IE10+ │
+│ │
+│ Use Cases: Use Cases: │
+│ - LLM Streaming - Chat Applications │
+│ - Live Feeds - Gaming │
+│ - Notifications - Collaborative Editing │
+│ - Stock Tickers - Voice/Video │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Detaillierter Vergleich
+
+| Aspekt | SSE | WebSocket |
+|--------|-----|-----------|
+| **Richtung** | Unidirektional (Server→Client) | Bidirektional |
+| **Protokoll** | HTTP | WS/WSS |
+| **Datenformat** | Text (UTF-8) | Text + Binary |
+| **Reconnection** | Automatisch | Manuell |
+| **Event IDs** | Built-in | Manuell |
+| **Proxy/Firewall** | Besser (HTTP) | Problematisch |
+| **HTTP/2 Multiplexing** | Ja | Nein (eigene Connection) |
+| **Max Connections** | Browser-Limit (~6/Domain) | Unbegrenzt |
+| **Memory Overhead** | Niedriger | Höher |
+| **Setup-Komplexität** | Einfach | Komplexer |
+
+---
+
+## SSE Implementation
+
+### Server (Node.js/Express)
+
+```typescript
+// src/sse/server.ts
+import express from 'express';
+
+const app = express();
+
+app.get('/events', (req, res) => {
+ // SSE Headers
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+
+ // Flushes für sofortige Delivery
+ res.flushHeaders();
+
+ // Client-ID für Tracking
+ const clientId = Date.now();
+ console.log(`SSE client connected: ${clientId}`);
+
+ // Event senden
+ const sendEvent = (event: string, data: any, id?: string) => {
+ if (id) res.write(`id: ${id}\n`);
+ res.write(`event: ${event}\n`);
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
+ };
+
+ // Initial Event
+ sendEvent('connected', { clientId });
+
+ // Periodische Updates
+ const interval = setInterval(() => {
+ sendEvent('heartbeat', { timestamp: Date.now() });
+ }, 30000);
+
+ // Cleanup bei Disconnect
+ req.on('close', () => {
+ clearInterval(interval);
+ console.log(`SSE client disconnected: ${clientId}`);
+ });
+});
+
+app.listen(3000);
+```
+
+### Client (Browser)
+
+```typescript
+// src/sse/client.ts
+class SSEClient {
+ private eventSource: EventSource | null = null;
+ private reconnectAttempts = 0;
+
+ connect(url: string) {
+ this.eventSource = new EventSource(url);
+
+ this.eventSource.onopen = () => {
+ console.log('SSE connected');
+ this.reconnectAttempts = 0;
+ };
+
+ this.eventSource.onerror = (error) => {
+ console.error('SSE error:', error);
+ // EventSource reconnects automatically
+ };
+
+ // Named Events
+ this.eventSource.addEventListener('connected', (event) => {
+ const data = JSON.parse(event.data);
+ console.log('Client ID:', data.clientId);
+ });
+
+ this.eventSource.addEventListener('heartbeat', (event) => {
+ const data = JSON.parse(event.data);
+ console.log('Heartbeat:', data.timestamp);
+ });
+
+ // Generic message event
+ this.eventSource.onmessage = (event) => {
+ console.log('Message:', event.data);
+ };
+ }
+
+ disconnect() {
+ this.eventSource?.close();
+ }
+}
+```
+
+---
+
+## SSE für LLM Streaming
+
+### Perfekter Use Case: AI Response Streaming
+
+```typescript
+// src/api/chat.ts
+import Anthropic from '@anthropic-ai/sdk';
+
+app.post('/api/chat/stream', async (req, res) => {
+ const { message } = req.body;
+
+ // SSE Headers
+ res.setHeader('Content-Type', 'text/event-stream');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+
+ const anthropic = new Anthropic();
+
+ try {
+ const stream = await anthropic.messages.stream({
+ model: 'claude-3-haiku-20240307',
+ max_tokens: 1000,
+ messages: [{ role: 'user', content: message }]
+ });
+
+ for await (const event of stream) {
+ if (event.type === 'content_block_delta') {
+ res.write(`event: delta\n`);
+ res.write(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`);
+ }
+ }
+
+ // Stream Ende
+ res.write(`event: done\n`);
+ res.write(`data: {}\n\n`);
+ res.end();
+
+ } catch (error) {
+ res.write(`event: error\n`);
+ res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
+ res.end();
+ }
+});
+```
+
+### Client für LLM Streaming
+
+```typescript
+// src/hooks/useStreamingChat.ts
+export function useStreamingChat() {
+ const [response, setResponse] = useState('');
+ const [isStreaming, setIsStreaming] = useState(false);
+
+ const sendMessage = async (message: string) => {
+ setResponse('');
+ setIsStreaming(true);
+
+ const eventSource = new EventSource(
+ `/api/chat/stream?message=${encodeURIComponent(message)}`
+ );
+
+ eventSource.addEventListener('delta', (event) => {
+ const { text } = JSON.parse(event.data);
+ setResponse(prev => prev + text);
+ });
+
+ eventSource.addEventListener('done', () => {
+ eventSource.close();
+ setIsStreaming(false);
+ });
+
+ eventSource.addEventListener('error', (event) => {
+ console.error('Stream error');
+ eventSource.close();
+ setIsStreaming(false);
+ });
+ };
+
+ return { response, isStreaming, sendMessage };
+}
+```
+
+---
+
+## WebSocket Implementation
+
+### Server
+
+```typescript
+// src/websocket/server.ts
+import { WebSocketServer, WebSocket } from 'ws';
+
+const wss = new WebSocketServer({ port: 3001 });
+
+wss.on('connection', (ws: WebSocket) => {
+ console.log('WebSocket client connected');
+
+ // Bidirektionale Kommunikation
+ ws.on('message', (data) => {
+ const message = JSON.parse(data.toString());
+
+ // Echo oder Processing
+ ws.send(JSON.stringify({
+ type: 'response',
+ data: `Received: ${message.content}`
+ }));
+ });
+
+ // Server-initiated Push
+ const interval = setInterval(() => {
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.send(JSON.stringify({
+ type: 'heartbeat',
+ timestamp: Date.now()
+ }));
+ }
+ }, 30000);
+
+ ws.on('close', () => {
+ clearInterval(interval);
+ console.log('WebSocket client disconnected');
+ });
+});
+```
+
+### Client
+
+```typescript
+// src/websocket/client.ts
+class WebSocketClient {
+ private ws: WebSocket | null = null;
+ private reconnectTimeout: NodeJS.Timeout | null = null;
+
+ connect(url: string) {
+ this.ws = new WebSocket(url);
+
+ this.ws.onopen = () => {
+ console.log('WebSocket connected');
+ };
+
+ this.ws.onmessage = (event) => {
+ const message = JSON.parse(event.data);
+ this.handleMessage(message);
+ };
+
+ this.ws.onclose = () => {
+ console.log('WebSocket disconnected');
+ this.scheduleReconnect();
+ };
+
+ this.ws.onerror = (error) => {
+ console.error('WebSocket error:', error);
+ };
+ }
+
+ send(data: any) {
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ this.ws.send(JSON.stringify(data));
+ }
+ }
+
+ private handleMessage(message: any) {
+ switch (message.type) {
+ case 'response':
+ console.log('Response:', message.data);
+ break;
+ case 'heartbeat':
+ console.log('Heartbeat:', message.timestamp);
+ break;
+ }
+ }
+
+ private scheduleReconnect() {
+ this.reconnectTimeout = setTimeout(() => {
+ this.connect(this.ws?.url || '');
+ }, 3000);
+ }
+
+ disconnect() {
+ if (this.reconnectTimeout) {
+ clearTimeout(this.reconnectTimeout);
+ }
+ this.ws?.close();
+ }
+}
+```
+
+---
+
+## Entscheidungsmatrix
+
+```typescript
+function chooseProtocol(requirements: Requirements): 'SSE' | 'WebSocket' {
+ // WebSocket wenn bidirektional benötigt
+ if (requirements.bidirectional) {
+ return 'WebSocket';
+ }
+
+ // WebSocket für Binary Data
+ if (requirements.binaryData) {
+ return 'WebSocket';
+ }
+
+ // WebSocket für viele Connections
+ if (requirements.connectionsPerDomain > 6) {
+ return 'WebSocket';
+ }
+
+ // SSE für einfaches Streaming
+ if (requirements.useCase === 'llm-streaming' ||
+ requirements.useCase === 'notifications' ||
+ requirements.useCase === 'live-feed') {
+ return 'SSE';
+ }
+
+ // SSE für bessere Proxy-Kompatibilität
+ if (requirements.behindCorporateProxy) {
+ return 'SSE';
+ }
+
+ // Default: SSE (einfacher)
+ return 'SSE';
+}
+```
+
+---
+
+## Use Case Empfehlungen
+
+| Use Case | Empfehlung | Grund |
+|----------|------------|-------|
+| **LLM Response Streaming** | SSE | Unidirektional, einfach |
+| **Chat Application** | WebSocket | Bidirektional nötig |
+| **Live Notifications** | SSE | Server-Push genügt |
+| **Collaborative Editing** | WebSocket | Echtzzeit-Sync nötig |
+| **Stock Ticker** | SSE | Nur Server→Client |
+| **Online Gaming** | WebSocket | Low-Latency bidirektional |
+| **File Upload Progress** | WebSocket | Client muss auch senden |
+| **Social Feed Updates** | SSE | Einfacher Server-Push |
+
+---
+
+## Hybrid Approach
+
+```typescript
+// Kombiniere beide für optimale Ergebnisse
+class HybridRealtime {
+ private sse: EventSource | null = null;
+ private ws: WebSocket | null = null;
+
+ connect(sseUrl: string, wsUrl: string) {
+ // SSE für Server-Push (Notifications, Updates)
+ this.sse = new EventSource(sseUrl);
+ this.sse.addEventListener('notification', this.handleNotification);
+ this.sse.addEventListener('update', this.handleUpdate);
+
+ // WebSocket für bidirektionale Kommunikation (Chat, Actions)
+ this.ws = new WebSocket(wsUrl);
+ this.ws.onmessage = this.handleWebSocketMessage;
+ }
+
+ // Sende nur über WebSocket
+ sendMessage(content: string) {
+ this.ws?.send(JSON.stringify({ type: 'chat', content }));
+ }
+
+ // Empfange über beide Kanäle
+ private handleNotification = (event: MessageEvent) => {
+ // SSE Notification
+ };
+
+ private handleWebSocketMessage = (event: MessageEvent) => {
+ // WebSocket Response
+ };
+}
+```
+
+---
+
+## Fazit
+
+**Wähle SSE wenn:**
+- Nur Server→Client Kommunikation nötig
+- LLM/AI Response Streaming
+- Einfachheit gewünscht
+- Proxy-Kompatibilität wichtig
+
+**Wähle WebSocket wenn:**
+- Bidirektionale Kommunikation nötig
+- Binary Data übertragen wird
+- Sehr niedrige Latenz kritisch
+- Viele simultane Verbindungen
+
+Für LLM-Streaming ist SSE der klare Gewinner – einfacher, effizienter und perfekt für den Use Case.
+
+---
+
+## Bildprompts
+
+1. "Two data streams - one flowing one direction, one flowing both ways, comparison visualization"
+2. "Server pushing data to multiple clients, server-sent events concept, clean diagram"
+3. "Bidirectional communication tunnel between devices, WebSocket visualization, tech art"
+
+---
+
+## Quellen
+
+- [MDN: Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
+- [MDN: WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
+- [HTTP/2 Server Push vs SSE](https://www.smashingmagazine.com/2018/02/sse-websockets-data-flow-http2/)
+- [Choosing Between SSE and WebSocket](https://ably.com/blog/websockets-vs-sse)
diff --git a/blog-posts/28-nextjs-15-deep-dive.md b/blog-posts/28-nextjs-15-deep-dive.md
new file mode 100644
index 0000000..b8ddffa
--- /dev/null
+++ b/blog-posts/28-nextjs-15-deep-dive.md
@@ -0,0 +1,511 @@
+# Next.js 15 Deep Dive: React Server Components und Streaming in Production
+
+**Meta-Description:** Umfassende Analyse von Next.js 15 Features. React Server Components, Turbopack, Server Actions und Performance-Optimierung für 2026.
+
+**Keywords:** Next.js 15, React Server Components, RSC, Turbopack, Server Actions, App Router, Streaming SSR, React 19
+
+---
+
+## Einführung
+
+Next.js 15 ist nicht nur ein Update – es ist ein **Paradigmenwechsel**. Server-first, Streaming-enabled und mit React 19 als Basis definiert es Full-Stack React 2026 neu.
+
+> "2026 ist Next.js nicht mehr nur für Rendering – es ist für skalierbare, server-first, streaming-enabled Applications."
+
+---
+
+## Die wichtigsten Features
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ NEXT.JS 15 HIGHLIGHTS │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ 🚀 Turbopack (Stable) │
+│ └── 10x schneller als Webpack │
+│ │
+│ ⚛️ React 19 Support │
+│ └── Server Components, Actions, Compiler │
+│ │
+│ 🔄 Async Request APIs │
+│ └── cookies(), headers(), params() sind async │
+│ │
+│ 📦 Caching Changes │
+│ └── fetch, GET Routes nicht mehr default cached │
+│ │
+│ ⚡ Partial Prerendering (Experimental) │
+│ └── Static Shell + Dynamic Streaming │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## React Server Components (RSC)
+
+### Das Konzept
+
+```tsx
+// Server Component (Default in App Router)
+// Kein 'use client' = Server Component
+async function ProductPage({ params }: { params: { id: string } }) {
+ // Direkt DB-Zugriff - kein API nötig!
+ const product = await prisma.product.findUnique({
+ where: { id: params.id }
+ });
+
+ // Dieses JavaScript wird NICHT an den Client gesendet
+ const analytics = calculateComplexAnalytics(product);
+
+ return (
+
+
{product.name}
+
+ {/* Client Component für Interaktivität */}
+
+
+ );
+}
+```
+
+### Server vs. Client Components
+
+```tsx
+// ❌ Don't: Alles als Client Component
+'use client';
+export function ProductList() {
+ const [products, setProducts] = useState([]);
+
+ useEffect(() => {
+ fetch('/api/products').then(r => r.json()).then(setProducts);
+ }, []);
+
+ return products.map(p => );
+}
+
+// ✅ Do: Server Component mit Client Component für Interaktivität
+// Server Component (app/products/page.tsx)
+async function ProductsPage() {
+ const products = await prisma.product.findMany();
+
+ return (
+
+ {products.map(p => (
+
+ {/* Client Component nur wo nötig */}
+
+
+ ))}
+
+ );
+}
+
+// Client Component (components/FavoriteButton.tsx)
+'use client';
+export function FavoriteButton({ productId }: { productId: string }) {
+ const [isFavorite, setIsFavorite] = useState(false);
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Server Actions
+
+### Form Handling ohne API Route
+
+```tsx
+// app/actions.ts
+'use server';
+
+import { revalidatePath } from 'next/cache';
+import { redirect } from 'next/navigation';
+
+export async function createProduct(formData: FormData) {
+ const name = formData.get('name') as string;
+ const price = parseFloat(formData.get('price') as string);
+
+ // Validation
+ if (!name || !price) {
+ return { error: 'Name und Preis erforderlich' };
+ }
+
+ // Direkt DB-Operation
+ const product = await prisma.product.create({
+ data: { name, price }
+ });
+
+ // Cache invalidieren
+ revalidatePath('/products');
+
+ // Redirect
+ redirect(`/products/${product.id}`);
+}
+```
+
+```tsx
+// app/products/new/page.tsx
+import { createProduct } from '../actions';
+
+export default function NewProductPage() {
+ return (
+
+ );
+}
+```
+
+### Server Actions mit useActionState (React 19)
+
+```tsx
+'use client';
+
+import { useActionState } from 'react';
+import { createProduct } from '../actions';
+
+export function ProductForm() {
+ const [state, formAction, isPending] = useActionState(
+ createProduct,
+ { error: null }
+ );
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Streaming SSR
+
+### Suspense für Progressive Loading
+
+```tsx
+// app/dashboard/page.tsx
+import { Suspense } from 'react';
+
+export default function DashboardPage() {
+ return (
+
+
Dashboard
+
+ {/* Schneller Content zuerst */}
+ }>
+
+
+
+ {/* Langsamer Content streamt nach */}
+ }>
+
+
+
+ }>
+
+
+
+ );
+}
+
+// Diese Components können unabhängig laden
+async function Stats() {
+ const stats = await fetchStats(); // 100ms
+ return ;
+}
+
+async function RevenueChart() {
+ const data = await fetchRevenueData(); // 500ms
+ return ;
+}
+
+async function RecentOrders() {
+ const orders = await fetchOrders(); // 300ms
+ return ;
+}
+```
+
+### Loading UI
+
+```tsx
+// app/dashboard/loading.tsx
+export default function DashboardLoading() {
+ return (
+
+
+
+ {[...Array(4)].map((_, i) => (
+
+ ))}
+
+
+
+ );
+}
+```
+
+---
+
+## Async Request APIs (Breaking Change)
+
+```tsx
+// Next.js 14
+import { cookies, headers } from 'next/headers';
+
+export default function Page() {
+ const cookieStore = cookies(); // Synchron
+ const headersList = headers(); // Synchron
+ return ...
;
+}
+
+// Next.js 15
+import { cookies, headers } from 'next/headers';
+
+export default async function Page() {
+ const cookieStore = await cookies(); // Async!
+ const headersList = await headers(); // Async!
+ return ...
;
+}
+```
+
+### Migration Codemod
+
+```bash
+npx @next/codemod@canary upgrade latest
+```
+
+---
+
+## Caching Changes
+
+### Neues Default-Verhalten
+
+```tsx
+// Next.js 14: Gecached by default
+const data = await fetch('https://api.example.com/data');
+
+// Next.js 15: NICHT gecached by default
+const data = await fetch('https://api.example.com/data');
+
+// Explizit cachen
+const cachedData = await fetch('https://api.example.com/data', {
+ cache: 'force-cache' // oder next: { revalidate: 3600 }
+});
+```
+
+### Route Handler Caching
+
+```tsx
+// app/api/data/route.ts
+
+// Next.js 15: GET ist NICHT mehr default cached
+export async function GET() {
+ const data = await fetchData();
+ return Response.json(data);
+}
+
+// Explizit cachen
+export const dynamic = 'force-static';
+export async function GET() {
+ // ...
+}
+```
+
+---
+
+## Turbopack in Production
+
+### Aktivierung
+
+```bash
+# next.config.js ist nicht nötig für Dev
+next dev --turbopack
+
+# Für Build (experimentell)
+TURBOPACK=1 next build
+```
+
+### Performance-Vergleich
+
+| Metrik | Webpack | Turbopack | Verbesserung |
+|--------|---------|-----------|--------------|
+| **Cold Start** | 8.5s | 1.2s | 7x schneller |
+| **HMR** | 500ms | 50ms | 10x schneller |
+| **Full Rebuild** | 45s | 8s | 5.6x schneller |
+
+---
+
+## Performance Best Practices
+
+### 1. Component Composition
+
+```tsx
+// ❌ Großes Client Component
+'use client';
+export function Dashboard() {
+ const [data, setData] = useState(null);
+ // Viel Logik...
+ return ...
;
+}
+
+// ✅ Server Component mit kleinen Client Components
+export async function Dashboard() {
+ const data = await fetchData();
+
+ return (
+
+
+
+
+ {/* Client */}
+ {/* Client */}
+
+
+ );
+}
+```
+
+### 2. Data Fetching Patterns
+
+```tsx
+// ❌ Sequential Fetching
+async function Page() {
+ const user = await fetchUser();
+ const posts = await fetchPosts(user.id);
+ const comments = await fetchComments(posts.map(p => p.id));
+ return ...
;
+}
+
+// ✅ Parallel Fetching
+async function Page() {
+ const user = await fetchUser();
+
+ // Parallel fetchen
+ const [posts, friends] = await Promise.all([
+ fetchPosts(user.id),
+ fetchFriends(user.id)
+ ]);
+
+ return ...
;
+}
+```
+
+### 3. Streaming Optimization
+
+```tsx
+// Optimale Streaming-Architektur
+export default function ProductPage({ params }) {
+ return (
+
+ {/* Sofort gerendert */}
+
+
+ {/* Schnelle Daten zuerst */}
+
}>
+
+
+
+ {/* Langsame Daten später */}
+
}>
+
+
+
+
}>
+
+
+
+ );
+}
+```
+
+---
+
+## Partial Prerendering (Experimental)
+
+```tsx
+// next.config.js
+module.exports = {
+ experimental: {
+ ppr: true
+ }
+};
+
+// app/page.tsx
+import { Suspense } from 'react';
+
+export default function HomePage() {
+ return (
+
+ {/* Statisch prerendered */}
+
+
+
+ {/* Dynamisch gestreamt */}
+
}>
+
+
+
+ {/* Statisch */}
+
+
+ );
+}
+```
+
+---
+
+## Security Notes
+
+Zwei kritische CVEs wurden für Next.js gemeldet:
+- **CVE-2025-55184**: DoS via Server Components
+- **CVE-2025-55183**: Source Code Exposure
+
+**Sofortiges Update auf die neueste Version empfohlen!**
+
+---
+
+## Fazit
+
+Next.js 15 verändert wie wir React-Anwendungen bauen:
+
+1. **Server-First**: RSC als Default reduziert Client-Bundle
+2. **Streaming**: Bessere Time-to-First-Byte
+3. **Server Actions**: Keine API-Routes mehr für Forms
+4. **Turbopack**: Dramatisch schnellere Builds
+
+2026 ist Next.js die Plattform für moderne Full-Stack React.
+
+---
+
+## Bildprompts
+
+1. "React components streaming from server to client, data flow visualization, modern web architecture"
+2. "Next.js logo with speed lines, turbo boost effect, performance concept"
+3. "Server and client components merging, hybrid rendering visualization, clean tech diagram"
+
+---
+
+## Quellen
+
+- [Next.js 15 Release Blog](https://nextjs.org/blog/next-15)
+- [Next.js Documentation](https://nextjs.org/docs)
+- [React Server Components](https://nextjs.org/docs/app/getting-started/server-and-client-components)
+- [Medium: Next.js 15 Deep Dive](https://medium.com/@EnaModernCoder/next-js-15-deep-dive-the-hidden-power-behind-the-latest-evolution-of-react-frameworks-e792e3e6e3ae)
diff --git a/blog-posts/29-react-server-components.md b/blog-posts/29-react-server-components.md
new file mode 100644
index 0000000..4f0d485
--- /dev/null
+++ b/blog-posts/29-react-server-components.md
@@ -0,0 +1,496 @@
+# React Server Components: Das neue Mental Model für 2026
+
+**Meta-Description:** Umfassendes Verständnis von React Server Components. Composition Patterns, Data Fetching, Client/Server Boundary und Migration von Client-only Apps.
+
+**Keywords:** React Server Components, RSC, Server Components, React 19, Next.js, Client Components, Hybrid Rendering
+
+---
+
+## Einführung
+
+React Server Components (RSC) sind nicht nur ein Feature – sie sind ein **neues Mental Model**. Server Components rendern auf dem Server, senden **kein JavaScript** zum Client und können direkt auf Backend-Ressourcen zugreifen.
+
+---
+
+## Das Mental Model
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ REACT SERVER COMPONENTS MODEL │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ SERVER CLIENT │
+│ ────────────────────── ────────────────────── │
+│ ┌─────────────────────┐ │
+│ │ Server Component │ ┌─────────────────────┐ │
+│ │ ├── DB Access │ →→→ │ HTML + RSC Payload │ │
+│ │ ├── File System │ │ (No JS Bundle!) │ │
+│ │ ├── API Calls │ └─────────────────────┘ │
+│ │ └── Heavy Compute │ │
+│ └─────────────────────┘ │
+│ │ │
+│ ▼ │
+│ ┌─────────────────────┐ ┌─────────────────────┐ │
+│ │ Client Component │ →→→ │ JS Bundle │ │
+│ │ ├── useState │ │ (Minimal!) │ │
+│ │ ├── useEffect │ └─────────────────────┘ │
+│ │ ├── Event Handlers │ │
+│ │ └── Browser APIs │ │
+│ └─────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Server Components vs. Client Components
+
+| Aspekt | Server Component | Client Component |
+|--------|------------------|------------------|
+| **Rendering** | Server | Server + Client |
+| **JavaScript** | 0 bytes zum Client | Wird gebundelt |
+| **State** | Nein (kein useState) | Ja |
+| **Effects** | Nein (kein useEffect) | Ja |
+| **Event Handlers** | Nein | Ja |
+| **Browser APIs** | Nein | Ja |
+| **DB/Filesystem** | Ja (direkt) | Nein |
+| **async/await** | Ja (top-level) | Nein |
+
+---
+
+## Composition Patterns
+
+### Pattern 1: Server Component als Wrapper
+
+```tsx
+// app/products/page.tsx (Server Component)
+import { ProductGrid } from '@/components/ProductGrid';
+import { FilterPanel } from '@/components/FilterPanel';
+
+async function ProductsPage() {
+ // Server-side data fetching
+ const products = await prisma.product.findMany({
+ include: { category: true }
+ });
+
+ return (
+
+ {/* Client Component für Interaktivität */}
+
p.category)} />
+
+ {/* Server Component rendert Produkte */}
+
+
+ );
+}
+```
+
+### Pattern 2: Client Component Children
+
+```tsx
+// components/Modal.tsx (Client Component)
+'use client';
+
+import { useState } from 'react';
+
+export function Modal({ children }: { children: React.ReactNode }) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+ <>
+
+
+ {isOpen && (
+
+ {/* Children können Server Components sein! */}
+ {children}
+
+
+ )}
+ >
+ );
+}
+
+// Verwendung (Server Component)
+async function ProductPage({ params }) {
+ const product = await fetchProduct(params.id);
+
+ return (
+
+
+ {/* Server Component als Child von Client Component */}
+
+
+
+ );
+}
+```
+
+### Pattern 3: Props Serialization
+
+```tsx
+// ❌ Fehler: Funktionen können nicht serialisiert werden
+async function ProductCard({ product }) {
+ return (
+
+ {product.name}
+ {/* Fehler: onClick kann nicht zum Client */}
+
+
+ );
+}
+
+// ✅ Richtig: Interaktivität in Client Component
+async function ProductCard({ product }) {
+ return (
+
+ {product.name}
+ {/* Client Component für Click Handler */}
+
+
+ );
+}
+
+// components/ProductActions.tsx
+'use client';
+export function ProductActions({ productId }: { productId: string }) {
+ return (
+
+ );
+}
+```
+
+---
+
+## Data Fetching Patterns
+
+### Pattern 1: Parallel Data Fetching
+
+```tsx
+// ❌ Sequential (langsam)
+async function Dashboard() {
+ const user = await fetchUser();
+ const stats = await fetchStats(); // Wartet auf user
+ const notifications = await fetchNotifications(); // Wartet auf stats
+
+ return ;
+}
+
+// ✅ Parallel (schnell)
+async function Dashboard() {
+ const [user, stats, notifications] = await Promise.all([
+ fetchUser(),
+ fetchStats(),
+ fetchNotifications()
+ ]);
+
+ return ;
+}
+```
+
+### Pattern 2: Data Colocation
+
+```tsx
+// Jede Component holt ihre eigenen Daten
+// (Next.js dedupliziert automatisch gleiche Requests)
+
+async function UserProfile() {
+ const user = await fetchUser(); // Request 1
+ return ;
+}
+
+async function UserPosts() {
+ const user = await fetchUser(); // Dedupliziert!
+ const posts = await fetchPosts(user.id);
+ return ;
+}
+
+async function UserPage() {
+ return (
+
+
+
+
+ );
+}
+```
+
+### Pattern 3: Streaming mit Suspense
+
+```tsx
+import { Suspense } from 'react';
+
+async function ProductPage({ params }) {
+ return (
+
+ {/* Sofort gerendert */}
+
+
+ {/* Streamt sobald ready */}
+
}>
+
+
+
+ {/* Streamt unabhängig */}
+
}>
+
+
+
+ );
+}
+
+async function ProductDetails({ id }) {
+ // Diese Funktion blockiert nicht die ganze Page
+ await new Promise(r => setTimeout(r, 1000)); // Simuliert DB-Call
+ const details = await fetchProductDetails(id);
+ return ;
+}
+```
+
+---
+
+## State Management mit Server Components
+
+### Lifting State Up to URL
+
+```tsx
+// Server Component liest State aus URL
+async function ProductsPage({
+ searchParams
+}: {
+ searchParams: { sort?: string; filter?: string }
+}) {
+ const products = await prisma.product.findMany({
+ orderBy: searchParams.sort ? { [searchParams.sort]: 'asc' } : undefined,
+ where: searchParams.filter
+ ? { category: searchParams.filter }
+ : undefined
+ });
+
+ return (
+
+ );
+}
+
+// Client Component für URL-Updates
+'use client';
+import { useRouter, useSearchParams } from 'next/navigation';
+
+function FilterBar({ currentSort, currentFilter }) {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const updateFilter = (key: string, value: string) => {
+ const params = new URLSearchParams(searchParams);
+ params.set(key, value);
+ router.push(`?${params.toString()}`);
+ };
+
+ return (
+
+
+
+ );
+}
+```
+
+### Server Actions für Mutations
+
+```tsx
+// Server Action
+'use server';
+import { revalidatePath } from 'next/cache';
+
+async function addToFavorites(productId: string) {
+ await prisma.favorite.create({
+ data: {
+ productId,
+ userId: getCurrentUserId()
+ }
+ });
+
+ revalidatePath('/favorites');
+}
+
+// Client Component nutzt Server Action
+'use client';
+import { useTransition } from 'react';
+
+function FavoriteButton({ productId }) {
+ const [isPending, startTransition] = useTransition();
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Migration Guide: Client → Server
+
+### Schritt 1: Identifiziere reine Daten-Components
+
+```tsx
+// Vorher: Client Component
+'use client';
+import { useEffect, useState } from 'react';
+
+function ProductList() {
+ const [products, setProducts] = useState([]);
+
+ useEffect(() => {
+ fetch('/api/products').then(r => r.json()).then(setProducts);
+ }, []);
+
+ return ;
+}
+
+// Nachher: Server Component
+async function ProductList() {
+ const products = await prisma.product.findMany();
+ return ;
+}
+```
+
+### Schritt 2: Extrahiere interaktive Teile
+
+```tsx
+// Vorher: Alles Client
+'use client';
+function ProductCard({ product }) {
+ const [count, setCount] = useState(1);
+
+ return (
+
+
{product.name}
+
{product.description}
+
{product.price} €
+
+
setCount(Number(e.target.value))}
+ />
+
+
+ );
+}
+
+// Nachher: Server + Client
+// ProductCard.tsx (Server)
+function ProductCard({ product }) {
+ return (
+
+
{product.name}
+
{product.description}
+
{product.price} €
+
+ {/* Nur interaktiver Teil als Client */}
+
+
+ );
+}
+
+// AddToCartForm.tsx (Client)
+'use client';
+function AddToCartForm({ productId }) {
+ const [count, setCount] = useState(1);
+
+ return (
+ <>
+ setCount(Number(e.target.value))}
+ />
+
+ >
+ );
+}
+```
+
+---
+
+## Performance-Vorteile
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ BUNDLE SIZE COMPARISON │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Client-Only App (alles 'use client'): │
+│ ├── React: 40KB │
+│ ├── Components: 150KB │
+│ ├── Dependencies: 200KB │
+│ └── Total: ~390KB JS │
+│ │
+│ RSC-First App (Server wo möglich): │
+│ ├── React: 40KB │
+│ ├── Client Components: 30KB │
+│ ├── Dependencies (Client only): 50KB │
+│ └── Total: ~120KB JS │
+│ │
+│ Reduktion: 70%! │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Fazit
+
+React Server Components transformieren wie wir React-Apps bauen:
+
+1. **Server-First**: Default auf Server, Client nur wo nötig
+2. **Zero Bundle**: Server Components senden kein JS
+3. **Direct Access**: DB, Filesystem direkt nutzbar
+4. **Composition**: Server + Client Components kombinierbar
+
+Das neue Mental Model: "Kann das auf dem Server bleiben?"
+
+---
+
+## Bildprompts
+
+1. "Two puzzle pieces fitting together - server and client components, React logo in center"
+2. "Data flowing from database through server component to client, simplified architecture"
+3. "Bundle size comparison chart - before and after Server Components, dramatic reduction"
+
+---
+
+## Quellen
+
+- [React Server Components RFC](https://github.com/reactjs/rfcs/blob/main/text/0188-server-components.md)
+- [Next.js Server Components Docs](https://nextjs.org/docs/app/getting-started/server-and-client-components)
+- [Vercel: Understanding React Server Components](https://vercel.com/blog/understanding-react-server-components)
+- [Dan Abramov: RSC from Scratch](https://github.com/reactwg/server-components/discussions/5)
diff --git a/blog-posts/30-streaming-ui-patterns.md b/blog-posts/30-streaming-ui-patterns.md
new file mode 100644
index 0000000..d2fedec
--- /dev/null
+++ b/blog-posts/30-streaming-ui-patterns.md
@@ -0,0 +1,495 @@
+# Streaming UI Patterns: Progressive Loading für moderne Web-Apps
+
+**Meta-Description:** Design Patterns für Streaming UI mit React Suspense. Skeleton Loading, Progressive Enhancement und optimale User Experience bei langsamen Daten.
+
+**Keywords:** Streaming UI, Suspense, Skeleton Loading, Progressive Loading, React Streaming, SSR Streaming, Loading States
+
+---
+
+## Einführung
+
+Streaming UI bedeutet: **Zeige sofort was du hast, streame den Rest nach**. Statt einer leeren Seite oder einem Spinner sieht der User sofort Inhalte – während langsame Daten im Hintergrund laden.
+
+---
+
+## Das Streaming-Prinzip
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ STREAMING VS. BLOCKING │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Blocking (Traditionell): │
+│ ┌────────────────────────────────────────────────────┐ │
+│ │ [Spinner 3s] → [Komplette Seite] │ │
+│ └────────────────────────────────────────────────────┘ │
+│ User wartet 3 Sekunden auf irgendetwas │
+│ │
+│ Streaming (Modern): │
+│ ┌────────────────────────────────────────────────────┐ │
+│ │ [Header] ─ sofort │ │
+│ │ [Nav] ─ sofort │ │
+│ │ [Hero] ─ 100ms │ │
+│ │ [Stats] ─ [Skeleton] → [Daten] ─ 500ms │ │
+│ │ [Chart] ─ [Skeleton] → [Daten] ─ 1s │ │
+│ │ [Table] ─ [Skeleton] → [Daten] ─ 2s │ │
+│ │ [Footer] ─ sofort │ │
+│ └────────────────────────────────────────────────────┘ │
+│ User sieht sofort Inhalte, Details laden progressiv │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Pattern 1: Suspense Boundaries
+
+### Strategische Platzierung
+
+```tsx
+// app/dashboard/page.tsx
+import { Suspense } from 'react';
+
+export default function DashboardPage() {
+ return (
+
+ {/* Sofort gerendert - keine Suspense */}
+
+
+
+ {/* Schnelle Daten */}
+ }>
+
+
+
+
+ {/* Mittlere Latenz - unabhängig voneinander */}
+ }>
+
+
+
+ }>
+
+
+
+
+ {/* Langsame Daten - lädt zuletzt */}
+ }>
+
+
+
+ {/* Statisch */}
+
+
+ );
+}
+```
+
+### Nested Suspense für feinere Kontrolle
+
+```tsx
+function ProductSection() {
+ return (
+
+ }>
+
+
+ {/* Nested: Reviews laden nach Produkten */}
+ }>
+
+
+
+
+ );
+}
+```
+
+---
+
+## Pattern 2: Skeleton Components
+
+### Anatomy-preserving Skeletons
+
+```tsx
+// components/skeletons/ProductCardSkeleton.tsx
+export function ProductCardSkeleton() {
+ return (
+
+ {/* Bild */}
+
+
+ {/* Titel */}
+
+
+ {/* Beschreibung */}
+
+
+ {/* Preis + Button */}
+
+
+ );
+}
+
+// Grid Skeleton
+export function ProductGridSkeleton({ count = 6 }) {
+ return (
+
+ {Array.from({ length: count }).map((_, i) => (
+
+ ))}
+
+ );
+}
+```
+
+### Shimmer Effect
+
+```css
+/* styles/skeleton.css */
+.skeleton-shimmer {
+ background: linear-gradient(
+ 90deg,
+ #f0f0f0 25%,
+ #e0e0e0 50%,
+ #f0f0f0 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+}
+
+@keyframes shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+```
+
+```tsx
+// Skeleton mit Shimmer
+export function ShimmerSkeleton({ className }: { className: string }) {
+ return ;
+}
+```
+
+---
+
+## Pattern 3: Optimistic Updates
+
+### Sofortige UI-Reaktion
+
+```tsx
+'use client';
+
+import { useOptimistic, useTransition } from 'react';
+import { addToFavorites } from './actions';
+
+function FavoriteButton({ productId, initialFavorited }) {
+ const [isPending, startTransition] = useTransition();
+
+ // Optimistic State
+ const [optimisticFavorited, addOptimistic] = useOptimistic(
+ initialFavorited,
+ (current, newValue: boolean) => newValue
+ );
+
+ const handleClick = () => {
+ // Sofort UI updaten
+ addOptimistic(!optimisticFavorited);
+
+ // Server-Action im Hintergrund
+ startTransition(async () => {
+ await addToFavorites(productId, !optimisticFavorited);
+ });
+ };
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Pattern 4: Loading Hierarchies
+
+### loading.tsx für Route-Level
+
+```tsx
+// app/products/loading.tsx
+export default function ProductsLoading() {
+ return (
+
+ {/* Header ist statisch */}
+
+
+ {/* Filter Bar Skeleton */}
+
+
+ {/* Product Grid Skeleton */}
+
+
+ );
+}
+```
+
+### Hierarchische Loading States
+
+```tsx
+// Layout mit persistentem Skeleton
+export default function ProductsLayout({
+ children
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+ {/* Immer sichtbar */}
+
+
+
+ {/* Content lädt */}
+
+ {children}
+
+
+ );
+}
+```
+
+---
+
+## Pattern 5: Streaming mit LLM
+
+### AI-Response Streaming UI
+
+```tsx
+'use client';
+
+import { useStreamingChat } from '@/hooks/useStreamingChat';
+
+function ChatInterface() {
+ const { response, isStreaming, sendMessage } = useStreamingChat();
+ const [input, setInput] = useState('');
+
+ return (
+
+
+ {/* Streaming Response */}
+ {(response || isStreaming) && (
+
+ {response}
+ {isStreaming && (
+
+ )}
+
+ )}
+
+
+
+
+ );
+}
+```
+
+### Partial Content während Streaming
+
+```tsx
+function AIAnalysisCard() {
+ const { analysis, isAnalyzing, sections } = useAIAnalysis();
+
+ return (
+
+ {/* Sections erscheinen progressiv */}
+ {sections.map((section, i) => (
+
+
{section.title}
+ {section.loaded ? (
+
{section.content}
+ ) : (
+
+ )}
+
+ ))}
+
+ {isAnalyzing && (
+
+ Analysiere... {sections.filter(s => s.loaded).length}/{sections.length}
+
+ )}
+
+ );
+}
+```
+
+---
+
+## Pattern 6: Error Boundaries mit Fallback
+
+```tsx
+// components/ErrorBoundary.tsx
+'use client';
+
+import { useEffect } from 'react';
+
+export function ErrorBoundary({
+ error,
+ reset
+}: {
+ error: Error;
+ reset: () => void;
+}) {
+ useEffect(() => {
+ console.error('Error:', error);
+ }, [error]);
+
+ return (
+
+
+ Etwas ist schiefgelaufen
+
+
+ {error.message}
+
+
+
+ );
+}
+
+// error.tsx für Route-Level
+export default function ProductsError({
+ error,
+ reset
+}: {
+ error: Error;
+ reset: () => void;
+}) {
+ return ;
+}
+```
+
+---
+
+## Performance-Metriken
+
+| Metrik | Blocking | Streaming | Verbesserung |
+|--------|----------|-----------|--------------|
+| **FCP** | 3.2s | 0.8s | 75% schneller |
+| **LCP** | 3.5s | 1.2s | 66% schneller |
+| **TTI** | 4.0s | 2.0s | 50% schneller |
+| **CLS** | 0.15 | 0.02 | 87% besser |
+
+---
+
+## Best Practices
+
+### Do's
+
+```tsx
+// ✅ Unabhängige Suspense Boundaries
+}>
+}>
+
+// ✅ Skeleton passt zur echten Komponente
+}>
+
+
+
+// ✅ Static Content außerhalb von Suspense
+ {/* Kein Suspense nötig */}
+
+
+
+ {/* Kein Suspense nötig */}
+```
+
+### Don'ts
+
+```tsx
+// ❌ Eine große Suspense für alles
+}>
+
+
+
+// ❌ Generischer Spinner statt Skeleton
+}>
+
+
+
+// ❌ Suspense um statischen Content
+
+
+
+```
+
+---
+
+## Fazit
+
+Streaming UI Patterns verbessern UX dramatisch:
+
+1. **Sofortige Inhalte**: User sieht nie eine leere Seite
+2. **Progressive Loading**: Wichtiges zuerst, Details später
+3. **Perceived Performance**: App fühlt sich schneller an
+4. **Resilience**: Langsame Teile blockieren nicht den Rest
+
+Die Zukunft ist nicht "Laden oder Geladen" – es ist ein Spektrum.
+
+---
+
+## Bildprompts
+
+1. "Website loading progressively in sections, waterfall effect, UI/UX concept"
+2. "Skeleton screens transforming into real content, animation sequence"
+3. "User happily browsing while content streams in background, modern web experience"
+
+---
+
+## Quellen
+
+- [React Suspense Documentation](https://react.dev/reference/react/Suspense)
+- [Next.js Loading UI](https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming)
+- [Vercel: Streaming SSR](https://vercel.com/blog/streaming-server-rendering-with-suspense)
+- [Web.dev: Skeleton Screens](https://web.dev/articles/skeleton-screens)
diff --git a/blog-posts/31-typescript-6-advanced-patterns.md b/blog-posts/31-typescript-6-advanced-patterns.md
new file mode 100644
index 0000000..eebd8cf
--- /dev/null
+++ b/blog-posts/31-typescript-6-advanced-patterns.md
@@ -0,0 +1,421 @@
+# TypeScript 6.0 und 7.0: Advanced Patterns für 2026
+
+**Meta-Description:** TypeScript 6.0/7.0 Features und fortgeschrittene Patterns. Der neue Go-basierte Compiler, Resource Management mit using und Best Practices für Large-Scale Apps.
+
+**Keywords:** TypeScript 6, TypeScript 7, Project Corsa, Advanced TypeScript, Type Safety, Go Compiler, Utility Types, TypeScript Patterns
+
+---
+
+## Einführung
+
+2026 ist TypeScript nicht mehr optional – es ist **der Standard**. TypeScript ist jetzt die meistgenutzte Sprache auf GitHub mit 2.6 Millionen monatlichen Contributors (+66% YoY). TypeScript 6.0 und 7.0 bringen massive Performance-Verbesserungen.
+
+---
+
+## TypeScript 6.0 und 7.0 Timeline
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ TYPESCRIPT ROADMAP 2026 │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ TypeScript 6.0 (Früh 2026) │
+│ ───────────────────────── │
+│ • Letzte Version auf alter Codebase │
+│ • Bridge-Release zu 7.0 │
+│ • Deprecations für 7.0-Kompatibilität │
+│ • using Keyword für Resource Management │
+│ │
+│ TypeScript 7.0 (Mitte 2026) - "Project Corsa" │
+│ ───────────────────────────────────────────── │
+│ • Neuer Go-basierter Compiler (~10x schneller) │
+│ • strict-by-default │
+│ • ES5 Target entfernt │
+│ • AMD/UMD/SystemJS Module entfernt │
+│ • Classic Node Resolution entfernt │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Performance: Project Corsa
+
+### Benchmark-Vergleich
+
+| Codebase | TS 5.x | TS 7.0 (Go) | Speedup |
+|----------|--------|-------------|---------|
+| **VS Code** | 77.8s | 7.5s | 10.4x |
+| **Playwright** | 11.1s | 1.1s | 10x |
+| **Large Monorepo** | 120s | 12s | 10x |
+
+### Was bedeutet das?
+
+```typescript
+// Früher: Warten auf Type-Checking
+// → IDE-Lag, langsame Builds, frustrierte Devs
+
+// Mit TS 7.0: Instant Feedback
+// → Type-Checking in Millisekunden
+// → Editor-Responsiveness dramatisch verbessert
+```
+
+---
+
+## Resource Management mit `using`
+
+### Das Problem
+
+```typescript
+// ❌ Manuelles Cleanup - fehleranfällig
+async function processData() {
+ const connection = await database.connect();
+ try {
+ const file = await fs.open('data.txt', 'r');
+ try {
+ // Verarbeitung...
+ return result;
+ } finally {
+ await file.close();
+ }
+ } finally {
+ await connection.close();
+ }
+}
+```
+
+### Die Lösung: `using`
+
+```typescript
+// ✅ Automatisches Cleanup mit using
+async function processData() {
+ using connection = await database.connect();
+ using file = await fs.open('data.txt', 'r');
+
+ // Verarbeitung...
+ return result;
+ // connection und file werden automatisch geschlossen!
+}
+
+// Disposable Interface
+interface Disposable {
+ [Symbol.dispose](): void;
+}
+
+interface AsyncDisposable {
+ [Symbol.asyncDispose](): Promise;
+}
+
+// Eigene Disposable Klasse
+class DatabaseConnection implements AsyncDisposable {
+ async [Symbol.asyncDispose]() {
+ await this.close();
+ console.log('Connection closed');
+ }
+}
+```
+
+---
+
+## Advanced Utility Types
+
+### Conditional Types
+
+```typescript
+// Bedingte Typen für flexible APIs
+type ApiResponse = T extends Array
+ ? { items: U[]; total: number }
+ : { data: T };
+
+// Verwendung
+type UserResponse = ApiResponse; // { data: User }
+type UsersResponse = ApiResponse; // { items: User[]; total: number }
+```
+
+### Template Literal Types
+
+```typescript
+// Typsichere Event-Namen
+type EventName = `on${Capitalize}`;
+
+type MouseEvents = EventName<'click' | 'move' | 'enter'>;
+// "onClick" | "onMove" | "onEnter"
+
+// API Route Builder
+type ApiRoute<
+ Method extends 'GET' | 'POST' | 'PUT' | 'DELETE',
+ Path extends string
+> = `${Method} ${Path}`;
+
+type UserRoutes =
+ | ApiRoute<'GET', '/users'>
+ | ApiRoute<'POST', '/users'>
+ | ApiRoute<'GET', '/users/:id'>
+ | ApiRoute<'PUT', '/users/:id'>
+ | ApiRoute<'DELETE', '/users/:id'>;
+```
+
+### Mapped Types mit Modifiers
+
+```typescript
+// Alle Properties optional und readonly
+type DeepPartialReadonly = {
+ readonly [P in keyof T]?: T[P] extends object
+ ? DeepPartialReadonly
+ : T[P];
+};
+
+// Nur bestimmte Keys required
+type RequiredKeys = Omit & Required>;
+
+type User = {
+ id?: string;
+ name?: string;
+ email?: string;
+};
+
+type UserWithEmail = RequiredKeys;
+// { id?: string; name?: string; email: string }
+```
+
+---
+
+## Const Type Parameters
+
+```typescript
+// Ohne const: Typ wird zu allgemeinem Array
+function createConfig(values: T) {
+ return values;
+}
+
+const config1 = createConfig(['a', 'b', 'c']);
+// Typ: string[]
+
+// Mit const: Exakte Literal-Typen
+function createConfigConst(values: T) {
+ return values;
+}
+
+const config2 = createConfigConst(['a', 'b', 'c']);
+// Typ: readonly ["a", "b", "c"]
+
+// Praktisches Beispiel: Type-safe API Client
+function defineEndpoints>(endpoints: T): T {
+ return endpoints;
+}
+
+const api = defineEndpoints({
+ getUsers: { method: 'GET', path: '/users' },
+ createUser: { method: 'POST', path: '/users' }
+});
+
+// api.getUsers.method ist exakt 'GET', nicht 'GET' | 'POST' | ...
+```
+
+---
+
+## Satisfies Operator
+
+```typescript
+// Typprüfung ohne Type Widening
+type Colors = 'red' | 'green' | 'blue';
+type ColorConfig = Record;
+
+// ❌ Mit as: Verliert spezifische Typen
+const colors1 = {
+ red: '#ff0000',
+ green: [0, 255, 0],
+ blue: '#0000ff'
+} as ColorConfig;
+// colors1.red ist string | number[]
+
+// ✅ Mit satisfies: Behält spezifische Typen
+const colors2 = {
+ red: '#ff0000',
+ green: [0, 255, 0],
+ blue: '#0000ff'
+} satisfies ColorConfig;
+// colors2.red ist string
+// colors2.green ist number[]
+```
+
+---
+
+## Pattern: Builder mit Method Chaining
+
+```typescript
+// Type-safe Builder Pattern
+class QueryBuilder {
+ private config: T = {} as T;
+
+ select(field: K): QueryBuilder {
+ return Object.assign(this, { config: { ...this.config, select: field } });
+ }
+
+ where(
+ field: K,
+ value: V
+ ): QueryBuilder {
+ return Object.assign(this, {
+ config: { ...this.config, where: { field, value } }
+ });
+ }
+
+ limit(n: number): QueryBuilder {
+ return Object.assign(this, { config: { ...this.config, limit: n } });
+ }
+
+ build(): T {
+ return this.config;
+ }
+}
+
+// Verwendung mit voller Typinferenz
+const query = new QueryBuilder()
+ .select('users')
+ .where('status', 'active')
+ .limit(10)
+ .build();
+
+// query hat Typ: {
+// select: "users";
+// where: { field: "status"; value: "active" };
+// limit: number;
+// }
+```
+
+---
+
+## Pattern: Discriminated Unions für State Management
+
+```typescript
+// Zustandsmaschine mit Discriminated Unions
+type LoadingState =
+ | { status: 'idle' }
+ | { status: 'loading' }
+ | { status: 'success'; data: T }
+ | { status: 'error'; error: Error };
+
+function handleState(state: LoadingState) {
+ switch (state.status) {
+ case 'idle':
+ return 'Bereit';
+ case 'loading':
+ return 'Lädt...';
+ case 'success':
+ // TypeScript weiß: state.data existiert
+ return `Daten: ${JSON.stringify(state.data)}`;
+ case 'error':
+ // TypeScript weiß: state.error existiert
+ return `Fehler: ${state.error.message}`;
+ }
+}
+
+// Exhaustive Check
+function assertNever(x: never): never {
+ throw new Error(`Unexpected value: ${x}`);
+}
+
+// Wenn ein Case vergessen wird, gibt es einen Compile-Error
+```
+
+---
+
+## Pattern: Branded Types
+
+```typescript
+// Branded Types für Runtime-Sicherheit
+type Brand = T & { __brand: B };
+
+type UserId = Brand;
+type ProductId = Brand;
+
+// Factory Functions
+function createUserId(id: string): UserId {
+ return id as UserId;
+}
+
+function createProductId(id: string): ProductId {
+ return id as ProductId;
+}
+
+// Kann nicht versehentlich verwechselt werden
+function getUser(id: UserId) { /*...*/ }
+function getProduct(id: ProductId) { /*...*/ }
+
+const userId = createUserId('user-123');
+const productId = createProductId('prod-456');
+
+getUser(userId); // ✅ OK
+getUser(productId); // ❌ Compile Error!
+```
+
+---
+
+## Migration zu TypeScript 7.0
+
+### Breaking Changes
+
+```typescript
+// ❌ Nicht mehr unterstützt in TS 7.0
+{
+ "compilerOptions": {
+ "target": "ES5", // Entfernt
+ "module": "AMD", // Entfernt
+ "moduleResolution": "classic" // Entfernt
+ }
+}
+
+// ✅ Empfohlene Config für TS 7.0
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true // Default in 7.0
+ }
+}
+```
+
+### Codemod für Migration
+
+```bash
+# TypeScript Upgrade Tool
+npx @typescript/upgrade
+
+# Automatische Fixes für Deprecations
+npx ts-migrate retype
+```
+
+---
+
+## Fazit
+
+TypeScript 6.0/7.0 bringt:
+
+1. **10x schnellere Builds**: Go-basierter Compiler
+2. **Resource Management**: `using` für automatisches Cleanup
+3. **Strict-by-Default**: Weniger Konfiguration für sichere Projekte
+4. **Modern-Only**: ES5 und Legacy-Module entfernt
+
+2026 ist TypeScript nicht mehr die Wahl – es ist der Standard.
+
+---
+
+## Bildprompts
+
+1. "TypeScript logo transforming into lightning bolt, speed and performance concept"
+2. "Code editor with instant type checking, no loading indicators, developer productivity"
+3. "Bridge connecting old TypeScript to new, version 6 to 7 transition visualization"
+
+---
+
+## Quellen
+
+- [Microsoft: Project Corsa Announcement](https://www.infoworld.com/article/4100582/microsoft-steers-native-port-of-typescript-to-early-2026-release.html)
+- [State of TypeScript 2026](https://devnewsletter.com/p/state-of-typescript-2026)
+- [TypeScript 6.0 Features](https://medium.com/@mernstackdevbykevin/typescript-6-0-the-biggest-changes-we-have-so-far-42563cb6d470)
+- [Advanced TypeScript Patterns 2026](https://medium.com/@100xmanas/advanced-typescript-techniques-every-developer-should-know-in-2026-9165059f56bd)
diff --git a/blog-posts/32-tailwind-css-4.md b/blog-posts/32-tailwind-css-4.md
new file mode 100644
index 0000000..d3f5697
--- /dev/null
+++ b/blog-posts/32-tailwind-css-4.md
@@ -0,0 +1,465 @@
+# Tailwind CSS 4.0: Die Oxide Engine Revolution
+
+**Meta-Description:** Tailwind CSS 4.0 Deep Dive. Die neue Oxide Engine, CSS-First Konfiguration, Container Queries und Performance-Optimierungen für 2026.
+
+**Keywords:** Tailwind CSS 4, Oxide Engine, CSS Framework, Utility-First CSS, Container Queries, CSS Variables, Performance CSS
+
+---
+
+## Einführung
+
+Tailwind CSS 4.0 ist ein **kompletter Neuschrieb** mit der Oxide Engine in Rust. Full Builds sind 5x schneller, inkrementelle Builds über 100x schneller – gemessen in **Mikrosekunden**.
+
+---
+
+## Performance-Revolution
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ TAILWIND 4 PERFORMANCE │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Full Build: │
+│ ├── Tailwind 3: 960ms │
+│ ├── Tailwind 4: 105ms │
+│ └── Speedup: ~9x │
+│ │
+│ Incremental Build: │
+│ ├── Tailwind 3: ~50ms │
+│ ├── Tailwind 4: <1ms (Mikrosekunden!) │
+│ └── Speedup: 100x+ │
+│ │
+│ Bundle Size: │
+│ ├── Tailwind 3: ~15MB installed │
+│ ├── Tailwind 4: ~10MB installed │
+│ └── Reduktion: 35% │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Die Oxide Engine
+
+Die neue Engine kombiniert:
+
+- **Rust** für CPU-intensive Operationen
+- **TypeScript** für Extensibility
+- **Lightning CSS** für CSS-Parsing
+
+```typescript
+// Die kritischen Pfade sind in Rust:
+// - Template Scanning
+// - Class Extraction
+// - CSS Generation
+// - Minification
+
+// TypeScript bleibt für:
+// - Plugin System
+// - Custom Configuration
+// - Developer Experience
+```
+
+---
+
+## CSS-First Konfiguration
+
+### Tailwind 3: JavaScript Config
+
+```javascript
+// tailwind.config.js (alt)
+module.exports = {
+ theme: {
+ extend: {
+ colors: {
+ primary: '#3b82f6',
+ secondary: '#10b981'
+ },
+ fontFamily: {
+ sans: ['Inter', 'sans-serif']
+ }
+ }
+ },
+ content: ['./src/**/*.{js,ts,jsx,tsx}']
+};
+```
+
+### Tailwind 4: CSS Config
+
+```css
+/* app.css (neu) */
+@import "tailwindcss";
+
+@theme {
+ /* Colors als CSS Variables */
+ --color-primary: #3b82f6;
+ --color-secondary: #10b981;
+
+ /* Fonts */
+ --font-sans: "Inter", sans-serif;
+
+ /* Spacing */
+ --spacing-18: 4.5rem;
+
+ /* Custom Breakpoints */
+ --breakpoint-3xl: 1920px;
+}
+```
+
+### Vorteile der CSS-First Config
+
+```css
+/* Volle CSS-Power nutzbar */
+@theme {
+ /* CSS Calc */
+ --spacing-golden: calc(1rem * 1.618);
+
+ /* Color Functions */
+ --color-primary-light: color-mix(in oklch, var(--color-primary), white 20%);
+
+ /* Media Query im Theme */
+ @media (prefers-color-scheme: dark) {
+ --color-background: #0f172a;
+ }
+}
+```
+
+---
+
+## Automatische Content Detection
+
+### Tailwind 3: Manuelle Konfiguration
+
+```javascript
+// tailwind.config.js
+module.exports = {
+ content: [
+ './src/**/*.{js,ts,jsx,tsx}',
+ './pages/**/*.{js,ts,jsx,tsx}',
+ './components/**/*.{js,ts,jsx,tsx}'
+ ]
+};
+```
+
+### Tailwind 4: Automatisch
+
+```css
+/* app.css - keine Content-Config nötig! */
+@import "tailwindcss";
+
+/* Tailwind 4 erkennt automatisch:
+ - Alle Dateien im Projekt
+ - Ignoriert .gitignore
+ - Ignoriert Binary-Dateien (Bilder, Videos)
+ - Scannt node_modules intelligent
+*/
+```
+
+---
+
+## Native Container Queries
+
+### Vorher: Plugin erforderlich
+
+```javascript
+// tailwind.config.js (Tailwind 3)
+plugins: [
+ require('@tailwindcss/container-queries')
+]
+```
+
+### Jetzt: Built-in
+
+```html
+
+
+```
+
+```css
+/* Generierte CSS */
+@container (min-width: 28rem) {
+ .\\@md\\:grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+```
+
+### Named Containers
+
+```html
+
+```
+
+---
+
+## 3D Transform Utilities
+
+```html
+
+
+
+
+
+
+
+
+ Nicht sichtbar wenn gedreht
+
+```
+
+---
+
+## Entry Animations mit @starting-style
+
+```html
+
+
+ Animiert rein
+
+```
+
+```css
+/* Generiertes CSS */
+.starting\:opacity-0 {
+ @starting-style {
+ opacity: 0;
+ }
+}
+
+.starting\:translate-y-4 {
+ @starting-style {
+ transform: translateY(1rem);
+ }
+}
+```
+
+---
+
+## Native CSS Variables
+
+```css
+/* Alle Design Tokens als CSS Variables */
+@theme {
+ --color-blue-500: #3b82f6;
+}
+```
+
+```html
+
+Tailwind Klasse
+CSS Variable
+```
+
+```javascript
+// In JavaScript nutzbar
+const primaryColor = getComputedStyle(document.documentElement)
+ .getPropertyValue('--color-blue-500');
+```
+
+---
+
+## Vite Plugin für maximale Performance
+
+```typescript
+// vite.config.ts
+import { defineConfig } from 'vite';
+import tailwindcss from '@tailwindcss/vite';
+
+export default defineConfig({
+ plugins: [
+ tailwindcss() // Tight Vite Integration
+ ]
+});
+```
+
+### Vergleich: PostCSS vs Vite Plugin
+
+| Aspekt | PostCSS | Vite Plugin |
+|--------|---------|-------------|
+| **HMR** | Gut | Instant |
+| **Dev Server Start** | ~500ms | ~100ms |
+| **Integration** | Universal | Vite only |
+
+---
+
+## Migration von Tailwind 3
+
+### Automatisches Upgrade Tool
+
+```bash
+# Upgrade Tool ausführen
+npx @tailwindcss/upgrade
+
+# Was es macht:
+# 1. tailwind.config.js → CSS @theme
+# 2. @apply Updates
+# 3. Deprecated Classes ersetzen
+# 4. Plugin-Migration
+```
+
+### Manuelle Änderungen
+
+```css
+/* Vorher: @tailwind Direktiven */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Nachher: Single Import */
+@import "tailwindcss";
+```
+
+### Breaking Changes
+
+```html
+
+
+Alt
+Neu
+
+
+
+Neuer Name
+```
+
+---
+
+## Browser Support
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ BROWSER SUPPORT │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ ✅ Unterstützt: │
+│ ├── Chrome 111+ │
+│ ├── Firefox 128+ │
+│ ├── Safari 16.4+ │
+│ ├── Edge 111+ │
+│ └── Alle modernen mobilen Browser │
+│ │
+│ ❌ Nicht unterstützt: │
+│ ├── Internet Explorer (alle Versionen) │
+│ └── Legacy Browser ohne CSS Custom Properties │
+│ │
+│ Anforderungen: │
+│ ├── CSS Custom Properties │
+│ ├── CSS Cascade Layers │
+│ ├── @property Registration │
+│ └── color-mix() Function │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Best Practices für Tailwind 4
+
+### 1. CSS-First Theme
+
+```css
+/* Organisiere Theme logisch */
+@theme {
+ /* === Colors === */
+ --color-*: ...;
+
+ /* === Typography === */
+ --font-*: ...;
+ --text-*: ...;
+
+ /* === Spacing === */
+ --spacing-*: ...;
+
+ /* === Effects === */
+ --shadow-*: ...;
+ --radius-*: ...;
+}
+```
+
+### 2. Component Classes mit @apply
+
+```css
+/* components.css */
+@layer components {
+ .btn {
+ @apply px-4 py-2 rounded-lg font-medium transition-colors;
+ }
+
+ .btn-primary {
+ @apply btn bg-primary text-white hover:bg-primary/90;
+ }
+
+ .card {
+ @apply bg-white rounded-xl shadow-lg p-6;
+ }
+}
+```
+
+### 3. Dark Mode
+
+```css
+@theme {
+ /* Light Mode (default) */
+ --color-background: white;
+ --color-text: #1f2937;
+
+ /* Dark Mode */
+ @media (prefers-color-scheme: dark) {
+ --color-background: #0f172a;
+ --color-text: #f1f5f9;
+ }
+}
+```
+
+---
+
+## Fazit
+
+Tailwind CSS 4.0 bringt:
+
+1. **Oxide Engine**: 5-100x schnellere Builds
+2. **CSS-First Config**: Keine JS-Config mehr nötig
+3. **Container Queries**: Built-in, ohne Plugin
+4. **3D Transforms**: Native Utilities
+5. **@starting-style**: Entry Animations
+
+Der beste Zeitpunkt zum Upgrade ist jetzt.
+
+---
+
+## Bildprompts
+
+1. "Rust gear and CSS stylesheet merging, Oxide engine concept, technical illustration"
+2. "Speed comparison chart showing dramatic improvement, before/after visualization"
+3. "CSS variables flowing through design system, modern web development concept"
+
+---
+
+## Quellen
+
+- [Tailwind CSS v4.0 Blog](https://tailwindcss.com/blog/tailwindcss-v4)
+- [Tailwind v4 vs v3 Comparison](https://frontend-hero.com/tailwind-v4-vs-v3)
+- [Tailwind 4 Performance Guide](https://medium.com/@mernstackdevbykevin/tailwind-css-v4-0-performance-boosts-build-times-jit-more-abf6b75e37bd)
+- [Oxide Engine Deep Dive](https://www.dataformathub.com/blog/tailwind-css-v4-deep-dive-why-the-oxide-engine-changes-everything-in-2025-lz4)
diff --git a/blog-posts/33-zod-schema-validation.md b/blog-posts/33-zod-schema-validation.md
new file mode 100644
index 0000000..5db6784
--- /dev/null
+++ b/blog-posts/33-zod-schema-validation.md
@@ -0,0 +1,480 @@
+# Zod Schema Validation: Type-Safe Validation für TypeScript
+
+**Meta-Description:** Umfassender Guide zu Zod für Schema-Validierung. TypeScript-Integration, API-Validation, Form-Handling und Best Practices für robuste Anwendungen.
+
+**Keywords:** Zod, Schema Validation, TypeScript Validation, Form Validation, API Validation, Runtime Validation, Type Safety
+
+---
+
+## Einführung
+
+Zod ist die führende Schema-Validierungsbibliothek für TypeScript. Sie bietet **Runtime-Validierung mit automatischer Type-Inferenz** – eine perfekte Brücke zwischen TypeScript's Compile-Time-Checks und der Realität von externen Daten.
+
+---
+
+## Das Problem
+
+```typescript
+// TypeScript prüft nur zur Compile-Time
+interface User {
+ name: string;
+ email: string;
+ age: number;
+}
+
+// Aber was passiert zur Runtime?
+const userData = await fetch('/api/user').then(r => r.json());
+
+// userData könnte ALLES sein - TypeScript vertraut blind
+const user: User = userData; // Keine Runtime-Prüfung!
+```
+
+---
+
+## Die Lösung: Zod
+
+```typescript
+import { z } from 'zod';
+
+// Schema definieren
+const UserSchema = z.object({
+ name: z.string().min(2),
+ email: z.string().email(),
+ age: z.number().int().positive()
+});
+
+// Type automatisch inferieren
+type User = z.infer;
+// { name: string; email: string; age: number }
+
+// Runtime-Validierung
+const userData = await fetch('/api/user').then(r => r.json());
+const user = UserSchema.parse(userData); // Wirft bei ungültigen Daten!
+
+// Oder mit safeParse für Error-Handling
+const result = UserSchema.safeParse(userData);
+if (result.success) {
+ console.log(result.data); // Typisiert als User
+} else {
+ console.error(result.error.errors);
+}
+```
+
+---
+
+## Basis-Typen
+
+```typescript
+import { z } from 'zod';
+
+// Primitive Typen
+const stringSchema = z.string();
+const numberSchema = z.number();
+const booleanSchema = z.boolean();
+const dateSchema = z.date();
+const bigintSchema = z.bigint();
+const symbolSchema = z.symbol();
+const undefinedSchema = z.undefined();
+const nullSchema = z.null();
+const voidSchema = z.void();
+const anySchema = z.any();
+const unknownSchema = z.unknown();
+const neverSchema = z.never();
+
+// Literale
+const tuna = z.literal('tuna');
+const twelve = z.literal(12);
+const isTrue = z.literal(true);
+
+// Enums
+const FishEnum = z.enum(['Salmon', 'Tuna', 'Trout']);
+type FishEnum = z.infer; // 'Salmon' | 'Tuna' | 'Trout'
+
+// Native Enums
+enum Fruits {
+ Apple,
+ Banana
+}
+const FruitEnum = z.nativeEnum(Fruits);
+```
+
+---
+
+## String Validierung
+
+```typescript
+const stringSchema = z.string()
+ // Länge
+ .min(5, 'Mindestens 5 Zeichen')
+ .max(100, 'Maximal 100 Zeichen')
+ .length(10, 'Exakt 10 Zeichen')
+
+ // Format
+ .email('Ungültige E-Mail')
+ .url('Ungültige URL')
+ .uuid('Ungültige UUID')
+ .cuid('Ungültige CUID')
+ .datetime('Ungültiges Datum')
+ .ip('Ungültige IP')
+
+ // Regex
+ .regex(/^[a-z]+$/, 'Nur Kleinbuchstaben')
+
+ // Transformationen
+ .trim()
+ .toLowerCase()
+ .toUpperCase()
+
+ // Custom
+ .refine(val => val.includes('@'), 'Muss @ enthalten');
+
+// Praktisches Beispiel
+const UsernameSchema = z.string()
+ .min(3, 'Username zu kurz')
+ .max(20, 'Username zu lang')
+ .regex(/^[a-zA-Z0-9_]+$/, 'Nur Buchstaben, Zahlen und _')
+ .toLowerCase();
+```
+
+---
+
+## Number Validierung
+
+```typescript
+const numberSchema = z.number()
+ // Constraints
+ .gt(0, 'Größer als 0')
+ .gte(0, 'Größer oder gleich 0')
+ .lt(100, 'Kleiner als 100')
+ .lte(100, 'Kleiner oder gleich 100')
+ .positive('Muss positiv sein')
+ .negative('Muss negativ sein')
+ .nonpositive()
+ .nonnegative()
+ .multipleOf(5, 'Muss durch 5 teilbar sein')
+ .int('Muss Ganzzahl sein')
+ .finite()
+ .safe(); // JavaScript safe integer
+
+// Praktisches Beispiel: Preis
+const PriceSchema = z.number()
+ .positive('Preis muss positiv sein')
+ .multipleOf(0.01, 'Maximal 2 Dezimalstellen')
+ .max(1000000, 'Preis zu hoch');
+```
+
+---
+
+## Object Schemas
+
+```typescript
+// Basis Object
+const UserSchema = z.object({
+ name: z.string(),
+ email: z.string().email(),
+ age: z.number().int().positive()
+});
+
+// Optional & Default
+const UserWithDefaultsSchema = z.object({
+ name: z.string(),
+ email: z.string().email(),
+ age: z.number().optional(), // number | undefined
+ role: z.string().default('user'), // Hat immer einen Wert
+ isActive: z.boolean().nullable() // boolean | null
+});
+
+// Extend
+const AdminSchema = UserSchema.extend({
+ permissions: z.array(z.string())
+});
+
+// Pick & Omit
+const UserNameOnly = UserSchema.pick({ name: true });
+const UserWithoutAge = UserSchema.omit({ age: true });
+
+// Partial & Required
+const PartialUser = UserSchema.partial(); // Alle optional
+const RequiredUser = PartialUser.required(); // Alle required
+
+// Strict Mode
+const StrictUser = UserSchema.strict(); // Wirft bei extra Keys
+
+// Passthrough & Strip
+const PassthroughUser = UserSchema.passthrough(); // Behält extra Keys
+const StrippedUser = UserSchema.strip(); // Entfernt extra Keys (default)
+```
+
+---
+
+## Array & Tuple
+
+```typescript
+// Array
+const StringArraySchema = z.array(z.string());
+const NumberArraySchema = z.array(z.number())
+ .min(1, 'Mindestens 1 Element')
+ .max(10, 'Maximal 10 Elemente')
+ .nonempty('Darf nicht leer sein');
+
+// Tuple
+const CoordinateSchema = z.tuple([
+ z.number(), // x
+ z.number(), // y
+ z.number().optional() // z (optional)
+]);
+
+type Coordinate = z.infer;
+// [number, number, number?]
+
+// Rest Elements
+const StringsThenNumbers = z.tuple([z.string(), z.string()])
+ .rest(z.number());
+// [string, string, ...number[]]
+```
+
+---
+
+## Union & Discriminated Union
+
+```typescript
+// Union
+const StringOrNumber = z.union([z.string(), z.number()]);
+
+// Shorthand
+const StringOrNull = z.string().nullable(); // string | null
+const StringOrUndefined = z.string().optional(); // string | undefined
+
+// Discriminated Union (performanter)
+const ResultSchema = z.discriminatedUnion('status', [
+ z.object({ status: z.literal('success'), data: z.string() }),
+ z.object({ status: z.literal('error'), error: z.string() })
+]);
+
+type Result = z.infer;
+// { status: 'success'; data: string } | { status: 'error'; error: string }
+```
+
+---
+
+## Transformationen
+
+```typescript
+// Transform Output
+const NumberFromString = z.string().transform(val => parseInt(val, 10));
+// Input: string, Output: number
+
+// Preprocessing
+const NumberSchema = z.preprocess(
+ (val) => {
+ if (typeof val === 'string') return parseInt(val, 10);
+ return val;
+ },
+ z.number()
+);
+
+// Praktisches Beispiel: API Response
+const ApiResponseSchema = z.object({
+ created_at: z.string().transform(val => new Date(val)),
+ price_cents: z.number().transform(val => val / 100),
+ is_active: z.union([z.boolean(), z.literal('true'), z.literal('false')])
+ .transform(val => val === true || val === 'true')
+});
+
+// Input: { created_at: "2024-01-15", price_cents: 1999, is_active: "true" }
+// Output: { created_at: Date, price_cents: 19.99, is_active: true }
+```
+
+---
+
+## Custom Validation mit Refine
+
+```typescript
+// Einfaches Refine
+const PasswordSchema = z.string()
+ .min(8)
+ .refine(
+ (val) => /[A-Z]/.test(val),
+ 'Muss Großbuchstaben enthalten'
+ )
+ .refine(
+ (val) => /[0-9]/.test(val),
+ 'Muss Zahlen enthalten'
+ );
+
+// Superrefine für komplexe Logik
+const SignupSchema = z.object({
+ password: z.string().min(8),
+ confirmPassword: z.string()
+}).superRefine((data, ctx) => {
+ if (data.password !== data.confirmPassword) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: 'Passwörter stimmen nicht überein',
+ path: ['confirmPassword']
+ });
+ }
+});
+
+// Async Validation
+const UniqueEmailSchema = z.string().email().refine(
+ async (email) => {
+ const exists = await checkEmailExists(email);
+ return !exists;
+ },
+ 'E-Mail bereits vergeben'
+);
+```
+
+---
+
+## Integration mit React Hook Form
+
+```typescript
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+
+const SignupSchema = z.object({
+ name: z.string().min(2, 'Name zu kurz'),
+ email: z.string().email('Ungültige E-Mail'),
+ password: z.string().min(8, 'Mindestens 8 Zeichen')
+});
+
+type SignupData = z.infer;
+
+function SignupForm() {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors }
+ } = useForm({
+ resolver: zodResolver(SignupSchema)
+ });
+
+ const onSubmit = (data: SignupData) => {
+ console.log(data); // Typsicher!
+ };
+
+ return (
+
+ );
+}
+```
+
+---
+
+## API Route Validation (Next.js)
+
+```typescript
+// app/api/users/route.ts
+import { z } from 'zod';
+import { NextRequest, NextResponse } from 'next/server';
+
+const CreateUserSchema = z.object({
+ name: z.string().min(2),
+ email: z.string().email(),
+ role: z.enum(['admin', 'user']).default('user')
+});
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const data = CreateUserSchema.parse(body);
+
+ // data ist typsicher
+ const user = await createUser(data);
+
+ return NextResponse.json(user, { status: 201 });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { errors: error.errors },
+ { status: 400 }
+ );
+ }
+ throw error;
+ }
+}
+```
+
+---
+
+## Error Handling
+
+```typescript
+import { z } from 'zod';
+
+const UserSchema = z.object({
+ name: z.string().min(2),
+ email: z.string().email()
+});
+
+try {
+ UserSchema.parse({ name: 'A', email: 'invalid' });
+} catch (error) {
+ if (error instanceof z.ZodError) {
+ // Formatierte Errors
+ console.log(error.format());
+ /*
+ {
+ name: { _errors: ['String must contain at least 2 character(s)'] },
+ email: { _errors: ['Invalid email'] }
+ }
+ */
+
+ // Flache Error-Liste
+ console.log(error.flatten());
+ /*
+ {
+ formErrors: [],
+ fieldErrors: {
+ name: ['String must contain at least 2 character(s)'],
+ email: ['Invalid email']
+ }
+ }
+ */
+ }
+}
+```
+
+---
+
+## Fazit
+
+Zod bietet:
+
+1. **Runtime + Compile-Time Safety**: Validierung wo TypeScript aufhört
+2. **Type Inference**: Keine doppelten Definitionen
+3. **Composability**: Schemas kombinieren und erweitern
+4. **Framework-Agnostisch**: React, Vue, Node.js, etc.
+
+Für jede Anwendung mit externen Daten ist Zod unverzichtbar.
+
+---
+
+## Bildprompts
+
+1. "Shield protecting data, validation concept, type-safe illustration"
+2. "TypeScript and runtime validation merging, code security concept"
+3. "Form with checkmarks appearing as user types, validation feedback visualization"
+
+---
+
+## Quellen
+
+- [Zod Documentation](https://zod.dev/)
+- [Zod GitHub](https://github.com/colinhacks/zod)
+- [React Hook Form + Zod](https://react-hook-form.com/get-started#SchemaValidation)
+- [tRPC + Zod Integration](https://trpc.io/docs/server/validators)
diff --git a/blog-posts/34-prisma-7-orm.md b/blog-posts/34-prisma-7-orm.md
new file mode 100644
index 0000000..b8c85e8
--- /dev/null
+++ b/blog-posts/34-prisma-7-orm.md
@@ -0,0 +1,550 @@
+# Prisma 7: Der TypeScript-Native ORM für 2026
+
+**Meta-Description:** Prisma 7 Deep Dive nach dem Rust-zu-TypeScript-Rewrite. 90% kleinere Bundles, 3x schnellere Queries und verbesserte Type-Safety.
+
+**Keywords:** Prisma, ORM, TypeScript, Database, PostgreSQL, MySQL, SQLite, Query Builder, Type Safety, Prisma 7
+
+---
+
+## Einführung
+
+Prisma 7 ist ein **Game-Changer**: Das Team hat die gesamte Query Engine von Rust nach TypeScript umgeschrieben. Das Ergebnis: 90% kleinere Bundles, 3x schnellere Queries und bessere Cold-Starts für Serverless.
+
+---
+
+## Die Revolution: Rust-Free Prisma
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ PRISMA 6 vs PRISMA 7 │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Prisma 6 (Rust Engine): │
+│ ├── Bundle Size: ~14MB (7MB gzipped) │
+│ ├── Cold Start: ~300ms (Lambda) │
+│ └── Query Speed: Baseline │
+│ │
+│ Prisma 7 (TypeScript Engine): │
+│ ├── Bundle Size: ~1.6MB (600KB gzipped) → 90% kleiner │
+│ ├── Cold Start: ~50ms (Lambda) → 6x schneller │
+│ └── Query Speed: 3x schneller bei großen Result Sets │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Setup & Schema
+
+### Installation
+
+```bash
+npm install prisma @prisma/client
+npx prisma init
+```
+
+### Schema Definition
+
+```prisma
+// prisma/schema.prisma
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+model User {
+ id String @id @default(cuid())
+ email String @unique
+ name String?
+ role Role @default(USER)
+ posts Post[]
+ profile Profile?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@index([email])
+}
+
+model Profile {
+ id String @id @default(cuid())
+ bio String?
+ avatar String?
+ userId String @unique
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+model Post {
+ id String @id @default(cuid())
+ title String
+ content String?
+ published Boolean @default(false)
+ author User @relation(fields: [authorId], references: [id])
+ authorId String
+ categories Category[]
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@index([authorId])
+ @@index([published])
+}
+
+model Category {
+ id String @id @default(cuid())
+ name String @unique
+ posts Post[]
+}
+
+enum Role {
+ USER
+ ADMIN
+ MODERATOR
+}
+```
+
+---
+
+## CRUD Operations
+
+### Create
+
+```typescript
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+// Einzelner Datensatz
+const user = await prisma.user.create({
+ data: {
+ email: 'alice@example.com',
+ name: 'Alice',
+ // Nested Create
+ profile: {
+ create: {
+ bio: 'Developer from Berlin'
+ }
+ },
+ posts: {
+ create: [
+ { title: 'First Post' },
+ { title: 'Second Post' }
+ ]
+ }
+ },
+ include: {
+ profile: true,
+ posts: true
+ }
+});
+
+// Mehrere Datensätze
+const { count } = await prisma.user.createMany({
+ data: [
+ { email: 'bob@example.com', name: 'Bob' },
+ { email: 'carol@example.com', name: 'Carol' }
+ ],
+ skipDuplicates: true
+});
+```
+
+### Read
+
+```typescript
+// Einzelner Datensatz
+const user = await prisma.user.findUnique({
+ where: { email: 'alice@example.com' }
+});
+
+// Mit Relations
+const userWithPosts = await prisma.user.findUnique({
+ where: { id: userId },
+ include: {
+ posts: {
+ where: { published: true },
+ orderBy: { createdAt: 'desc' },
+ take: 5
+ },
+ profile: true
+ }
+});
+
+// Nur bestimmte Felder
+const userBasic = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true,
+ name: true,
+ email: true
+ }
+});
+
+// Liste mit Filter
+const users = await prisma.user.findMany({
+ where: {
+ role: 'USER',
+ email: {
+ contains: '@example.com'
+ },
+ posts: {
+ some: {
+ published: true
+ }
+ }
+ },
+ orderBy: [
+ { role: 'asc' },
+ { name: 'asc' }
+ ],
+ skip: 0,
+ take: 10
+});
+```
+
+### Update
+
+```typescript
+// Einzelner Datensatz
+const updatedUser = await prisma.user.update({
+ where: { id: userId },
+ data: {
+ name: 'Alice Updated',
+ // Nested Update
+ profile: {
+ update: {
+ bio: 'Senior Developer from Berlin'
+ }
+ }
+ }
+});
+
+// Upsert (Create or Update)
+const user = await prisma.user.upsert({
+ where: { email: 'alice@example.com' },
+ update: { name: 'Alice' },
+ create: {
+ email: 'alice@example.com',
+ name: 'Alice'
+ }
+});
+
+// Mehrere aktualisieren
+const { count } = await prisma.user.updateMany({
+ where: {
+ role: 'USER',
+ createdAt: {
+ lt: new Date('2024-01-01')
+ }
+ },
+ data: {
+ role: 'MODERATOR'
+ }
+});
+```
+
+### Delete
+
+```typescript
+// Einzelner Datensatz
+const deletedUser = await prisma.user.delete({
+ where: { id: userId }
+});
+
+// Mehrere Datensätze
+const { count } = await prisma.user.deleteMany({
+ where: {
+ posts: {
+ none: {}
+ },
+ createdAt: {
+ lt: new Date('2023-01-01')
+ }
+ }
+});
+```
+
+---
+
+## Erweiterte Queries
+
+### Aggregations
+
+```typescript
+// Count
+const userCount = await prisma.user.count({
+ where: { role: 'USER' }
+});
+
+// Aggregate
+const stats = await prisma.post.aggregate({
+ _count: { id: true },
+ _avg: { viewCount: true },
+ _sum: { viewCount: true },
+ _min: { createdAt: true },
+ _max: { createdAt: true }
+});
+
+// Group By
+const postsByAuthor = await prisma.post.groupBy({
+ by: ['authorId'],
+ _count: { id: true },
+ _sum: { viewCount: true },
+ having: {
+ id: {
+ _count: {
+ gt: 5
+ }
+ }
+ },
+ orderBy: {
+ _count: {
+ id: 'desc'
+ }
+ }
+});
+```
+
+### Raw SQL
+
+```typescript
+// Raw Query
+const users = await prisma.$queryRaw`
+ SELECT * FROM "User"
+ WHERE email LIKE ${'%@example.com'}
+ ORDER BY "createdAt" DESC
+`;
+
+// Raw Execute
+const result = await prisma.$executeRaw`
+ UPDATE "User"
+ SET "lastLogin" = NOW()
+ WHERE id = ${userId}
+`;
+
+// Mit TypedSQL (Prisma 7.1+)
+import { sql } from '@prisma/client/sql';
+
+const activeUsers = await prisma.$queryRawTyped(
+ sql`SELECT * FROM "User" WHERE "isActive" = true`
+);
+```
+
+### Transactions
+
+```typescript
+// Interactive Transaction
+const result = await prisma.$transaction(async (tx) => {
+ // Guthaben abziehen
+ const sender = await tx.account.update({
+ where: { id: senderId },
+ data: { balance: { decrement: amount } }
+ });
+
+ if (sender.balance < 0) {
+ throw new Error('Insufficient funds');
+ }
+
+ // Guthaben hinzufügen
+ const receiver = await tx.account.update({
+ where: { id: receiverId },
+ data: { balance: { increment: amount } }
+ });
+
+ return { sender, receiver };
+}, {
+ isolationLevel: 'Serializable',
+ timeout: 10000
+});
+
+// Sequential Operations (Batch)
+const [users, posts] = await prisma.$transaction([
+ prisma.user.findMany(),
+ prisma.post.findMany({ where: { published: true } })
+]);
+```
+
+---
+
+## Type-Safety Features
+
+### Prisma 7 Typed Improvements
+
+```typescript
+// 98% weniger Types bei Schema-Evaluation
+// 45% weniger Types bei Query-Evaluation
+// 70% schnelleres Type-Checking
+
+// Full Type Safety
+const user = await prisma.user.findUnique({
+ where: { id: userId },
+ include: { posts: true }
+});
+
+// user ist typisiert als:
+// {
+// id: string;
+// email: string;
+// name: string | null;
+// role: Role;
+// posts: Post[];
+// ...
+// } | null
+
+// Conditional Include Typing
+const userMaybeWithPosts = await prisma.user.findUnique({
+ where: { id: userId },
+ include: includePosts ? { posts: true } : undefined
+});
+// Typ reflektiert die Bedingung
+```
+
+### Validierung mit Zod
+
+```typescript
+import { z } from 'zod';
+import { Prisma } from '@prisma/client';
+
+// Schema aus Prisma Types ableiten
+const CreateUserSchema = z.object({
+ email: z.string().email(),
+ name: z.string().min(2).optional(),
+ role: z.enum(['USER', 'ADMIN', 'MODERATOR']).default('USER')
+}) satisfies z.Schema;
+
+// Validierung vor DB-Operation
+async function createUser(input: unknown) {
+ const data = CreateUserSchema.parse(input);
+ return prisma.user.create({ data });
+}
+```
+
+---
+
+## SQL Comments (Prisma 7.1)
+
+```typescript
+// Observability und Debugging
+const users = await prisma.user.findMany({
+ // Diese Informationen erscheinen als SQL-Kommentar
+ // für besseres Tracing
+}).$extends({
+ query: {
+ $allOperations({ operation, args, query }) {
+ return query(args);
+ }
+ }
+});
+
+// Generiertes SQL:
+// /* prisma:client,user.findMany,requestId:abc123 */
+// SELECT * FROM "User"
+```
+
+---
+
+## Serverless & Edge Optimierung
+
+```typescript
+// Für Cloudflare Workers, Vercel Edge, etc.
+import { PrismaClient } from '@prisma/client/edge';
+import { withAccelerate } from '@prisma/extension-accelerate';
+
+const prisma = new PrismaClient().$extends(withAccelerate());
+
+// Connection Pooling via Prisma Accelerate
+const users = await prisma.user.findMany({
+ cacheStrategy: {
+ ttl: 60, // Cache für 60 Sekunden
+ swr: 300 // Stale-While-Revalidate für 5 Minuten
+ }
+});
+```
+
+---
+
+## Migration & Deployment
+
+```bash
+# Schema-Änderungen als Migration
+npx prisma migrate dev --name add_user_role
+
+# Production Deployment
+npx prisma migrate deploy
+
+# Client generieren
+npx prisma generate
+
+# Datenbank seeden
+npx prisma db seed
+
+# Studio (Daten-Browser)
+npx prisma studio
+```
+
+### Seed Script
+
+```typescript
+// prisma/seed.ts
+import { PrismaClient } from '@prisma/client';
+
+const prisma = new PrismaClient();
+
+async function main() {
+ // Admin User
+ await prisma.user.upsert({
+ where: { email: 'admin@example.com' },
+ update: {},
+ create: {
+ email: 'admin@example.com',
+ name: 'Admin',
+ role: 'ADMIN'
+ }
+ });
+
+ // Categories
+ const categories = ['Technology', 'Design', 'Business'];
+ for (const name of categories) {
+ await prisma.category.upsert({
+ where: { name },
+ update: {},
+ create: { name }
+ });
+ }
+}
+
+main()
+ .catch(console.error)
+ .finally(() => prisma.$disconnect());
+```
+
+---
+
+## Fazit
+
+Prisma 7 bringt:
+
+1. **90% kleinere Bundles**: Perfekt für Serverless
+2. **3x schnellere Queries**: Besonders bei großen Datasets
+3. **Bessere Type-Safety**: 70% schnelleres Type-Checking
+4. **SQL Comments**: Verbessertes Debugging
+
+Der Rust-zu-TypeScript-Rewrite macht Prisma schneller, kleiner und besser wartbar.
+
+---
+
+## Bildprompts
+
+1. "Database schema transforming into TypeScript types, code generation visualization"
+2. "Performance comparison chart showing bundle size reduction, before/after"
+3. "Prisma logo with speed lines, serverless deployment concept"
+
+---
+
+## Quellen
+
+- [Prisma 7 Release Announcement](https://www.prisma.io/blog/announcing-prisma-orm-7-0-0)
+- [Prisma 7 Upgrade Guide](https://www.prisma.io/docs/orm/more/upgrade-guides/upgrading-versions/upgrading-to-prisma-7)
+- [Prisma Performance Benchmarks](https://www.prisma.io/blog/prisma-orm-without-rust-latest-performance-benchmarks)
+- [Prisma Documentation](https://www.prisma.io/docs)
diff --git a/blog-posts/35-trpc-type-safe-apis.md b/blog-posts/35-trpc-type-safe-apis.md
new file mode 100644
index 0000000..2ec0244
--- /dev/null
+++ b/blog-posts/35-trpc-type-safe-apis.md
@@ -0,0 +1,478 @@
+# tRPC: End-to-End Type Safety ohne Code-Generierung
+
+**Meta-Description:** tRPC für vollständig typsichere APIs zwischen Frontend und Backend. Keine Schemas, keine Code-Generierung – nur TypeScript.
+
+**Keywords:** tRPC, Type Safety, API, TypeScript, Full-Stack, End-to-End Types, React Query, Next.js
+
+---
+
+## Einführung
+
+tRPC eliminiert die Grenze zwischen Frontend und Backend. **Ein TypeScript-Typ, der vom Server kommt, ist automatisch im Client verfügbar** – ohne REST, ohne GraphQL, ohne Code-Generierung.
+
+---
+
+## Das Problem
+
+```typescript
+// REST: Kein automatischer Type-Share
+// Backend
+app.get('/api/users/:id', async (req, res) => {
+ const user = await getUser(req.params.id);
+ res.json(user);
+});
+
+// Frontend - Types müssen manuell synchronisiert werden
+const response = await fetch('/api/users/123');
+const user: User = await response.json(); // Hoffen dass es stimmt...
+```
+
+---
+
+## Die Lösung: tRPC
+
+```typescript
+// Router Definition (Backend)
+import { initTRPC } from '@trpc/server';
+import { z } from 'zod';
+
+const t = initTRPC.create();
+
+const appRouter = t.router({
+ user: t.router({
+ getById: t.procedure
+ .input(z.string())
+ .query(async ({ input }) => {
+ return await prisma.user.findUnique({
+ where: { id: input }
+ });
+ })
+ })
+});
+
+export type AppRouter = typeof appRouter;
+
+// Client (Frontend)
+import { trpc } from './utils/trpc';
+
+function UserProfile({ userId }: { userId: string }) {
+ // Vollständig typisiert! 🎉
+ const { data: user } = trpc.user.getById.useQuery(userId);
+
+ // user ist automatisch typisiert als:
+ // { id: string; name: string; email: string; ... } | null | undefined
+}
+```
+
+---
+
+## Setup mit Next.js App Router
+
+### 1. Server-Setup
+
+```typescript
+// src/server/trpc.ts
+import { initTRPC, TRPCError } from '@trpc/server';
+import { ZodError } from 'zod';
+import superjson from 'superjson';
+
+const t = initTRPC.context().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError
+ ? error.cause.flatten()
+ : null
+ }
+ };
+ }
+});
+
+export const router = t.router;
+export const publicProcedure = t.procedure;
+
+// Middleware für Auth
+export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
+ if (!ctx.session?.user) {
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
+ }
+ return next({
+ ctx: {
+ ...ctx,
+ user: ctx.session.user
+ }
+ });
+});
+```
+
+### 2. Router Definition
+
+```typescript
+// src/server/routers/_app.ts
+import { router } from '../trpc';
+import { userRouter } from './user';
+import { postRouter } from './post';
+
+export const appRouter = router({
+ user: userRouter,
+ post: postRouter
+});
+
+export type AppRouter = typeof appRouter;
+```
+
+```typescript
+// src/server/routers/user.ts
+import { z } from 'zod';
+import { router, publicProcedure, protectedProcedure } from '../trpc';
+
+export const userRouter = router({
+ // Public Query
+ getById: publicProcedure
+ .input(z.string())
+ .query(async ({ input }) => {
+ return prisma.user.findUnique({
+ where: { id: input },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ image: true
+ }
+ });
+ }),
+
+ // Protected Query
+ getMe: protectedProcedure
+ .query(async ({ ctx }) => {
+ return prisma.user.findUnique({
+ where: { id: ctx.user.id }
+ });
+ }),
+
+ // Mutation mit Validation
+ update: protectedProcedure
+ .input(z.object({
+ name: z.string().min(2).optional(),
+ bio: z.string().max(500).optional()
+ }))
+ .mutation(async ({ ctx, input }) => {
+ return prisma.user.update({
+ where: { id: ctx.user.id },
+ data: input
+ });
+ })
+});
+```
+
+### 3. API Route Handler
+
+```typescript
+// src/app/api/trpc/[trpc]/route.ts
+import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
+import { appRouter } from '@/server/routers/_app';
+import { createContext } from '@/server/context';
+
+const handler = (req: Request) =>
+ fetchRequestHandler({
+ endpoint: '/api/trpc',
+ req,
+ router: appRouter,
+ createContext
+ });
+
+export { handler as GET, handler as POST };
+```
+
+### 4. Client Setup
+
+```typescript
+// src/utils/trpc.ts
+import { createTRPCReact } from '@trpc/react-query';
+import type { AppRouter } from '@/server/routers/_app';
+
+export const trpc = createTRPCReact();
+```
+
+```typescript
+// src/app/providers.tsx
+'use client';
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { httpBatchLink } from '@trpc/client';
+import { trpc } from '@/utils/trpc';
+import superjson from 'superjson';
+
+export function TRPCProvider({ children }: { children: React.ReactNode }) {
+ const [queryClient] = useState(() => new QueryClient());
+ const [trpcClient] = useState(() =>
+ trpc.createClient({
+ links: [
+ httpBatchLink({
+ url: '/api/trpc',
+ transformer: superjson
+ })
+ ]
+ })
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+```
+
+---
+
+## Client-Side Usage
+
+### Queries
+
+```typescript
+'use client';
+
+import { trpc } from '@/utils/trpc';
+
+function UserProfile({ userId }: { userId: string }) {
+ // Basic Query
+ const { data, isLoading, error } = trpc.user.getById.useQuery(userId);
+
+ // Mit Options
+ const { data: user } = trpc.user.getById.useQuery(userId, {
+ enabled: !!userId,
+ staleTime: 60 * 1000,
+ refetchOnWindowFocus: false
+ });
+
+ // Suspense Query
+ const [user] = trpc.user.getById.useSuspenseQuery(userId);
+
+ if (isLoading) return ;
+ if (error) return ;
+
+ return {data?.name}
;
+}
+```
+
+### Mutations
+
+```typescript
+function UpdateProfileForm() {
+ const utils = trpc.useUtils();
+
+ const updateUser = trpc.user.update.useMutation({
+ onSuccess: () => {
+ // Cache invalidieren
+ utils.user.getMe.invalidate();
+ },
+ onError: (error) => {
+ // Zod Errors sind typisiert
+ if (error.data?.zodError) {
+ console.log(error.data.zodError.fieldErrors);
+ }
+ }
+ });
+
+ const handleSubmit = (data: FormData) => {
+ updateUser.mutate({
+ name: data.get('name') as string
+ });
+ };
+
+ return (
+
+ );
+}
+```
+
+### Optimistic Updates
+
+```typescript
+function TodoList() {
+ const utils = trpc.useUtils();
+
+ const addTodo = trpc.todo.create.useMutation({
+ // Optimistic Update
+ onMutate: async (newTodo) => {
+ await utils.todo.list.cancel();
+
+ const previousTodos = utils.todo.list.getData();
+
+ utils.todo.list.setData(undefined, (old) => [
+ ...(old ?? []),
+ { id: 'temp', ...newTodo, createdAt: new Date() }
+ ]);
+
+ return { previousTodos };
+ },
+ onError: (err, newTodo, context) => {
+ utils.todo.list.setData(undefined, context?.previousTodos);
+ },
+ onSettled: () => {
+ utils.todo.list.invalidate();
+ }
+ });
+
+ return (/* ... */);
+}
+```
+
+---
+
+## Server-Side Rendering
+
+```typescript
+// src/app/users/[id]/page.tsx
+import { createServerSideHelpers } from '@trpc/react-query/server';
+import { appRouter } from '@/server/routers/_app';
+import superjson from 'superjson';
+
+export default async function UserPage({
+ params
+}: {
+ params: { id: string }
+}) {
+ const helpers = createServerSideHelpers({
+ router: appRouter,
+ ctx: await createContext(),
+ transformer: superjson
+ });
+
+ // Prefetch on Server
+ await helpers.user.getById.prefetch(params.id);
+
+ return (
+
+
+
+ );
+}
+```
+
+---
+
+## Subscriptions (WebSocket)
+
+```typescript
+// Server
+export const chatRouter = router({
+ onMessage: publicProcedure
+ .input(z.object({ roomId: z.string() }))
+ .subscription(async function* ({ input }) {
+ // Async Generator für Subscription
+ for await (const message of messageStream(input.roomId)) {
+ yield message;
+ }
+ })
+});
+
+// Client
+function ChatRoom({ roomId }: { roomId: string }) {
+ const [messages, setMessages] = useState([]);
+
+ trpc.chat.onMessage.useSubscription(
+ { roomId },
+ {
+ onData: (message) => {
+ setMessages(prev => [...prev, message]);
+ }
+ }
+ );
+
+ return (/* ... */);
+}
+```
+
+---
+
+## Error Handling
+
+```typescript
+// Server-Side Error
+import { TRPCError } from '@trpc/server';
+
+export const postRouter = router({
+ delete: protectedProcedure
+ .input(z.string())
+ .mutation(async ({ ctx, input }) => {
+ const post = await prisma.post.findUnique({
+ where: { id: input }
+ });
+
+ if (!post) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Post nicht gefunden'
+ });
+ }
+
+ if (post.authorId !== ctx.user.id) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ message: 'Keine Berechtigung'
+ });
+ }
+
+ return prisma.post.delete({ where: { id: input } });
+ })
+});
+
+// Client-Side Handling
+const deletePost = trpc.post.delete.useMutation({
+ onError: (error) => {
+ switch (error.data?.code) {
+ case 'NOT_FOUND':
+ toast.error('Post existiert nicht');
+ break;
+ case 'FORBIDDEN':
+ toast.error('Keine Berechtigung');
+ break;
+ default:
+ toast.error('Ein Fehler ist aufgetreten');
+ }
+ }
+});
+```
+
+---
+
+## Fazit
+
+tRPC bietet:
+
+1. **Zero-Config Type Safety**: TypeScript-Types automatisch geteilt
+2. **Keine Code-Generierung**: Kein GraphQL Codegen, kein OpenAPI
+3. **React Query Integration**: Caching, Optimistic Updates built-in
+4. **Zod Validation**: Runtime + Compile-Time Safety
+
+Für TypeScript-Monorepos ist tRPC die perfekte API-Lösung.
+
+---
+
+## Bildprompts
+
+1. "TypeScript types flowing seamlessly between server and client, connected code blocks"
+2. "API calls with automatic type completion, developer productivity visualization"
+3. "Bridge connecting frontend and backend with TypeScript logo, full-stack concept"
+
+---
+
+## Quellen
+
+- [tRPC Documentation](https://trpc.io/)
+- [tRPC GitHub](https://github.com/trpc/trpc)
+- [tRPC + Next.js App Router](https://trpc.io/docs/client/nextjs/setup)
+- [tRPC + Zod](https://trpc.io/docs/server/validators)
diff --git a/blog-posts/36-zustand-vs-jotai.md b/blog-posts/36-zustand-vs-jotai.md
new file mode 100644
index 0000000..7563c1e
--- /dev/null
+++ b/blog-posts/36-zustand-vs-jotai.md
@@ -0,0 +1,523 @@
+# Zustand vs. Jotai: Moderne State Management für React 2026
+
+**Meta-Description:** Vergleich der führenden React State Management Libraries. Zustand vs. Jotai - Architektur, Performance, Use Cases und Entscheidungshilfe.
+
+**Keywords:** Zustand, Jotai, React State Management, Redux Alternative, Atomic State, Global State, React Context
+
+---
+
+## Einführung
+
+Redux-Fatigue ist real. 2026 dominieren **Zustand** und **Jotai** als leichtgewichtige, TypeScript-first Alternativen. Beide kommen von Poimandres (formerly pmndrs) – aber lösen verschiedene Probleme.
+
+---
+
+## Quick Comparison
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ ZUSTAND vs. JOTAI │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ ZUSTAND JOTAI │
+│ ──────────────────── ──────────────────── │
+│ Store-basiert Atom-basiert │
+│ Top-Down Bottom-Up │
+│ Zentraler State Dezentraler State │
+│ Simpler für globalen State Flexibler für UI State │
+│ │
+│ Best for: Best for: │
+│ • App-weiter State • Component-lokaler State │
+│ • Einfache Stores • Derived State │
+│ • Server State (mit Persist) • Async State │
+│ • Redux-Migration • Code-Splitting │
+│ │
+│ Bundle: ~1.2KB Bundle: ~2.2KB │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Zustand
+
+### Basic Store
+
+```typescript
+// stores/useStore.ts
+import { create } from 'zustand';
+
+interface CounterState {
+ count: number;
+ increment: () => void;
+ decrement: () => void;
+ reset: () => void;
+}
+
+export const useCounterStore = create((set) => ({
+ count: 0,
+ increment: () => set((state) => ({ count: state.count + 1 })),
+ decrement: () => set((state) => ({ count: state.count - 1 })),
+ reset: () => set({ count: 0 })
+}));
+
+// Verwendung in Component
+function Counter() {
+ const { count, increment, decrement } = useCounterStore();
+
+ return (
+
+ {count}
+
+
+
+ );
+}
+
+// Selective Subscription (Performance!)
+function CountDisplay() {
+ const count = useCounterStore((state) => state.count);
+ return {count};
+}
+```
+
+### Komplexer Store mit Slices
+
+```typescript
+// stores/userStore.ts
+import { create } from 'zustand';
+import { persist, devtools } from 'zustand/middleware';
+
+interface User {
+ id: string;
+ name: string;
+ email: string;
+}
+
+interface UserState {
+ user: User | null;
+ isAuthenticated: boolean;
+ isLoading: boolean;
+ error: string | null;
+
+ login: (email: string, password: string) => Promise;
+ logout: () => void;
+ updateProfile: (data: Partial) => Promise;
+}
+
+export const useUserStore = create()(
+ devtools(
+ persist(
+ (set, get) => ({
+ user: null,
+ isAuthenticated: false,
+ isLoading: false,
+ error: null,
+
+ login: async (email, password) => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ body: JSON.stringify({ email, password })
+ });
+
+ if (!response.ok) throw new Error('Login failed');
+
+ const user = await response.json();
+ set({
+ user,
+ isAuthenticated: true,
+ isLoading: false
+ });
+ } catch (error) {
+ set({
+ error: error.message,
+ isLoading: false
+ });
+ }
+ },
+
+ logout: () => {
+ set({
+ user: null,
+ isAuthenticated: false
+ });
+ },
+
+ updateProfile: async (data) => {
+ const { user } = get();
+ if (!user) return;
+
+ set({ isLoading: true });
+ try {
+ const response = await fetch('/api/user/profile', {
+ method: 'PATCH',
+ body: JSON.stringify(data)
+ });
+
+ const updatedUser = await response.json();
+ set({
+ user: updatedUser,
+ isLoading: false
+ });
+ } catch (error) {
+ set({
+ error: error.message,
+ isLoading: false
+ });
+ }
+ }
+ }),
+ {
+ name: 'user-storage', // localStorage key
+ partialize: (state) => ({ user: state.user }) // Nur user persistieren
+ }
+ ),
+ { name: 'UserStore' } // DevTools name
+ )
+);
+```
+
+### Store Slices Pattern
+
+```typescript
+// stores/slices/cartSlice.ts
+import { StateCreator } from 'zustand';
+
+export interface CartSlice {
+ items: CartItem[];
+ addItem: (item: CartItem) => void;
+ removeItem: (id: string) => void;
+ clearCart: () => void;
+ totalPrice: () => number;
+}
+
+export const createCartSlice: StateCreator = (set, get) => ({
+ items: [],
+
+ addItem: (item) => set((state) => ({
+ items: [...state.items, item]
+ })),
+
+ removeItem: (id) => set((state) => ({
+ items: state.items.filter((i) => i.id !== id)
+ })),
+
+ clearCart: () => set({ items: [] }),
+
+ totalPrice: () => get().items.reduce((sum, item) => sum + item.price, 0)
+});
+
+// stores/index.ts
+import { create } from 'zustand';
+import { CartSlice, createCartSlice } from './slices/cartSlice';
+import { UserSlice, createUserSlice } from './slices/userSlice';
+
+type StoreState = CartSlice & UserSlice;
+
+export const useStore = create()((...a) => ({
+ ...createCartSlice(...a),
+ ...createUserSlice(...a)
+}));
+```
+
+---
+
+## Jotai
+
+### Basic Atoms
+
+```typescript
+// atoms/counter.ts
+import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
+
+// Primitive Atom
+export const countAtom = atom(0);
+
+// Derived Atom (Read-only)
+export const doubleCountAtom = atom((get) => get(countAtom) * 2);
+
+// Derived Atom (Read-Write)
+export const countWithDoubleAtom = atom(
+ (get) => get(countAtom),
+ (get, set, newValue: number) => {
+ set(countAtom, newValue * 2);
+ }
+);
+
+// Verwendung
+function Counter() {
+ const [count, setCount] = useAtom(countAtom);
+ const doubleCount = useAtomValue(doubleCountAtom);
+
+ return (
+
+ {count} (double: {doubleCount})
+
+
+ );
+}
+
+// Nur Setter (keine Re-renders bei count-Änderung)
+function IncrementButton() {
+ const setCount = useSetAtom(countAtom);
+ return ;
+}
+```
+
+### Async Atoms
+
+```typescript
+// atoms/user.ts
+import { atom } from 'jotai';
+import { atomWithQuery } from 'jotai-tanstack-query';
+
+// Async Read Atom
+export const userAtom = atom(async () => {
+ const response = await fetch('/api/user');
+ return response.json();
+});
+
+// Mit React Query Integration
+export const userQueryAtom = atomWithQuery(() => ({
+ queryKey: ['user'],
+ queryFn: async () => {
+ const response = await fetch('/api/user');
+ return response.json();
+ }
+}));
+
+// Verwendung mit Suspense
+function UserProfile() {
+ const [user] = useAtom(userAtom);
+ // user ist bereits resolved!
+ return {user.name}
;
+}
+
+// In parent
+}>
+
+
+```
+
+### Atom Families
+
+```typescript
+// atoms/todos.ts
+import { atom } from 'jotai';
+import { atomFamily } from 'jotai/utils';
+
+interface Todo {
+ id: string;
+ text: string;
+ done: boolean;
+}
+
+// Atom für jeden Todo (by ID)
+export const todoAtomFamily = atomFamily((id: string) =>
+ atom(null)
+);
+
+// Liste der IDs
+export const todoIdsAtom = atom([]);
+
+// Derived: Alle Todos
+export const todosAtom = atom((get) => {
+ const ids = get(todoIdsAtom);
+ return ids.map(id => get(todoAtomFamily(id))).filter(Boolean);
+});
+
+// Verwendung
+function TodoItem({ id }: { id: string }) {
+ const [todo, setTodo] = useAtom(todoAtomFamily(id));
+
+ if (!todo) return null;
+
+ return (
+
+ setTodo({ ...todo, done: !todo.done })}
+ />
+ {todo.text}
+
+ );
+}
+```
+
+### Persistence mit atomWithStorage
+
+```typescript
+import { atomWithStorage } from 'jotai/utils';
+
+// Automatisch in localStorage persistiert
+export const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');
+
+export const settingsAtom = atomWithStorage('settings', {
+ notifications: true,
+ language: 'de'
+});
+
+// Session Storage
+export const sessionDataAtom = atomWithStorage(
+ 'session',
+ null,
+ undefined, // default serializer
+ { getOnInit: true }
+);
+```
+
+---
+
+## Vergleich: Gleiche Funktionalität
+
+### Shopping Cart mit Zustand
+
+```typescript
+// stores/cartStore.ts
+import { create } from 'zustand';
+
+interface CartStore {
+ items: CartItem[];
+ addItem: (item: CartItem) => void;
+ removeItem: (id: string) => void;
+ updateQuantity: (id: string, quantity: number) => void;
+ total: number;
+}
+
+export const useCartStore = create((set, get) => ({
+ items: [],
+
+ addItem: (item) => set((state) => {
+ const existing = state.items.find(i => i.id === item.id);
+ if (existing) {
+ return {
+ items: state.items.map(i =>
+ i.id === item.id
+ ? { ...i, quantity: i.quantity + 1 }
+ : i
+ )
+ };
+ }
+ return { items: [...state.items, { ...item, quantity: 1 }] };
+ }),
+
+ removeItem: (id) => set((state) => ({
+ items: state.items.filter(i => i.id !== id)
+ })),
+
+ updateQuantity: (id, quantity) => set((state) => ({
+ items: state.items.map(i =>
+ i.id === id ? { ...i, quantity } : i
+ )
+ })),
+
+ get total() {
+ return get().items.reduce(
+ (sum, item) => sum + item.price * item.quantity,
+ 0
+ );
+ }
+}));
+```
+
+### Shopping Cart mit Jotai
+
+```typescript
+// atoms/cart.ts
+import { atom } from 'jotai';
+
+export const cartItemsAtom = atom([]);
+
+export const addItemAtom = atom(
+ null,
+ (get, set, item: CartItem) => {
+ const items = get(cartItemsAtom);
+ const existing = items.find(i => i.id === item.id);
+
+ if (existing) {
+ set(cartItemsAtom, items.map(i =>
+ i.id === item.id
+ ? { ...i, quantity: i.quantity + 1 }
+ : i
+ ));
+ } else {
+ set(cartItemsAtom, [...items, { ...item, quantity: 1 }]);
+ }
+ }
+);
+
+export const removeItemAtom = atom(
+ null,
+ (get, set, id: string) => {
+ set(cartItemsAtom, get(cartItemsAtom).filter(i => i.id !== id));
+ }
+);
+
+export const cartTotalAtom = atom((get) => {
+ const items = get(cartItemsAtom);
+ return items.reduce(
+ (sum, item) => sum + item.price * item.quantity,
+ 0
+ );
+});
+```
+
+---
+
+## Entscheidungshilfe
+
+| Kriterium | Zustand | Jotai |
+|-----------|---------|-------|
+| **Lernkurve** | Sehr einfach | Einfach |
+| **Bundle Size** | 1.2KB | 2.2KB |
+| **Globaler State** | Exzellent | Gut |
+| **Komponenten-State** | Gut | Exzellent |
+| **Derived State** | Selectors | Atoms (eleganter) |
+| **Async** | Middleware | Nativ |
+| **DevTools** | Ja | Ja |
+| **Redux Migration** | Einfach | Schwieriger |
+
+### Wähle Zustand wenn:
+
+- Du einen zentralen, Redux-ähnlichen Store willst
+- Dein State hauptsächlich global ist
+- Du von Redux migrierst
+- Du Actions und State zusammen halten willst
+
+### Wähle Jotai wenn:
+
+- Du viel derived/computed State hast
+- Du feingranulare Re-Renders brauchst
+- Du React Suspense für Async State nutzt
+- Du atomare Updates bevorzugst
+
+---
+
+## Fazit
+
+Beide Libraries sind exzellent. Die Wahl hängt vom Denkmodell ab:
+
+- **Zustand**: "Ein Store, viele Slices" (Top-Down)
+- **Jotai**: "Viele Atoms, komponiert" (Bottom-Up)
+
+Für die meisten Apps: **Zustand** für Einfachheit, **Jotai** für Flexibilität.
+
+---
+
+## Bildprompts
+
+1. "Two state management approaches - central store vs distributed atoms, architectural diagram"
+2. "React components with state flowing down, tree structure visualization"
+3. "Lightweight boxes vs heavy Redux container, bundle size comparison concept"
+
+---
+
+## Quellen
+
+- [Zustand GitHub](https://github.com/pmndrs/zustand)
+- [Jotai Documentation](https://jotai.org/)
+- [Zustand vs Jotai Comparison](https://docs.pmnd.rs/zustand/getting-started/comparison)
+- [Jotai Tutorial](https://jotai.org/docs/basics/primitives)
diff --git a/blog-posts/37-tanstack-query.md b/blog-posts/37-tanstack-query.md
new file mode 100644
index 0000000..a0944c0
--- /dev/null
+++ b/blog-posts/37-tanstack-query.md
@@ -0,0 +1,503 @@
+# TanStack Query: Intelligentes Data Fetching für React
+
+**Meta-Description:** TanStack Query (React Query) für Server State Management. Caching, Background Refetch, Optimistic Updates und Best Practices.
+
+**Keywords:** TanStack Query, React Query, Data Fetching, Server State, Caching, React, API Client, SWR
+
+---
+
+## Einführung
+
+TanStack Query (ehemals React Query) löst das schwerste Problem in React: **Server State Management**. Statt manuelles Fetching, Loading States und Caching zu bauen, bietet es eine deklarative, caching-first Lösung.
+
+---
+
+## Warum TanStack Query?
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ SERVER STATE CHALLENGES │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Ohne TanStack Query: │
+│ ├── Manuelles Loading/Error State │
+│ ├── Keine automatische Cache-Invalidierung │
+│ ├── Duplizierte Requests │
+│ ├── Kein Background Refetch │
+│ ├── Komplizierte Pagination │
+│ └── Race Conditions │
+│ │
+│ Mit TanStack Query: │
+│ ├── Automatisches Loading/Error Handling │
+│ ├── Intelligentes Caching │
+│ ├── Request Deduplication │
+│ ├── Stale-While-Revalidate │
+│ ├── Eingebaute Pagination/Infinite Scroll │
+│ └── Automatische Retry-Logik │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Setup
+
+```typescript
+// app/providers.tsx
+'use client';
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import { useState } from 'react';
+
+export function QueryProvider({ children }: { children: React.ReactNode }) {
+ const [queryClient] = useState(() => new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60 * 1000, // 1 Minute
+ gcTime: 5 * 60 * 1000, // 5 Minuten (früher cacheTime)
+ retry: 3,
+ refetchOnWindowFocus: true,
+ refetchOnReconnect: true
+ }
+ }
+ }));
+
+ return (
+
+ {children}
+
+
+ );
+}
+```
+
+---
+
+## Basic Queries
+
+### useQuery
+
+```typescript
+import { useQuery } from '@tanstack/react-query';
+
+// API Function
+async function fetchUser(userId: string): Promise {
+ const response = await fetch(`/api/users/${userId}`);
+ if (!response.ok) throw new Error('Failed to fetch user');
+ return response.json();
+}
+
+// Component
+function UserProfile({ userId }: { userId: string }) {
+ const {
+ data: user,
+ isLoading,
+ isError,
+ error,
+ isFetching, // true auch bei Background Refetch
+ isStale, // Daten sind "veraltet"
+ refetch // Manueller Refetch
+ } = useQuery({
+ queryKey: ['user', userId],
+ queryFn: () => fetchUser(userId),
+ enabled: !!userId, // Nur fetchen wenn userId existiert
+ staleTime: 5 * 60 * 1000,
+ placeholderData: previousUser // Zeige alte Daten während Refetch
+ });
+
+ if (isLoading) return ;
+ if (isError) return ;
+
+ return (
+
+
{user.name}
+ {isFetching && Updating...}
+
+ );
+}
+```
+
+### Query mit abhängigen Daten
+
+```typescript
+function UserPosts({ userId }: { userId: string }) {
+ // Erst User laden
+ const { data: user } = useQuery({
+ queryKey: ['user', userId],
+ queryFn: () => fetchUser(userId)
+ });
+
+ // Dann Posts (nur wenn User da)
+ const { data: posts } = useQuery({
+ queryKey: ['posts', user?.id],
+ queryFn: () => fetchUserPosts(user!.id),
+ enabled: !!user // Wartet auf User
+ });
+
+ return (/* ... */);
+}
+```
+
+---
+
+## Mutations
+
+```typescript
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
+function CreatePostForm() {
+ const queryClient = useQueryClient();
+
+ const createPost = useMutation({
+ mutationFn: async (newPost: CreatePostInput) => {
+ const response = await fetch('/api/posts', {
+ method: 'POST',
+ body: JSON.stringify(newPost)
+ });
+ if (!response.ok) throw new Error('Failed to create post');
+ return response.json();
+ },
+
+ // Bei Erfolg: Cache invalidieren
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['posts'] });
+ },
+
+ // Oder: Optimistic Update
+ onMutate: async (newPost) => {
+ // Laufende Queries canceln
+ await queryClient.cancelQueries({ queryKey: ['posts'] });
+
+ // Snapshot des aktuellen States
+ const previousPosts = queryClient.getQueryData(['posts']);
+
+ // Optimistisch updaten
+ queryClient.setQueryData(['posts'], (old: Post[]) => [
+ { id: 'temp', ...newPost },
+ ...old
+ ]);
+
+ return { previousPosts };
+ },
+
+ onError: (err, newPost, context) => {
+ // Bei Fehler: Rollback
+ queryClient.setQueryData(['posts'], context?.previousPosts);
+ },
+
+ onSettled: () => {
+ // Immer am Ende: Refetch für Konsistenz
+ queryClient.invalidateQueries({ queryKey: ['posts'] });
+ }
+ });
+
+ const handleSubmit = (data: FormData) => {
+ createPost.mutate({
+ title: data.get('title') as string,
+ content: data.get('content') as string
+ });
+ };
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Pagination & Infinite Scroll
+
+### Pagination
+
+```typescript
+function PaginatedPosts() {
+ const [page, setPage] = useState(1);
+
+ const { data, isPlaceholderData } = useQuery({
+ queryKey: ['posts', page],
+ queryFn: () => fetchPosts(page),
+ placeholderData: keepPreviousData // Zeigt alte Daten während neuer Page lädt
+ });
+
+ return (
+
+ {data?.posts.map(post =>
)}
+
+
+
+
+ Seite {page}
+
+
+
+
+ );
+}
+```
+
+### Infinite Scroll
+
+```typescript
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { useInView } from 'react-intersection-observer';
+
+function InfinitePosts() {
+ const { ref, inView } = useInView();
+
+ const {
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage
+ } = useInfiniteQuery({
+ queryKey: ['posts', 'infinite'],
+ queryFn: ({ pageParam }) => fetchPosts(pageParam),
+ initialPageParam: 1,
+ getNextPageParam: (lastPage) =>
+ lastPage.hasMore ? lastPage.nextPage : undefined
+ });
+
+ // Auto-load bei Scroll
+ useEffect(() => {
+ if (inView && hasNextPage) {
+ fetchNextPage();
+ }
+ }, [inView, hasNextPage, fetchNextPage]);
+
+ return (
+
+ {data?.pages.map(page =>
+ page.posts.map(post =>
)
+ )}
+
+
+ {isFetchingNextPage
+ ?
+ : hasNextPage
+ ? 'Mehr laden...'
+ : 'Keine weiteren Posts'}
+
+
+ );
+}
+```
+
+---
+
+## Prefetching
+
+```typescript
+// Hover Prefetch
+function PostLink({ postId }: { postId: string }) {
+ const queryClient = useQueryClient();
+
+ const prefetchPost = () => {
+ queryClient.prefetchQuery({
+ queryKey: ['post', postId],
+ queryFn: () => fetchPost(postId),
+ staleTime: 60 * 1000
+ });
+ };
+
+ return (
+
+ Post anzeigen
+
+ );
+}
+
+// Server-Side Prefetch (Next.js)
+export async function getServerSideProps() {
+ const queryClient = new QueryClient();
+
+ await queryClient.prefetchQuery({
+ queryKey: ['posts'],
+ queryFn: fetchPosts
+ });
+
+ return {
+ props: {
+ dehydratedState: dehydrate(queryClient)
+ }
+ };
+}
+```
+
+---
+
+## Query Invalidation
+
+```typescript
+const queryClient = useQueryClient();
+
+// Einzelnen Query invalidieren
+queryClient.invalidateQueries({ queryKey: ['user', userId] });
+
+// Alle User-Queries
+queryClient.invalidateQueries({ queryKey: ['user'] });
+
+// Alle Queries
+queryClient.invalidateQueries();
+
+// Mit Predicate
+queryClient.invalidateQueries({
+ predicate: (query) =>
+ query.queryKey[0] === 'post' &&
+ (query.queryKey[1] as Post)?.authorId === userId
+});
+
+// Nur refetchen wenn aktiv (sichtbar)
+queryClient.invalidateQueries({
+ queryKey: ['posts'],
+ refetchType: 'active' // 'all' | 'active' | 'inactive' | 'none'
+});
+```
+
+---
+
+## Suspense Mode
+
+```typescript
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+function UserProfile({ userId }: { userId: string }) {
+ // Wirft Promise während Loading (für Suspense)
+ const { data: user } = useSuspenseQuery({
+ queryKey: ['user', userId],
+ queryFn: () => fetchUser(userId)
+ });
+
+ // user ist garantiert da (kein undefined)
+ return {user.name}
;
+}
+
+// Parent
+function UserPage({ userId }: { userId: string }) {
+ return (
+ }>
+
+
+ );
+}
+```
+
+---
+
+## Custom Hooks Pattern
+
+```typescript
+// hooks/useUser.ts
+export function useUser(userId: string) {
+ return useQuery({
+ queryKey: ['user', userId],
+ queryFn: () => fetchUser(userId),
+ enabled: !!userId
+ });
+}
+
+export function useUpdateUser() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: updateUser,
+ onSuccess: (data, variables) => {
+ queryClient.setQueryData(['user', variables.id], data);
+ }
+ });
+}
+
+// Verwendung
+function Profile({ userId }: { userId: string }) {
+ const { data: user, isLoading } = useUser(userId);
+ const updateUser = useUpdateUser();
+
+ // ...
+}
+```
+
+---
+
+## DevTools & Debugging
+
+```typescript
+// Query Logging
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ onError: (error) => {
+ console.error('Query error:', error);
+ // Sentry, LogRocket, etc.
+ }
+ },
+ mutations: {
+ onError: (error) => {
+ console.error('Mutation error:', error);
+ }
+ }
+ }
+});
+
+// DevTools
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+
+
+ {children}
+
+
+```
+
+---
+
+## Fazit
+
+TanStack Query bietet:
+
+1. **Automatisches Caching**: Keine manuelle Cache-Logik
+2. **Background Refetch**: Daten bleiben frisch
+3. **Optimistic Updates**: Schnelle UX
+4. **DevTools**: Debugging leicht gemacht
+
+Für jede App mit Server-Daten ist TanStack Query unverzichtbar.
+
+---
+
+## Bildprompts
+
+1. "Data flowing from server to UI with caching layer in between, architectural diagram"
+2. "Stale data refreshing in background, real-time update visualization"
+3. "Query cache tree with multiple components accessing same data, efficiency concept"
+
+---
+
+## Quellen
+
+- [TanStack Query Documentation](https://tanstack.com/query)
+- [TanStack Query GitHub](https://github.com/TanStack/query)
+- [Practical React Query](https://tkdodo.eu/blog/practical-react-query)
+- [React Query DevTools](https://tanstack.com/query/latest/docs/framework/react/devtools)
diff --git a/blog-posts/38-react-hook-form.md b/blog-posts/38-react-hook-form.md
new file mode 100644
index 0000000..9df7a7c
--- /dev/null
+++ b/blog-posts/38-react-hook-form.md
@@ -0,0 +1,574 @@
+# React Hook Form: Performante Formulare mit TypeScript
+
+**Meta-Description:** React Hook Form Masterguide für 2026. Performance-Optimierung, TypeScript-Patterns, Zod-Integration und Server Actions Support.
+
+**Keywords:** React Hook Form, Form Validation, TypeScript Forms, Zod Integration, Server Actions, React Forms, Uncontrolled Components
+
+---
+
+## Einführung
+
+React Hook Form ist der **Gold-Standard für Formulare in React 2026**. Durch Uncontrolled Components und Ref-basiertes State Management erreicht es minimale Re-Renders – selbst bei Formularen mit hunderten Feldern.
+
+---
+
+## Warum React Hook Form?
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ REACT HOOK FORM VORTEILE │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Performance: │
+│ ├── Uncontrolled Components (keine Re-Renders) │
+│ ├── Ref-basiertes State Management │
+│ ├── Isolierte Input-Subscriptions │
+│ └── 100+ Felder ohne Lag │
+│ │
+│ Developer Experience: │
+│ ├── Minimale API │
+│ ├── First-Class TypeScript Support │
+│ ├── Tree-Shakeable (~8KB gzipped) │
+│ └── Keine Dependencies │
+│ │
+│ 2026 Features: │
+│ ├── Server Actions Integration │
+│ ├── useActionState Support │
+│ └── useFormStatus Hook │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Basic Setup
+
+```typescript
+import { useForm } from 'react-hook-form';
+
+interface LoginForm {
+ email: string;
+ password: string;
+ rememberMe: boolean;
+}
+
+function LoginPage() {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isSubmitting }
+ } = useForm({
+ defaultValues: {
+ email: '',
+ password: '',
+ rememberMe: false
+ }
+ });
+
+ const onSubmit = async (data: LoginForm) => {
+ console.log(data); // Vollständig typisiert!
+ await login(data);
+ };
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Zod Integration
+
+```typescript
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+
+// Schema definieren
+const SignupSchema = z.object({
+ name: z.string()
+ .min(2, 'Name zu kurz')
+ .max(50, 'Name zu lang'),
+ email: z.string()
+ .email('Ungültige E-Mail'),
+ password: z.string()
+ .min(8, 'Mindestens 8 Zeichen')
+ .regex(/[A-Z]/, 'Mindestens ein Großbuchstabe')
+ .regex(/[0-9]/, 'Mindestens eine Zahl'),
+ confirmPassword: z.string(),
+ terms: z.literal(true, {
+ errorMap: () => ({ message: 'Sie müssen die AGB akzeptieren' })
+ })
+}).refine(data => data.password === data.confirmPassword, {
+ message: 'Passwörter stimmen nicht überein',
+ path: ['confirmPassword']
+});
+
+// Type aus Schema inferieren
+type SignupData = z.infer;
+
+function SignupForm() {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors }
+ } = useForm({
+ resolver: zodResolver(SignupSchema),
+ mode: 'onBlur' // Validierung bei Blur
+ });
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Controller für UI Libraries
+
+```typescript
+import { useForm, Controller } from 'react-hook-form';
+import { Select, DatePicker, Switch } from './ui-library';
+
+interface ProfileForm {
+ country: string;
+ birthDate: Date;
+ newsletter: boolean;
+}
+
+function ProfileForm() {
+ const { control, handleSubmit } = useForm();
+
+ return (
+
+ );
+}
+```
+
+---
+
+## useFormContext für Nested Components
+
+```typescript
+import { useForm, FormProvider, useFormContext } from 'react-hook-form';
+
+interface CheckoutForm {
+ shipping: {
+ name: string;
+ address: string;
+ city: string;
+ };
+ billing: {
+ cardNumber: string;
+ expiry: string;
+ cvv: string;
+ };
+}
+
+// Parent Form
+function CheckoutPage() {
+ const methods = useForm();
+
+ return (
+
+
+
+ );
+}
+
+// Child Component mit typisiertem Context
+function ShippingSection() {
+ const { register, formState: { errors } } = useFormContext();
+
+ return (
+
+ );
+}
+
+function BillingSection() {
+ const { register } = useFormContext();
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Field Arrays (Dynamische Felder)
+
+```typescript
+import { useForm, useFieldArray } from 'react-hook-form';
+
+interface OrderForm {
+ items: {
+ productId: string;
+ quantity: number;
+ price: number;
+ }[];
+}
+
+function OrderForm() {
+ const { control, register, handleSubmit, watch } = useForm({
+ defaultValues: {
+ items: [{ productId: '', quantity: 1, price: 0 }]
+ }
+ });
+
+ const { fields, append, remove, move } = useFieldArray({
+ control,
+ name: 'items'
+ });
+
+ const watchItems = watch('items');
+ const total = watchItems.reduce((sum, item) =>
+ sum + (item.quantity * item.price), 0
+ );
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Server Actions Integration (Next.js 15)
+
+```typescript
+// app/actions.ts
+'use server';
+
+import { z } from 'zod';
+
+const ContactSchema = z.object({
+ name: z.string().min(2),
+ email: z.string().email(),
+ message: z.string().min(10)
+});
+
+export async function submitContact(formData: FormData) {
+ const data = ContactSchema.parse({
+ name: formData.get('name'),
+ email: formData.get('email'),
+ message: formData.get('message')
+ });
+
+ await sendEmail(data);
+ return { success: true };
+}
+
+// app/contact/page.tsx
+'use client';
+
+import { useForm } from 'react-hook-form';
+import { useActionState } from 'react';
+import { submitContact } from './actions';
+
+function ContactForm() {
+ const { register, handleSubmit, formState: { errors } } = useForm();
+ const [state, formAction, isPending] = useActionState(submitContact, null);
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Performance-Optimierung
+
+```typescript
+import { useForm, useWatch } from 'react-hook-form';
+import { memo } from 'react';
+
+// 1. Isolierte Watch für einzelne Felder
+function PriceDisplay({ control }: { control: Control }) {
+ // Nur dieses Feld subscriben
+ const price = useWatch({ control, name: 'price' });
+ return {price} €;
+}
+
+// 2. Memoized Components
+const ExpensiveField = memo(function ExpensiveField({
+ register,
+ name
+}: {
+ register: UseFormRegister;
+ name: keyof FormData;
+}) {
+ return ;
+});
+
+// 3. Mode-Strategien
+const { register } = useForm({
+ mode: 'onBlur', // Validierung nur bei Blur
+ reValidateMode: 'onChange', // Re-Validierung bei Change
+ shouldFocusError: true, // Fokus auf erstes Fehlerfeld
+ criteriaMode: 'firstError' // Nur erster Fehler pro Feld
+});
+
+// 4. Verzögerte Validierung
+const { register } = useForm({
+ delayError: 500 // Fehleranzeige um 500ms verzögern
+});
+```
+
+---
+
+## Error Handling Patterns
+
+```typescript
+import { useForm, FieldErrors } from 'react-hook-form';
+
+// Globales Error Display
+function ErrorSummary({ errors }: { errors: FieldErrors }) {
+ const errorMessages = Object.entries(errors)
+ .filter(([_, error]) => error?.message)
+ .map(([field, error]) => ({
+ field,
+ message: error?.message as string
+ }));
+
+ if (errorMessages.length === 0) return null;
+
+ return (
+
+
Bitte korrigieren Sie folgende Fehler:
+
+ {errorMessages.map(({ field, message }) => (
+ - {message}
+ ))}
+
+
+ );
+}
+
+// Server Error Integration
+function FormWithServerErrors() {
+ const {
+ setError,
+ clearErrors,
+ formState: { errors }
+ } = useForm();
+
+ const onSubmit = async (data: FormData) => {
+ try {
+ await submitToServer(data);
+ } catch (error) {
+ if (error instanceof ValidationError) {
+ // Server-Errors in Form setzen
+ error.fields.forEach(({ name, message }) => {
+ setError(name, { type: 'server', message });
+ });
+ } else {
+ // Globaler Error
+ setError('root', {
+ type: 'server',
+ message: 'Ein unerwarteter Fehler ist aufgetreten'
+ });
+ }
+ }
+ };
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Fazit
+
+React Hook Form bietet:
+
+1. **Maximale Performance**: Uncontrolled Components, minimale Re-Renders
+2. **TypeScript-First**: Volle Type-Safety ohne Aufwand
+3. **Flexible Validation**: Zod, Yup, Superstruct Integration
+4. **Server Actions Ready**: Perfekte Integration mit Next.js 15
+
+Für jedes React-Projekt 2026 ist React Hook Form die erste Wahl.
+
+---
+
+## Bildprompts
+
+1. "Form inputs with performance metrics overlay, minimal re-render visualization"
+2. "TypeScript code flowing into form components, type safety concept"
+3. "Server and client form validation synchronization, full-stack concept"
+
+---
+
+## Quellen
+
+- [React Hook Form Documentation](https://react-hook-form.com/)
+- [React Hook Form TypeScript Support](https://www.react-hook-form.com/ts/)
+- [Zod Resolver](https://react-hook-form.com/get-started#SchemaValidation)
+- [Best React Form Libraries 2026](https://blog.croct.com/post/best-react-form-libraries)
diff --git a/blog-posts/39-vitest-testing.md b/blog-posts/39-vitest-testing.md
new file mode 100644
index 0000000..cf73ba5
--- /dev/null
+++ b/blog-posts/39-vitest-testing.md
@@ -0,0 +1,603 @@
+# Vitest: Next-Gen Testing mit Browser Mode
+
+**Meta-Description:** Vitest als Vite-native Testing Framework. Browser Mode, Component Testing, TypeScript-Support und Migration von Jest.
+
+**Keywords:** Vitest, Testing, Browser Mode, Component Testing, Playwright, React Testing, TypeScript, Jest Alternative
+
+---
+
+## Einführung
+
+Vitest ist das **native Testing-Framework für Vite** und hat Jest in modernen Projekten weitgehend abgelöst. Mit **Browser Mode** testet Vitest Components in echten Browsern – ohne JSDOM-Limitierungen.
+
+---
+
+## Warum Vitest?
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ VITEST vs JEST │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ VITEST JEST │
+│ ──────────────────── ──────────────────── │
+│ Vite-Native Babel-basiert │
+│ ESM First CommonJS Default │
+│ Shared Config mit Vite Separate Config │
+│ Hot Module Replacement Full Restart │
+│ Browser Mode JSDOM only │
+│ │
+│ Performance: │
+│ ├── Instant Watch Mode │
+│ ├── Native TypeScript Support │
+│ ├── Out-of-Box ESM Support │
+│ └── Shared Vite Pipeline │
+│ │
+│ Bundle: ~5MB vs ~65MB (Jest + Babel) │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Quick Setup
+
+```bash
+# Installation
+npm install -D vitest
+
+# Browser Mode (optional)
+npm install -D @vitest/browser playwright
+
+# UI (optional)
+npm install -D @vitest/ui
+```
+
+```typescript
+// vite.config.ts
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: './src/test/setup.ts',
+ include: ['**/*.{test,spec}.{js,ts,jsx,tsx}'],
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'html', 'lcov']
+ }
+ }
+});
+```
+
+```typescript
+// src/test/setup.ts
+import '@testing-library/jest-dom';
+import { cleanup } from '@testing-library/react';
+import { afterEach } from 'vitest';
+
+afterEach(() => {
+ cleanup();
+});
+```
+
+---
+
+## Basic Unit Testing
+
+```typescript
+// src/utils/math.ts
+export function add(a: number, b: number): number {
+ return a + b;
+}
+
+export function multiply(a: number, b: number): number {
+ return a * b;
+}
+
+export async function fetchData(url: string): Promise {
+ const response = await fetch(url);
+ return response.json();
+}
+
+// src/utils/math.test.ts
+import { describe, it, expect, vi } from 'vitest';
+import { add, multiply, fetchData } from './math';
+
+describe('Math Utils', () => {
+ it('should add two numbers', () => {
+ expect(add(2, 3)).toBe(5);
+ expect(add(-1, 1)).toBe(0);
+ });
+
+ it('should multiply two numbers', () => {
+ expect(multiply(2, 3)).toBe(6);
+ expect(multiply(0, 100)).toBe(0);
+ });
+});
+
+describe('fetchData', () => {
+ it('should fetch and parse JSON', async () => {
+ const mockData = { name: 'Test' };
+
+ // Mock fetch
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
+ json: () => Promise.resolve(mockData)
+ }));
+
+ const result = await fetchData('/api/test');
+ expect(result).toEqual(mockData);
+
+ vi.unstubAllGlobals();
+ });
+});
+```
+
+---
+
+## Component Testing (JSDOM)
+
+```typescript
+// src/components/Counter.tsx
+import { useState } from 'react';
+
+interface CounterProps {
+ initialValue?: number;
+ onCountChange?: (count: number) => void;
+}
+
+export function Counter({ initialValue = 0, onCountChange }: CounterProps) {
+ const [count, setCount] = useState(initialValue);
+
+ const increment = () => {
+ const newCount = count + 1;
+ setCount(newCount);
+ onCountChange?.(newCount);
+ };
+
+ const decrement = () => {
+ const newCount = count - 1;
+ setCount(newCount);
+ onCountChange?.(newCount);
+ };
+
+ return (
+
+ {count}
+
+
+
+ );
+}
+
+// src/components/Counter.test.tsx
+import { describe, it, expect, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { Counter } from './Counter';
+
+describe('Counter', () => {
+ it('renders with initial value', () => {
+ render();
+ expect(screen.getByTestId('count')).toHaveTextContent('5');
+ });
+
+ it('increments count when + clicked', () => {
+ render();
+
+ fireEvent.click(screen.getByText('+'));
+
+ expect(screen.getByTestId('count')).toHaveTextContent('1');
+ });
+
+ it('decrements count when - clicked', () => {
+ render();
+
+ fireEvent.click(screen.getByText('-'));
+
+ expect(screen.getByTestId('count')).toHaveTextContent('4');
+ });
+
+ it('calls onCountChange callback', () => {
+ const handleChange = vi.fn();
+ render();
+
+ fireEvent.click(screen.getByText('+'));
+
+ expect(handleChange).toHaveBeenCalledWith(1);
+ });
+});
+```
+
+---
+
+## Browser Mode (Real Browser Testing)
+
+```bash
+# Browser Mode initialisieren
+npx vitest init browser
+```
+
+```typescript
+// vitest.config.ts
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ browser: {
+ enabled: true,
+ provider: 'playwright', // oder 'webdriverio'
+ name: 'chromium',
+ headless: true
+ }
+ }
+});
+```
+
+```typescript
+// src/components/Dialog.browser.test.tsx
+import { describe, it, expect } from 'vitest';
+import { render } from 'vitest-browser-react';
+import { Dialog } from './Dialog';
+
+describe('Dialog (Browser Mode)', () => {
+ it('should open and close correctly', async () => {
+ const screen = render(
+
+ );
+
+ // Initial: Dialog geschlossen
+ await expect.element(screen.getByText('Dialog Content')).not.toBeVisible();
+
+ // Öffnen
+ await screen.getByText('Open').click();
+ await expect.element(screen.getByText('Dialog Content')).toBeVisible();
+
+ // Schließen mit Escape
+ await screen.getByRole('dialog').press('Escape');
+ await expect.element(screen.getByText('Dialog Content')).not.toBeVisible();
+ });
+
+ it('should focus trap correctly', async () => {
+ const screen = render(
+
+ );
+
+ await screen.getByText('Open').click();
+
+ // Fokus sollte im Dialog gefangen sein
+ const input1 = screen.getByTestId('input1');
+ const input2 = screen.getByTestId('input2');
+
+ await input1.focus();
+ await input1.press('Tab');
+ await expect.element(input2).toBeFocused();
+ });
+});
+```
+
+---
+
+## Mocking
+
+```typescript
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+
+// 1. Function Mocking
+const mockFn = vi.fn();
+mockFn.mockReturnValue(42);
+mockFn.mockResolvedValue({ data: 'test' });
+mockFn.mockImplementation((x) => x * 2);
+
+// 2. Module Mocking
+vi.mock('./api', () => ({
+ fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }),
+ updateUser: vi.fn().mockResolvedValue({ success: true })
+}));
+
+// 3. Partial Mocking
+vi.mock('./utils', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ complexFunction: vi.fn().mockReturnValue('mocked')
+ };
+});
+
+// 4. Timer Mocking
+describe('Timer Tests', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('should handle setTimeout', async () => {
+ const callback = vi.fn();
+
+ setTimeout(callback, 1000);
+
+ expect(callback).not.toHaveBeenCalled();
+
+ vi.advanceTimersByTime(1000);
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should handle intervals', () => {
+ const callback = vi.fn();
+
+ setInterval(callback, 100);
+
+ vi.advanceTimersByTime(350);
+
+ expect(callback).toHaveBeenCalledTimes(3);
+ });
+});
+
+// 5. Date Mocking
+it('should mock current date', () => {
+ const mockDate = new Date('2026-01-15');
+ vi.setSystemTime(mockDate);
+
+ expect(new Date().toISOString()).toContain('2026-01-15');
+
+ vi.useRealTimers();
+});
+```
+
+---
+
+## Snapshot Testing
+
+```typescript
+import { describe, it, expect } from 'vitest';
+import { render } from '@testing-library/react';
+import { UserCard } from './UserCard';
+
+describe('UserCard Snapshots', () => {
+ it('matches snapshot for basic user', () => {
+ const { container } = render(
+
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+
+ it('matches inline snapshot', () => {
+ const user = { name: 'Jane', role: 'admin' };
+
+ expect(user).toMatchInlineSnapshot(`
+ {
+ "name": "Jane",
+ "role": "admin",
+ }
+ `);
+ });
+
+ // File Snapshots
+ it('matches file snapshot for large data', () => {
+ const complexData = generateLargeDataset();
+
+ expect(complexData).toMatchFileSnapshot('./snapshots/large-data.json');
+ });
+});
+```
+
+---
+
+## Test Coverage
+
+```typescript
+// vitest.config.ts
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ coverage: {
+ provider: 'v8', // oder 'istanbul'
+ reporter: ['text', 'html', 'lcov', 'json'],
+ reportsDirectory: './coverage',
+ include: ['src/**/*.{ts,tsx}'],
+ exclude: [
+ 'src/**/*.test.{ts,tsx}',
+ 'src/**/*.d.ts',
+ 'src/test/**/*'
+ ],
+ thresholds: {
+ statements: 80,
+ branches: 80,
+ functions: 80,
+ lines: 80
+ }
+ }
+ }
+});
+```
+
+```bash
+# Coverage generieren
+npx vitest --coverage
+
+# Watch Mode mit Coverage
+npx vitest --coverage --watch
+```
+
+---
+
+## Parallel & Sequential Tests
+
+```typescript
+import { describe, it, expect } from 'vitest';
+
+// Parallel (Default)
+describe('Parallel Tests', () => {
+ it.concurrent('test 1', async () => {
+ await sleep(1000);
+ expect(true).toBe(true);
+ });
+
+ it.concurrent('test 2', async () => {
+ await sleep(1000);
+ expect(true).toBe(true);
+ });
+ // Beide laufen parallel → ~1s total
+});
+
+// Sequential
+describe('Sequential Tests', { sequential: true }, () => {
+ let sharedState = 0;
+
+ it('first', () => {
+ sharedState = 1;
+ expect(sharedState).toBe(1);
+ });
+
+ it('second', () => {
+ sharedState = 2;
+ expect(sharedState).toBe(2);
+ });
+});
+
+// Test Isolation
+describe.concurrent('Isolated Concurrent', () => {
+ // Jeder Test bekommt eigene Isolation
+ it('isolated 1', async ({ expect }) => {
+ // ...
+ });
+
+ it('isolated 2', async ({ expect }) => {
+ // ...
+ });
+});
+```
+
+---
+
+## API Testing
+
+```typescript
+import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+import { setupServer } from 'msw/node';
+import { http, HttpResponse } from 'msw';
+
+const server = setupServer(
+ http.get('/api/users', () => {
+ return HttpResponse.json([
+ { id: 1, name: 'Alice' },
+ { id: 2, name: 'Bob' }
+ ]);
+ }),
+
+ http.post('/api/users', async ({ request }) => {
+ const body = await request.json();
+ return HttpResponse.json({ id: 3, ...body }, { status: 201 });
+ })
+);
+
+beforeAll(() => server.listen());
+afterAll(() => server.close());
+
+describe('API Client', () => {
+ it('fetches users', async () => {
+ const response = await fetch('/api/users');
+ const users = await response.json();
+
+ expect(users).toHaveLength(2);
+ expect(users[0].name).toBe('Alice');
+ });
+
+ it('creates a user', async () => {
+ const response = await fetch('/api/users', {
+ method: 'POST',
+ body: JSON.stringify({ name: 'Charlie' })
+ });
+
+ expect(response.status).toBe(201);
+
+ const user = await response.json();
+ expect(user.name).toBe('Charlie');
+ });
+});
+```
+
+---
+
+## Migration von Jest
+
+```typescript
+// jest.config.js → vitest.config.ts
+
+// Jest
+module.exports = {
+ testEnvironment: 'jsdom',
+ setupFilesAfterEnv: ['/src/setupTests.ts'],
+ moduleNameMapper: {
+ '^@/(.*)$': '/src/$1'
+ }
+};
+
+// Vitest
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ environment: 'jsdom',
+ setupFiles: './src/setupTests.ts',
+ alias: {
+ '@': './src'
+ }
+ }
+});
+
+// API-Änderungen
+// Jest: jest.fn() → Vitest: vi.fn()
+// Jest: jest.mock() → Vitest: vi.mock()
+// Jest: jest.spyOn() → Vitest: vi.spyOn()
+```
+
+---
+
+## Fazit
+
+Vitest bietet:
+
+1. **Vite-Native**: Shared Config, HMR, sofortiger Start
+2. **Browser Mode**: Echte Browser statt JSDOM
+3. **TypeScript-First**: Out-of-Box Support ohne Config
+4. **Jest-kompatibel**: Einfache Migration
+
+Für jedes Vite-Projekt ist Vitest die natürliche Wahl.
+
+---
+
+## Bildprompts
+
+1. "Test runner showing green checkmarks, development workflow visualization"
+2. "Browser and code split screen, component testing in real browser concept"
+3. "Fast forward icon with test results, instant feedback development loop"
+
+---
+
+## Quellen
+
+- [Vitest Documentation](https://vitest.dev/)
+- [Vitest Browser Mode](https://vitest.dev/guide/browser/)
+- [vitest-browser-react](https://github.com/vitest-dev/vitest-browser-react)
+- [Vitest vs JSDOM - InfoQ](https://www.infoq.com/news/2025/06/vitest-browser-mode-jsdom/)
diff --git a/blog-posts/40-radix-ui-components.md b/blog-posts/40-radix-ui-components.md
new file mode 100644
index 0000000..8387a8e
--- /dev/null
+++ b/blog-posts/40-radix-ui-components.md
@@ -0,0 +1,589 @@
+# Radix UI: Accessible Headless Components für React
+
+**Meta-Description:** Radix UI Primitives für barrierefreie React-Komponenten. Headless Architecture, WAI-ARIA Compliance und shadcn/ui Integration.
+
+**Keywords:** Radix UI, Headless Components, Accessibility, WAI-ARIA, React Components, shadcn/ui, Unstyled Components
+
+---
+
+## Einführung
+
+Radix UI ist eine **Headless Component Library** für React. Statt vorgestylter Komponenten liefert sie zugängliche, ungestylte Bausteine – perfekte Grundlage für Design Systems mit voller Accessibility out-of-the-box.
+
+---
+
+## Warum Radix UI?
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ RADIX UI VORTEILE │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Accessibility (A11Y): │
+│ ├── WAI-ARIA Design Patterns │
+│ ├── Keyboard Navigation │
+│ ├── Focus Management │
+│ ├── Screen Reader Support │
+│ └── Reduced Motion Support │
+│ │
+│ Headless Architecture: │
+│ ├── Zero Styles (volle Kontrolle) │
+│ ├── Jedes Styling möglich (CSS, Tailwind, CSS-in-JS) │
+│ ├── Kein UI Lock-in │
+│ └── Perfekt für Design Systems │
+│ │
+│ Developer Experience: │
+│ ├── Composable APIs │
+│ ├── TypeScript Support │
+│ ├── SSR Compatible │
+│ └── Tree-Shakeable │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Installation
+
+```bash
+# Einzelne Primitives installieren
+npm install @radix-ui/react-dialog
+npm install @radix-ui/react-dropdown-menu
+npm install @radix-ui/react-tabs
+npm install @radix-ui/react-tooltip
+
+# Oder alle Primitives
+npm install @radix-ui/primitives
+```
+
+---
+
+## Dialog (Modal)
+
+```typescript
+import * as Dialog from '@radix-ui/react-dialog';
+import { X } from 'lucide-react';
+
+function ConfirmDialog({
+ trigger,
+ title,
+ description,
+ onConfirm
+}: {
+ trigger: React.ReactNode;
+ title: string;
+ description: string;
+ onConfirm: () => void;
+}) {
+ return (
+
+
+ {trigger}
+
+
+
+
+
+
+
+ {title}
+
+
+
+ {description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// Verwendung
+Löschen}
+ title="Eintrag löschen?"
+ description="Diese Aktion kann nicht rückgängig gemacht werden."
+ onConfirm={() => deleteItem(id)}
+/>
+```
+
+---
+
+## Dropdown Menu
+
+```typescript
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
+import { Check, ChevronRight, Circle } from 'lucide-react';
+
+function UserMenu({ user }: { user: User }) {
+ const [bookmarksChecked, setBookmarksChecked] = useState(true);
+ const [person, setPerson] = useState('pedro');
+
+ return (
+
+
+
+
+
+
+
+ {/* Regular Items */}
+
+ Profil
+
+
+
+ Einstellungen
+
+
+
+
+ {/* Checkbox Item */}
+
+
+
+
+ Lesezeichen anzeigen
+
+
+
+
+ {/* Radio Group */}
+
+ Personen
+
+
+
+
+
+
+
+ Pedro
+
+
+
+
+
+
+ Maria
+
+
+
+
+
+ {/* Sub Menu */}
+
+
+ Mehr
+
+
+
+
+
+
+ Hilfe
+
+
+ Über uns
+
+
+
+
+
+
+
+ {/* Destructive Item */}
+
+ Abmelden
+
+
+
+
+ );
+}
+```
+
+---
+
+## Tabs
+
+```typescript
+import * as Tabs from '@radix-ui/react-tabs';
+
+function SettingsTabs() {
+ return (
+
+
+
+ Account
+
+
+
+ Passwort
+
+
+
+ Benachrichtigungen
+
+
+
+
+ Account-Einstellungen
+
+
+
+
+ Passwort ändern
+ {/* Passwort Form */}
+
+
+
+ Benachrichtigungen
+ {/* Notification Settings */}
+
+
+ );
+}
+```
+
+---
+
+## Tooltip
+
+```typescript
+import * as Tooltip from '@radix-ui/react-tooltip';
+
+function IconButton({
+ icon,
+ label,
+ onClick
+}: {
+ icon: React.ReactNode;
+ label: string;
+ onClick: () => void;
+}) {
+ return (
+
+
+
+
+
+
+
+
+ {label}
+
+
+
+
+
+ );
+}
+
+// Verwendung
+}
+ label="Einstellungen"
+ onClick={() => openSettings()}
+/>
+```
+
+---
+
+## Accordion
+
+```typescript
+import * as Accordion from '@radix-ui/react-accordion';
+import { ChevronDown } from 'lucide-react';
+
+interface FAQItem {
+ question: string;
+ answer: string;
+}
+
+function FAQ({ items }: { items: FAQItem[] }) {
+ return (
+
+ {items.map((item, index) => (
+
+
+
+ {item.question}
+
+
+
+
+
+
+ {item.answer}
+
+
+
+ ))}
+
+ );
+}
+```
+
+---
+
+## Select
+
+```typescript
+import * as Select from '@radix-ui/react-select';
+import { Check, ChevronDown, ChevronUp } from 'lucide-react';
+
+interface Option {
+ value: string;
+ label: string;
+}
+
+function CustomSelect({
+ options,
+ value,
+ onChange,
+ placeholder = 'Auswählen...'
+}: {
+ options: Option[];
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+}) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {options.map((option) => (
+
+
+
+
+ {option.label}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+---
+
+## shadcn/ui Integration
+
+shadcn/ui baut auf Radix Primitives und Tailwind CSS:
+
+```bash
+# shadcn/ui initialisieren
+npx shadcn@latest init
+
+# Komponenten hinzufügen
+npx shadcn@latest add button
+npx shadcn@latest add dialog
+npx shadcn@latest add dropdown-menu
+```
+
+```typescript
+// components/ui/dialog.tsx (generiert von shadcn/ui)
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import { cn } from '@/lib/utils';
+
+const Dialog = DialogPrimitive.Root;
+const DialogTrigger = DialogPrimitive.Trigger;
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+));
+```
+
+---
+
+## Accessibility Best Practices
+
+```typescript
+// 1. Keyboard Navigation
+// Radix handled automatisch:
+// - Tab: Fokus zwischen interaktiven Elementen
+// - Enter/Space: Aktivierung
+// - Escape: Schließen von Overlays
+// - Arrow Keys: Navigation in Listen/Menüs
+
+// 2. ARIA Attributes
+// Automatisch gesetzt von Radix:
+// - aria-expanded
+// - aria-controls
+// - aria-labelledby
+// - role="dialog", role="menu", etc.
+
+// 3. Focus Management
+// Radix handled:
+// - Focus Trap in Modals
+// - Focus Restore beim Schließen
+// - Focus auf erstes interaktives Element
+
+// 4. Reduced Motion
+import * as Dialog from '@radix-ui/react-dialog';
+
+// CSS mit prefers-reduced-motion
+const styles = `
+ .dialog-content {
+ animation: slideIn 200ms ease-out;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .dialog-content {
+ animation: none;
+ }
+ }
+`;
+```
+
+---
+
+## Fazit
+
+Radix UI bietet:
+
+1. **Volle Accessibility**: WAI-ARIA compliant out-of-the-box
+2. **Styling-Freiheit**: Headless Architecture, kein UI Lock-in
+3. **Composable**: Flexible, zusammensetzbare APIs
+4. **shadcn/ui Basis**: Foundation für moderne Design Systems
+
+Für barrierefreie React-Anwendungen ist Radix UI die beste Grundlage.
+
+---
+
+## Bildprompts
+
+1. "Accessibility symbols surrounding React components, inclusive design concept"
+2. "Unstyled building blocks transforming into styled components, headless architecture"
+3. "Keyboard navigation flow through UI components, accessibility visualization"
+
+---
+
+## Quellen
+
+- [Radix UI Documentation](https://www.radix-ui.com/)
+- [Radix Primitives](https://www.radix-ui.com/primitives)
+- [shadcn/ui](https://ui.shadcn.com/)
+- [WAI-ARIA Design Patterns](https://www.w3.org/WAI/ARIA/apg/patterns/)
diff --git a/blog-posts/41-supabase-fullstack-platform.md b/blog-posts/41-supabase-fullstack-platform.md
new file mode 100644
index 0000000..6b7e338
--- /dev/null
+++ b/blog-posts/41-supabase-fullstack-platform.md
@@ -0,0 +1,585 @@
+# Supabase: Die Open-Source Firebase Alternative für 2026
+
+**Meta-Description:** Supabase als vollständige Backend-Plattform. PostgreSQL, Realtime, Auth, Edge Functions und Vector Embeddings in einem Stack.
+
+**Keywords:** Supabase, PostgreSQL, Realtime Database, Edge Functions, Authentication, Firebase Alternative, Vector Embeddings
+
+---
+
+## Einführung
+
+Supabase ist die **Open-Source Firebase Alternative**, die auf PostgreSQL aufbaut. 2026 bietet es eine vollständige Backend-Plattform: Database, Auth, Realtime, Edge Functions, Storage und Vector Embeddings – alles integriert.
+
+---
+
+## Supabase Stack
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ SUPABASE PLATFORM │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Database Layer: │
+│ ├── PostgreSQL (Enterprise-Grade) │
+│ ├── Row Level Security (RLS) │
+│ ├── PostgREST (Auto-Generated APIs) │
+│ └── pgvector (Vector Embeddings) │
+│ │
+│ Realtime Layer: │
+│ ├── Postgres Changes (CDC) │
+│ ├── Broadcast (User-to-User) │
+│ ├── Presence (Online Status) │
+│ └── WebSocket Connections │
+│ │
+│ Auth Layer: │
+│ ├── Email/Password │
+│ ├── OAuth Providers (Google, GitHub, etc.) │
+│ ├── Magic Links │
+│ └── Phone/SMS Auth │
+│ │
+│ Edge Functions: │
+│ ├── Deno Runtime │
+│ ├── TypeScript/JavaScript │
+│ └── Global Distribution │
+│ │
+│ Storage: │
+│ ├── S3-Compatible │
+│ ├── CDN Integration │
+│ └── Image Transformations │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Database Setup
+
+```typescript
+// lib/supabase.ts
+import { createClient } from '@supabase/supabase-js';
+import { Database } from './database.types';
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
+const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
+
+export const supabase = createClient(supabaseUrl, supabaseAnonKey);
+
+// Server-Side mit Service Role
+import { createClient } from '@supabase/supabase-js';
+
+export const supabaseAdmin = createClient(
+ process.env.SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
+ {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false
+ }
+ }
+);
+```
+
+### Schema Definition (SQL)
+
+```sql
+-- Users Tabelle (erweitert auth.users)
+CREATE TABLE public.profiles (
+ id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
+ username TEXT UNIQUE,
+ full_name TEXT,
+ avatar_url TEXT,
+ bio TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Posts Tabelle
+CREATE TABLE public.posts (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ author_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE NOT NULL,
+ title TEXT NOT NULL,
+ content TEXT,
+ published BOOLEAN DEFAULT FALSE,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Trigger für updated_at
+CREATE OR REPLACE FUNCTION update_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER profiles_updated_at
+ BEFORE UPDATE ON public.profiles
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
+
+CREATE TRIGGER posts_updated_at
+ BEFORE UPDATE ON public.posts
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at();
+```
+
+---
+
+## Row Level Security (RLS)
+
+```sql
+-- RLS aktivieren
+ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;
+
+-- Profiles: Jeder kann lesen, nur Owner kann schreiben
+CREATE POLICY "Profiles sind öffentlich lesbar"
+ ON public.profiles FOR SELECT
+ USING (true);
+
+CREATE POLICY "User kann eigenes Profil bearbeiten"
+ ON public.profiles FOR UPDATE
+ USING (auth.uid() = id);
+
+-- Posts: Veröffentlichte sind lesbar, Autor kann alles
+CREATE POLICY "Veröffentlichte Posts sind lesbar"
+ ON public.posts FOR SELECT
+ USING (published = true OR auth.uid() = author_id);
+
+CREATE POLICY "Autor kann eigene Posts erstellen"
+ ON public.posts FOR INSERT
+ WITH CHECK (auth.uid() = author_id);
+
+CREATE POLICY "Autor kann eigene Posts bearbeiten"
+ ON public.posts FOR UPDATE
+ USING (auth.uid() = author_id);
+
+CREATE POLICY "Autor kann eigene Posts löschen"
+ ON public.posts FOR DELETE
+ USING (auth.uid() = author_id);
+```
+
+---
+
+## CRUD Operations
+
+```typescript
+// CREATE
+async function createPost(post: { title: string; content: string }) {
+ const { data, error } = await supabase
+ .from('posts')
+ .insert({
+ title: post.title,
+ content: post.content,
+ author_id: (await supabase.auth.getUser()).data.user?.id
+ })
+ .select()
+ .single();
+
+ if (error) throw error;
+ return data;
+}
+
+// READ
+async function getPosts() {
+ const { data, error } = await supabase
+ .from('posts')
+ .select(`
+ *,
+ author:profiles(username, avatar_url)
+ `)
+ .eq('published', true)
+ .order('created_at', { ascending: false })
+ .limit(10);
+
+ if (error) throw error;
+ return data;
+}
+
+// READ mit Pagination
+async function getPostsPaginated(page: number, pageSize: number = 10) {
+ const from = page * pageSize;
+ const to = from + pageSize - 1;
+
+ const { data, error, count } = await supabase
+ .from('posts')
+ .select('*, author:profiles(username)', { count: 'exact' })
+ .eq('published', true)
+ .range(from, to);
+
+ return { data, count, hasMore: count ? to < count - 1 : false };
+}
+
+// UPDATE
+async function updatePost(id: string, updates: Partial) {
+ const { data, error } = await supabase
+ .from('posts')
+ .update(updates)
+ .eq('id', id)
+ .select()
+ .single();
+
+ if (error) throw error;
+ return data;
+}
+
+// DELETE
+async function deletePost(id: string) {
+ const { error } = await supabase
+ .from('posts')
+ .delete()
+ .eq('id', id);
+
+ if (error) throw error;
+}
+```
+
+---
+
+## Realtime Subscriptions
+
+```typescript
+import { useEffect, useState } from 'react';
+import { supabase } from '@/lib/supabase';
+import { RealtimeChannel } from '@supabase/supabase-js';
+
+// 1. Database Changes (CDC)
+function useRealtimePosts() {
+ const [posts, setPosts] = useState([]);
+
+ useEffect(() => {
+ // Initial Load
+ supabase.from('posts').select('*').then(({ data }) => {
+ if (data) setPosts(data);
+ });
+
+ // Subscribe to Changes
+ const channel = supabase
+ .channel('posts-changes')
+ .on(
+ 'postgres_changes',
+ {
+ event: '*',
+ schema: 'public',
+ table: 'posts'
+ },
+ (payload) => {
+ if (payload.eventType === 'INSERT') {
+ setPosts(prev => [payload.new as Post, ...prev]);
+ } else if (payload.eventType === 'UPDATE') {
+ setPosts(prev =>
+ prev.map(p => p.id === payload.new.id ? payload.new as Post : p)
+ );
+ } else if (payload.eventType === 'DELETE') {
+ setPosts(prev => prev.filter(p => p.id !== payload.old.id));
+ }
+ }
+ )
+ .subscribe();
+
+ return () => {
+ supabase.removeChannel(channel);
+ };
+ }, []);
+
+ return posts;
+}
+
+// 2. Broadcast (User-to-User Messaging)
+function useBroadcast(roomId: string) {
+ const [messages, setMessages] = useState([]);
+
+ useEffect(() => {
+ const channel = supabase.channel(`room:${roomId}`)
+ .on('broadcast', { event: 'message' }, ({ payload }) => {
+ setMessages(prev => [...prev, payload]);
+ })
+ .subscribe();
+
+ return () => {
+ supabase.removeChannel(channel);
+ };
+ }, [roomId]);
+
+ const sendMessage = (content: string) => {
+ supabase.channel(`room:${roomId}`).send({
+ type: 'broadcast',
+ event: 'message',
+ payload: { content, timestamp: new Date().toISOString() }
+ });
+ };
+
+ return { messages, sendMessage };
+}
+
+// 3. Presence (Online Status)
+function usePresence(roomId: string, userId: string) {
+ const [onlineUsers, setOnlineUsers] = useState([]);
+
+ useEffect(() => {
+ const channel = supabase.channel(`presence:${roomId}`)
+ .on('presence', { event: 'sync' }, () => {
+ const state = channel.presenceState();
+ const users = Object.values(state).flat().map(p => p.user_id);
+ setOnlineUsers(users);
+ })
+ .subscribe(async (status) => {
+ if (status === 'SUBSCRIBED') {
+ await channel.track({ user_id: userId });
+ }
+ });
+
+ return () => {
+ supabase.removeChannel(channel);
+ };
+ }, [roomId, userId]);
+
+ return onlineUsers;
+}
+```
+
+---
+
+## Authentication
+
+```typescript
+// Sign Up
+async function signUp(email: string, password: string) {
+ const { data, error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ emailRedirectTo: `${window.location.origin}/auth/callback`
+ }
+ });
+ return { data, error };
+}
+
+// Sign In
+async function signIn(email: string, password: string) {
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password
+ });
+ return { data, error };
+}
+
+// OAuth Sign In
+async function signInWithOAuth(provider: 'google' | 'github') {
+ const { data, error } = await supabase.auth.signInWithOAuth({
+ provider,
+ options: {
+ redirectTo: `${window.location.origin}/auth/callback`
+ }
+ });
+ return { data, error };
+}
+
+// Sign Out
+async function signOut() {
+ const { error } = await supabase.auth.signOut();
+ return { error };
+}
+
+// Auth State Hook
+function useAuth() {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ supabase.auth.getSession().then(({ data: { session } }) => {
+ setUser(session?.user ?? null);
+ setLoading(false);
+ });
+
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(
+ (_event, session) => {
+ setUser(session?.user ?? null);
+ }
+ );
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ return { user, loading };
+}
+```
+
+---
+
+## Edge Functions
+
+```typescript
+// supabase/functions/send-email/index.ts
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
+
+const corsHeaders = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
+};
+
+serve(async (req) => {
+ if (req.method === 'OPTIONS') {
+ return new Response('ok', { headers: corsHeaders });
+ }
+
+ try {
+ const supabase = createClient(
+ Deno.env.get('SUPABASE_URL')!,
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+ );
+
+ const { to, subject, body } = await req.json();
+
+ // Send email via Resend, SendGrid, etc.
+ const response = await fetch('https://api.resend.com/emails', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ from: 'noreply@example.com',
+ to,
+ subject,
+ html: body
+ })
+ });
+
+ const result = await response.json();
+
+ return new Response(JSON.stringify(result), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ return new Response(JSON.stringify({ error: error.message }), {
+ status: 500,
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+ });
+ }
+});
+
+// Client-Side Aufruf
+const { data, error } = await supabase.functions.invoke('send-email', {
+ body: {
+ to: 'user@example.com',
+ subject: 'Welcome!',
+ body: 'Welcome to our platform!
'
+ }
+});
+```
+
+---
+
+## Vector Embeddings (AI)
+
+```sql
+-- pgvector Extension aktivieren
+CREATE EXTENSION IF NOT EXISTS vector;
+
+-- Documents Tabelle mit Embeddings
+CREATE TABLE documents (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ content TEXT NOT NULL,
+ embedding VECTOR(1536), -- OpenAI ada-002 Dimension
+ metadata JSONB,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Index für schnelle Similarity Search
+CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
+ WITH (lists = 100);
+
+-- Similarity Search Funktion
+CREATE OR REPLACE FUNCTION search_documents(
+ query_embedding VECTOR(1536),
+ match_count INT DEFAULT 5
+)
+RETURNS TABLE (
+ id UUID,
+ content TEXT,
+ similarity FLOAT
+) AS $$
+BEGIN
+ RETURN QUERY
+ SELECT
+ documents.id,
+ documents.content,
+ 1 - (documents.embedding <=> query_embedding) AS similarity
+ FROM documents
+ ORDER BY documents.embedding <=> query_embedding
+ LIMIT match_count;
+END;
+$$ LANGUAGE plpgsql;
+```
+
+```typescript
+// Embedding generieren und speichern
+async function addDocument(content: string) {
+ // Embedding via OpenAI
+ const embeddingResponse = await openai.embeddings.create({
+ model: 'text-embedding-ada-002',
+ input: content
+ });
+
+ const embedding = embeddingResponse.data[0].embedding;
+
+ // In Supabase speichern
+ const { data, error } = await supabase
+ .from('documents')
+ .insert({ content, embedding })
+ .select()
+ .single();
+
+ return data;
+}
+
+// Similarity Search
+async function searchDocuments(query: string) {
+ // Query Embedding
+ const embeddingResponse = await openai.embeddings.create({
+ model: 'text-embedding-ada-002',
+ input: query
+ });
+
+ const queryEmbedding = embeddingResponse.data[0].embedding;
+
+ // Search via RPC
+ const { data, error } = await supabase.rpc('search_documents', {
+ query_embedding: queryEmbedding,
+ match_count: 5
+ });
+
+ return data;
+}
+```
+
+---
+
+## Fazit
+
+Supabase bietet 2026:
+
+1. **PostgreSQL Power**: Enterprise-DB mit RLS, Triggers, Functions
+2. **Realtime Built-in**: CDC, Broadcast, Presence über WebSockets
+3. **Edge Functions**: Deno-basiert, global verteilt
+4. **Vector Search**: pgvector für AI/RAG-Anwendungen
+
+Eine vollständige Backend-Plattform für moderne Anwendungen.
+
+---
+
+## Bildprompts
+
+1. "Database and real-time connections flowing together, Supabase architecture visualization"
+2. "PostgreSQL elephant with real-time lightning bolts, modern database concept"
+3. "Edge functions distributed globally on world map, serverless infrastructure"
+
+---
+
+## Quellen
+
+- [Supabase Documentation](https://supabase.com/docs)
+- [Supabase Features](https://supabase.com/features)
+- [Supabase GitHub](https://github.com/supabase/supabase)
+- [Supabase Review 2026](https://hackceleration.com/supabase-review/)
diff --git a/blog-posts/42-mongodb-atlas-vector-search.md b/blog-posts/42-mongodb-atlas-vector-search.md
new file mode 100644
index 0000000..ba6a83b
--- /dev/null
+++ b/blog-posts/42-mongodb-atlas-vector-search.md
@@ -0,0 +1,500 @@
+# MongoDB Atlas Vector Search für AI-Anwendungen
+
+**Meta-Description:** MongoDB Atlas Vector Search für semantische Suche und RAG. Embedding API, Hybrid Queries und Integration mit LLMs.
+
+**Keywords:** MongoDB Atlas, Vector Search, Semantic Search, RAG, Embeddings, AI Database, LLM Integration
+
+---
+
+## Einführung
+
+MongoDB Atlas bietet mit **Vector Search** eine native Lösung für AI-Anwendungen. Operative Daten und Vector Embeddings in einer Datenbank – ohne Sync-Probleme zwischen separaten Systemen.
+
+---
+
+## Vector Search Architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ MONGODB ATLAS VECTOR SEARCH │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Single Platform: │
+│ ├── Operative Daten (Documents) │
+│ ├── Vector Embeddings (1536+ Dimensions) │
+│ ├── Metadata (für Filtering) │
+│ └── Full-Text Search (Atlas Search) │
+│ │
+│ Vector Index Types: │
+│ ├── HNSW (Hierarchical Navigable Small World) │
+│ ├── IVF (Inverted File Index) │
+│ └── Vector Quantization (Cost Reduction) │
+│ │
+│ Use Cases: │
+│ ├── Semantic Search │
+│ ├── RAG (Retrieval-Augmented Generation) │
+│ ├── Recommendation Systems │
+│ └── Image/Audio Similarity │
+│ │
+│ Performance (Dedicated Search Nodes): │
+│ └── 40-60% schnellere Query Times │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Setup
+
+```typescript
+// lib/mongodb.ts
+import { MongoClient } from 'mongodb';
+
+const uri = process.env.MONGODB_URI!;
+const options = {};
+
+let client: MongoClient;
+let clientPromise: Promise;
+
+if (process.env.NODE_ENV === 'development') {
+ const globalWithMongo = global as typeof globalThis & {
+ _mongoClientPromise?: Promise;
+ };
+
+ if (!globalWithMongo._mongoClientPromise) {
+ client = new MongoClient(uri, options);
+ globalWithMongo._mongoClientPromise = client.connect();
+ }
+ clientPromise = globalWithMongo._mongoClientPromise;
+} else {
+ client = new MongoClient(uri, options);
+ clientPromise = client.connect();
+}
+
+export default clientPromise;
+```
+
+---
+
+## Vector Index erstellen
+
+```javascript
+// Atlas Search Index Definition (JSON)
+{
+ "name": "vector_index",
+ "type": "vectorSearch",
+ "definition": {
+ "fields": [
+ {
+ "type": "vector",
+ "path": "embedding",
+ "numDimensions": 1536,
+ "similarity": "cosine"
+ },
+ {
+ "type": "filter",
+ "path": "category"
+ },
+ {
+ "type": "filter",
+ "path": "createdAt"
+ }
+ ]
+ }
+}
+```
+
+```typescript
+// Programmatisch Index erstellen
+import { MongoClient } from 'mongodb';
+
+async function createVectorIndex() {
+ const client = await MongoClient.connect(process.env.MONGODB_URI!);
+ const db = client.db('myapp');
+
+ await db.command({
+ createSearchIndexes: 'documents',
+ indexes: [
+ {
+ name: 'vector_index',
+ type: 'vectorSearch',
+ definition: {
+ fields: [
+ {
+ type: 'vector',
+ path: 'embedding',
+ numDimensions: 1536,
+ similarity: 'cosine'
+ }
+ ]
+ }
+ }
+ ]
+ });
+
+ await client.close();
+}
+```
+
+---
+
+## Documents mit Embeddings speichern
+
+```typescript
+import OpenAI from 'openai';
+import clientPromise from '@/lib/mongodb';
+
+const openai = new OpenAI();
+
+interface Document {
+ _id?: string;
+ title: string;
+ content: string;
+ embedding: number[];
+ category: string;
+ createdAt: Date;
+}
+
+// Embedding generieren
+async function generateEmbedding(text: string): Promise {
+ const response = await openai.embeddings.create({
+ model: 'text-embedding-ada-002',
+ input: text
+ });
+ return response.data[0].embedding;
+}
+
+// Document speichern
+async function saveDocument(
+ title: string,
+ content: string,
+ category: string
+): Promise {
+ const client = await clientPromise;
+ const db = client.db('myapp');
+
+ // Embedding für Titel + Content
+ const embedding = await generateEmbedding(`${title}\n\n${content}`);
+
+ const document: Document = {
+ title,
+ content,
+ embedding,
+ category,
+ createdAt: new Date()
+ };
+
+ const result = await db.collection('documents').insertOne(document);
+ return { ...document, _id: result.insertedId.toString() };
+}
+
+// Bulk Import
+async function bulkImportDocuments(docs: Array<{ title: string; content: string; category: string }>) {
+ const client = await clientPromise;
+ const db = client.db('myapp');
+
+ const documentsWithEmbeddings = await Promise.all(
+ docs.map(async (doc) => ({
+ ...doc,
+ embedding: await generateEmbedding(`${doc.title}\n\n${doc.content}`),
+ createdAt: new Date()
+ }))
+ );
+
+ await db.collection('documents').insertMany(documentsWithEmbeddings);
+}
+```
+
+---
+
+## Vector Search Queries
+
+```typescript
+// Basic Vector Search
+async function searchDocuments(query: string, limit: number = 5) {
+ const client = await clientPromise;
+ const db = client.db('myapp');
+
+ const queryEmbedding = await generateEmbedding(query);
+
+ const results = await db.collection('documents').aggregate([
+ {
+ $vectorSearch: {
+ index: 'vector_index',
+ path: 'embedding',
+ queryVector: queryEmbedding,
+ numCandidates: 100,
+ limit: limit
+ }
+ },
+ {
+ $project: {
+ _id: 1,
+ title: 1,
+ content: 1,
+ category: 1,
+ score: { $meta: 'vectorSearchScore' }
+ }
+ }
+ ]).toArray();
+
+ return results;
+}
+
+// Vector Search mit Filter
+async function searchByCategory(
+ query: string,
+ category: string,
+ limit: number = 5
+) {
+ const client = await clientPromise;
+ const db = client.db('myapp');
+
+ const queryEmbedding = await generateEmbedding(query);
+
+ const results = await db.collection('documents').aggregate([
+ {
+ $vectorSearch: {
+ index: 'vector_index',
+ path: 'embedding',
+ queryVector: queryEmbedding,
+ filter: {
+ category: category
+ },
+ numCandidates: 100,
+ limit: limit
+ }
+ },
+ {
+ $project: {
+ title: 1,
+ content: 1,
+ score: { $meta: 'vectorSearchScore' }
+ }
+ }
+ ]).toArray();
+
+ return results;
+}
+
+// Hybrid Search (Vector + Full-Text)
+async function hybridSearch(query: string, limit: number = 10) {
+ const client = await clientPromise;
+ const db = client.db('myapp');
+
+ const queryEmbedding = await generateEmbedding(query);
+
+ const results = await db.collection('documents').aggregate([
+ {
+ $vectorSearch: {
+ index: 'vector_index',
+ path: 'embedding',
+ queryVector: queryEmbedding,
+ numCandidates: 150,
+ limit: 50
+ }
+ },
+ {
+ $addFields: {
+ vectorScore: { $meta: 'vectorSearchScore' }
+ }
+ },
+ {
+ $unionWith: {
+ coll: 'documents',
+ pipeline: [
+ {
+ $search: {
+ index: 'text_index',
+ text: {
+ query: query,
+ path: ['title', 'content']
+ }
+ }
+ },
+ {
+ $addFields: {
+ textScore: { $meta: 'searchScore' }
+ }
+ },
+ { $limit: 50 }
+ ]
+ }
+ },
+ {
+ $group: {
+ _id: '$_id',
+ title: { $first: '$title' },
+ content: { $first: '$content' },
+ vectorScore: { $max: '$vectorScore' },
+ textScore: { $max: '$textScore' }
+ }
+ },
+ {
+ $addFields: {
+ combinedScore: {
+ $add: [
+ { $ifNull: ['$vectorScore', 0] },
+ { $multiply: [{ $ifNull: ['$textScore', 0] }, 0.5] }
+ ]
+ }
+ }
+ },
+ { $sort: { combinedScore: -1 } },
+ { $limit: limit }
+ ]).toArray();
+
+ return results;
+}
+```
+
+---
+
+## RAG Implementation
+
+```typescript
+import OpenAI from 'openai';
+
+const openai = new OpenAI();
+
+async function ragQuery(userQuestion: string) {
+ // 1. Relevante Dokumente finden
+ const relevantDocs = await searchDocuments(userQuestion, 5);
+
+ // 2. Context aufbauen
+ const context = relevantDocs
+ .map(doc => `Title: ${doc.title}\nContent: ${doc.content}`)
+ .join('\n\n---\n\n');
+
+ // 3. LLM mit Context aufrufen
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4-turbo-preview',
+ messages: [
+ {
+ role: 'system',
+ content: `Du bist ein hilfreicher Assistent. Beantworte Fragen basierend auf dem folgenden Kontext. Wenn der Kontext die Antwort nicht enthält, sage das ehrlich.
+
+Kontext:
+${context}`
+ },
+ {
+ role: 'user',
+ content: userQuestion
+ }
+ ],
+ temperature: 0.7,
+ max_tokens: 1000
+ });
+
+ return {
+ answer: response.choices[0].message.content,
+ sources: relevantDocs.map(d => ({
+ title: d.title,
+ score: d.score
+ }))
+ };
+}
+```
+
+---
+
+## Automated Embedding mit Voyage AI
+
+```typescript
+// MongoDB Atlas Embedding API (Preview)
+// Automatisches Embedding ohne eigene OpenAI-Calls
+
+// Index Definition mit Auto-Embedding
+{
+ "name": "auto_vector_index",
+ "type": "vectorSearch",
+ "definition": {
+ "fields": [
+ {
+ "type": "vector",
+ "path": "embedding",
+ "numDimensions": 1024,
+ "similarity": "cosine"
+ }
+ ],
+ "embeddingModel": {
+ "name": "voyage-3",
+ "inputType": "document",
+ "fieldMappings": [
+ {
+ "source": "content",
+ "target": "embedding"
+ }
+ ]
+ }
+ }
+}
+
+// Documents ohne manuelles Embedding speichern
+async function saveDocumentAutoEmbed(title: string, content: string) {
+ const client = await clientPromise;
+ const db = client.db('myapp');
+
+ // Embedding wird automatisch generiert!
+ await db.collection('documents').insertOne({
+ title,
+ content,
+ createdAt: new Date()
+ });
+}
+```
+
+---
+
+## Vector Quantization (Kostenoptimierung)
+
+```javascript
+// Index mit Quantization für große Datasets
+{
+ "name": "quantized_vector_index",
+ "type": "vectorSearch",
+ "definition": {
+ "fields": [
+ {
+ "type": "vector",
+ "path": "embedding",
+ "numDimensions": 1536,
+ "similarity": "cosine",
+ "quantization": {
+ "type": "scalar" // Reduziert Speicher um ~75%
+ }
+ }
+ ]
+ }
+}
+```
+
+---
+
+## Fazit
+
+MongoDB Atlas Vector Search bietet:
+
+1. **Unified Platform**: Operative Daten + Vectors in einer DB
+2. **Hybrid Search**: Kombination von Vector + Full-Text
+3. **Auto-Embedding**: Voyage AI Integration für automatische Embeddings
+4. **Skalierbarkeit**: Milliarden Vectors mit Quantization
+
+Ideal für AI-Features in bestehenden MongoDB-Anwendungen.
+
+---
+
+## Bildprompts
+
+1. "Vector embeddings flowing into search results, semantic similarity visualization"
+2. "MongoDB document with vector arrows connecting similar documents"
+3. "RAG pipeline showing document retrieval and LLM generation, AI workflow"
+
+---
+
+## Quellen
+
+- [MongoDB Atlas Vector Search](https://www.mongodb.com/products/platform/atlas-vector-search)
+- [MongoDB Vector Search Overview](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-overview/)
+- [MongoDB Embedding API](https://www.mongodb.com/company/blog/product-release-announcements/introducing-the-embedding-and-reranking-api-on-mongodb-atlas)
+- [Vector Databases for LLM 2026](https://www.secondtalent.com/resources/top-vector-databases-for-llm-applications/)
diff --git a/blog-posts/43-redis-stack-vector-database.md b/blog-posts/43-redis-stack-vector-database.md
new file mode 100644
index 0000000..16245cd
--- /dev/null
+++ b/blog-posts/43-redis-stack-vector-database.md
@@ -0,0 +1,479 @@
+# Redis Stack: In-Memory Vector Database für AI
+
+**Meta-Description:** Redis als Vector Database nutzen. RediSearch, RedisJSON, Vector Set und High-Performance AI-Anwendungen.
+
+**Keywords:** Redis Stack, Vector Database, RediSearch, RedisJSON, In-Memory Database, AI Cache, Semantic Search
+
+---
+
+## Einführung
+
+Redis 8 vereint alle Module in einem Package: **Vector Search, JSON, Full-Text Search** und mehr. Als In-Memory Database bietet Redis ultra-niedrige Latenz für AI-Anwendungen – ideal für Caching, Session Management und Echtzeit-Suche.
+
+---
+
+## Redis 8 Stack
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ REDIS 8 STACK │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Core Data Structures: │
+│ ├── Strings, Lists, Sets, Hashes, Sorted Sets │
+│ ├── Streams (Event Streaming) │
+│ └── HyperLogLog, Bitmaps │
+│ │
+│ New in Redis 8: │
+│ ├── Vector Set (Beta) - Similarity Search │
+│ ├── JSON - Native JSON Document Store │
+│ ├── Time Series - Metrics & Monitoring │
+│ └── Probabilistic Structures │
+│ ├── Bloom Filter │
+│ ├── Cuckoo Filter │
+│ ├── Count-Min Sketch │
+│ ├── Top-K │
+│ └── T-Digest │
+│ │
+│ Search Capabilities: │
+│ ├── Full-Text Search │
+│ ├── Vector Search (FLAT, HNSW, SVS-VAMANA) │
+│ ├── Numeric/Tag Filtering │
+│ └── Geospatial Queries │
+│ │
+│ Performance: │
+│ └── Sub-Millisecond Latency (In-Memory) │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Setup
+
+```bash
+# Docker
+docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
+
+# Redis Cloud (Managed)
+# https://redis.com/try-free/
+```
+
+```typescript
+// lib/redis.ts
+import { createClient } from 'redis';
+
+const redis = createClient({
+ url: process.env.REDIS_URL || 'redis://localhost:6379'
+});
+
+redis.on('error', (err) => console.error('Redis Client Error', err));
+
+await redis.connect();
+
+export default redis;
+```
+
+---
+
+## Vector Search mit Hash
+
+```typescript
+import redis from '@/lib/redis';
+import { SchemaFieldTypes, VectorAlgorithms } from 'redis';
+
+// 1. Index erstellen
+async function createVectorIndex() {
+ try {
+ await redis.ft.create('idx:docs', {
+ '$.title': {
+ type: SchemaFieldTypes.TEXT,
+ AS: 'title'
+ },
+ '$.content': {
+ type: SchemaFieldTypes.TEXT,
+ AS: 'content'
+ },
+ '$.category': {
+ type: SchemaFieldTypes.TAG,
+ AS: 'category'
+ },
+ '$.embedding': {
+ type: SchemaFieldTypes.VECTOR,
+ AS: 'embedding',
+ ALGORITHM: VectorAlgorithms.HNSW,
+ TYPE: 'FLOAT32',
+ DIM: 1536,
+ DISTANCE_METRIC: 'COSINE'
+ }
+ }, {
+ ON: 'JSON',
+ PREFIX: 'doc:'
+ });
+ console.log('Index created');
+ } catch (e) {
+ if ((e as Error).message.includes('Index already exists')) {
+ console.log('Index already exists');
+ } else {
+ throw e;
+ }
+ }
+}
+
+// 2. Document speichern (JSON)
+async function saveDocument(
+ id: string,
+ title: string,
+ content: string,
+ category: string,
+ embedding: number[]
+) {
+ await redis.json.set(`doc:${id}`, '$', {
+ title,
+ content,
+ category,
+ embedding
+ });
+}
+
+// 3. Vector Search
+async function searchSimilar(
+ queryEmbedding: number[],
+ limit: number = 5
+) {
+ const results = await redis.ft.search('idx:docs', '*=>[KNN $K @embedding $BLOB AS score]', {
+ PARAMS: {
+ K: limit.toString(),
+ BLOB: Buffer.from(new Float32Array(queryEmbedding).buffer)
+ },
+ RETURN: ['title', 'content', 'score'],
+ SORTBY: {
+ BY: 'score',
+ DIRECTION: 'ASC' // Lower = more similar for COSINE
+ },
+ DIALECT: 2
+ });
+
+ return results.documents.map(doc => ({
+ id: doc.id,
+ title: doc.value.title,
+ content: doc.value.content,
+ score: 1 - parseFloat(doc.value.score as string) // Convert to similarity
+ }));
+}
+
+// 4. Hybrid Search (Vector + Filter)
+async function searchWithFilter(
+ queryEmbedding: number[],
+ category: string,
+ limit: number = 5
+) {
+ const results = await redis.ft.search(
+ 'idx:docs',
+ `(@category:{${category}})=>[KNN $K @embedding $BLOB AS score]`,
+ {
+ PARAMS: {
+ K: limit.toString(),
+ BLOB: Buffer.from(new Float32Array(queryEmbedding).buffer)
+ },
+ RETURN: ['title', 'content', 'category', 'score'],
+ DIALECT: 2
+ }
+ );
+
+ return results.documents;
+}
+```
+
+---
+
+## Vector Set (Redis 8 Beta)
+
+```typescript
+// Neuer Datentyp in Redis 8 - Inspiriert von Sorted Sets
+
+// Vector hinzufügen
+await redis.sendCommand([
+ 'VADD', 'products',
+ 'VECTOR', ...embedding.map(v => v.toString()),
+ 'product:123'
+]);
+
+// Ähnliche Vektoren finden
+const similar = await redis.sendCommand([
+ 'VSIM', 'products',
+ 'VECTOR', ...queryEmbedding.map(v => v.toString()),
+ 'COUNT', '10'
+]);
+
+// Vorteile von Vector Set:
+// - Einfachere API als RediSearch
+// - Optimiert für Similarity Search
+// - Von Salvatore Sanfilippo (Redis Creator) entwickelt
+```
+
+---
+
+## Session Cache mit Vector Search
+
+```typescript
+// Kombination: Session Management + Semantic Search
+
+interface UserSession {
+ userId: string;
+ lastQuery: string;
+ queryEmbedding: number[];
+ searchHistory: string[];
+ createdAt: number;
+ expiresAt: number;
+}
+
+// Session speichern
+async function saveSession(session: UserSession) {
+ const key = `session:${session.userId}`;
+
+ await redis.json.set(key, '$', session);
+ await redis.expireAt(key, session.expiresAt);
+}
+
+// Ähnliche Queries aus History finden
+async function findSimilarPastQueries(
+ userId: string,
+ currentQueryEmbedding: number[]
+) {
+ // Alle Sessions mit Query-History durchsuchen
+ const results = await redis.ft.search(
+ 'idx:sessions',
+ '*=>[KNN 5 @queryEmbedding $BLOB AS score]',
+ {
+ PARAMS: {
+ BLOB: Buffer.from(new Float32Array(currentQueryEmbedding).buffer)
+ },
+ RETURN: ['userId', 'lastQuery', 'score'],
+ DIALECT: 2
+ }
+ );
+
+ return results.documents;
+}
+```
+
+---
+
+## Full-Text + Vector Hybrid Search
+
+```typescript
+// Index mit Text + Vector
+await redis.ft.create('idx:articles', {
+ '$.title': {
+ type: SchemaFieldTypes.TEXT,
+ AS: 'title',
+ WEIGHT: 2.0
+ },
+ '$.body': {
+ type: SchemaFieldTypes.TEXT,
+ AS: 'body'
+ },
+ '$.tags': {
+ type: SchemaFieldTypes.TAG,
+ AS: 'tags'
+ },
+ '$.embedding': {
+ type: SchemaFieldTypes.VECTOR,
+ AS: 'embedding',
+ ALGORITHM: VectorAlgorithms.HNSW,
+ TYPE: 'FLOAT32',
+ DIM: 1536,
+ DISTANCE_METRIC: 'COSINE'
+ }
+}, {
+ ON: 'JSON',
+ PREFIX: 'article:'
+});
+
+// Hybrid Search: Text + Vector
+async function hybridSearch(
+ textQuery: string,
+ queryEmbedding: number[],
+ limit: number = 10
+) {
+ // Full-Text Search
+ const textResults = await redis.ft.search(
+ 'idx:articles',
+ `@title|body:(${textQuery})`,
+ {
+ RETURN: ['title', 'body'],
+ LIMIT: { from: 0, size: limit }
+ }
+ );
+
+ // Vector Search
+ const vectorResults = await redis.ft.search(
+ 'idx:articles',
+ '*=>[KNN $K @embedding $BLOB AS vector_score]',
+ {
+ PARAMS: {
+ K: limit.toString(),
+ BLOB: Buffer.from(new Float32Array(queryEmbedding).buffer)
+ },
+ RETURN: ['title', 'body', 'vector_score'],
+ DIALECT: 2
+ }
+ );
+
+ // Scores kombinieren (RRF - Reciprocal Rank Fusion)
+ const combined = new Map();
+ const k = 60; // RRF constant
+
+ textResults.documents.forEach((doc, rank) => {
+ const score = 1 / (k + rank + 1);
+ combined.set(doc.id, {
+ doc: doc.value,
+ score: score
+ });
+ });
+
+ vectorResults.documents.forEach((doc, rank) => {
+ const vectorScore = 1 / (k + rank + 1);
+ const existing = combined.get(doc.id);
+
+ if (existing) {
+ existing.score += vectorScore;
+ } else {
+ combined.set(doc.id, {
+ doc: doc.value,
+ score: vectorScore
+ });
+ }
+ });
+
+ return Array.from(combined.values())
+ .sort((a, b) => b.score - a.score)
+ .slice(0, limit);
+}
+```
+
+---
+
+## Caching für AI Embeddings
+
+```typescript
+// Embedding Cache - vermeidet wiederholte API-Calls
+
+async function getOrCreateEmbedding(
+ text: string,
+ generateFn: (text: string) => Promise
+): Promise {
+ // Hash als Cache Key
+ const hash = await crypto.subtle.digest(
+ 'SHA-256',
+ new TextEncoder().encode(text)
+ );
+ const cacheKey = `emb:${Buffer.from(hash).toString('hex').slice(0, 16)}`;
+
+ // Cache Check
+ const cached = await redis.get(cacheKey);
+ if (cached) {
+ return JSON.parse(cached);
+ }
+
+ // Generate & Cache
+ const embedding = await generateFn(text);
+
+ await redis.set(cacheKey, JSON.stringify(embedding), {
+ EX: 60 * 60 * 24 * 7 // 7 Tage TTL
+ });
+
+ return embedding;
+}
+
+// RAG Response Cache
+async function getCachedRAGResponse(
+ queryEmbedding: number[],
+ threshold: number = 0.95
+) {
+ // Suche nach sehr ähnlichen vorherigen Queries
+ const results = await redis.ft.search(
+ 'idx:rag_cache',
+ '*=>[KNN 1 @queryEmbedding $BLOB AS score]',
+ {
+ PARAMS: {
+ BLOB: Buffer.from(new Float32Array(queryEmbedding).buffer)
+ },
+ RETURN: ['query', 'response', 'score'],
+ DIALECT: 2
+ }
+ );
+
+ if (results.documents.length > 0) {
+ const similarity = 1 - parseFloat(results.documents[0].value.score as string);
+ if (similarity >= threshold) {
+ return results.documents[0].value.response;
+ }
+ }
+
+ return null;
+}
+```
+
+---
+
+## Pub/Sub für Real-Time Updates
+
+```typescript
+// Vector Search + Real-Time Updates
+
+// Publisher
+async function publishNewDocument(doc: Document) {
+ // In Redis speichern
+ await saveDocument(doc.id, doc.title, doc.content, doc.category, doc.embedding);
+
+ // Event publishen
+ await redis.publish('documents:new', JSON.stringify({
+ id: doc.id,
+ title: doc.title,
+ category: doc.category
+ }));
+}
+
+// Subscriber
+const subscriber = redis.duplicate();
+await subscriber.connect();
+
+await subscriber.subscribe('documents:new', (message) => {
+ const doc = JSON.parse(message);
+ console.log('New document:', doc.title);
+
+ // UI Update, Cache Invalidation, etc.
+});
+```
+
+---
+
+## Fazit
+
+Redis Stack bietet:
+
+1. **Sub-Millisecond Latency**: In-Memory für Echtzeit-AI
+2. **Unified Platform**: Vector, JSON, Full-Text in einem System
+3. **Vector Set**: Neuer Datentyp für einfache Similarity Search
+4. **Caching Layer**: Ideal für Embedding-Caches
+
+Perfekt als High-Performance Layer vor AI-Anwendungen.
+
+---
+
+## Bildprompts
+
+1. "In-memory database with vectors flowing at high speed, performance concept"
+2. "Redis logo with vector arrows and AI neural network, modern database"
+3. "Cache layer between AI model and application, latency optimization"
+
+---
+
+## Quellen
+
+- [Redis Vector Database](https://redis.io/solutions/vector-database/)
+- [Redis 8 GA Announcement](https://redis.io/blog/redis-8-ga/)
+- [RediSearch Documentation](https://redis.io/docs/latest/develop/ai/search-and-query/)
+- [Redis Vector Search Concepts](https://redis.io/docs/latest/develop/ai/search-and-query/vectors/)
diff --git a/blog-posts/44-drizzle-orm-lightweight.md b/blog-posts/44-drizzle-orm-lightweight.md
new file mode 100644
index 0000000..bca1686
--- /dev/null
+++ b/blog-posts/44-drizzle-orm-lightweight.md
@@ -0,0 +1,494 @@
+# Drizzle ORM: Die leichtgewichtige Prisma-Alternative
+
+**Meta-Description:** Drizzle ORM für TypeScript. SQL-nah, serverless-optimiert und nur 7KB. Vergleich mit Prisma und Migration Guide.
+
+**Keywords:** Drizzle ORM, TypeScript ORM, SQL Builder, Serverless Database, Prisma Alternative, Edge Computing, Lightweight ORM
+
+---
+
+## Einführung
+
+Drizzle ORM ist ein **TypeScript-first ORM** mit SQL-naher Syntax. Mit nur ~7KB (gzipped) und null Dependencies ist es perfekt für Serverless und Edge Computing – wo Prismas Bundle-Size zum Problem wird.
+
+---
+
+## Drizzle vs Prisma
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ DRIZZLE vs PRISMA │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ DRIZZLE PRISMA │
+│ ──────────────────── ──────────────────── │
+│ ~7KB gzipped ~600KB+ (mit Engine) │
+│ SQL-nah (Query Builder) Abstrahierte API │
+│ Code-First Schema Schema-First (.prisma) │
+│ Zero Dependencies Rust Binary Engine │
+│ Instant Cold Starts Cold Start Issues │
+│ │
+│ Best for: Best for: │
+│ • Serverless/Edge • Rapid Development │
+│ • SQL-Erfahrene Devs • Schema-First Teams │
+│ • Performance-Critical • Große Teams/Abstraction │
+│ • Kleine Bundles • Prisma Studio │
+│ │
+│ Supported DBs: Supported DBs: │
+│ PostgreSQL, MySQL, SQLite PostgreSQL, MySQL, │
+│ Turso, Neon, PlanetScale SQLite, MongoDB, │
+│ Cloudflare D1, Vercel SQL Server, CockroachDB │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Installation & Setup
+
+```bash
+# Installation
+npm install drizzle-orm
+npm install -D drizzle-kit
+
+# Für PostgreSQL
+npm install postgres
+
+# Für MySQL
+npm install mysql2
+
+# Für SQLite/Turso
+npm install @libsql/client
+```
+
+```typescript
+// drizzle.config.ts
+import { defineConfig } from 'drizzle-kit';
+
+export default defineConfig({
+ schema: './src/db/schema.ts',
+ out: './drizzle',
+ dialect: 'postgresql',
+ dbCredentials: {
+ url: process.env.DATABASE_URL!
+ }
+});
+```
+
+---
+
+## Schema Definition (Code-First)
+
+```typescript
+// src/db/schema.ts
+import {
+ pgTable,
+ uuid,
+ text,
+ timestamp,
+ boolean,
+ integer,
+ pgEnum
+} from 'drizzle-orm/pg-core';
+import { relations } from 'drizzle-orm';
+
+// Enum
+export const roleEnum = pgEnum('role', ['user', 'admin', 'moderator']);
+
+// Users Table
+export const users = pgTable('users', {
+ id: uuid('id').defaultRandom().primaryKey(),
+ email: text('email').notNull().unique(),
+ name: text('name'),
+ role: roleEnum('role').default('user').notNull(),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull()
+});
+
+// Posts Table
+export const posts = pgTable('posts', {
+ id: uuid('id').defaultRandom().primaryKey(),
+ title: text('title').notNull(),
+ content: text('content'),
+ published: boolean('published').default(false).notNull(),
+ authorId: uuid('author_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ updatedAt: timestamp('updated_at').defaultNow().notNull()
+});
+
+// Comments Table
+export const comments = pgTable('comments', {
+ id: uuid('id').defaultRandom().primaryKey(),
+ content: text('content').notNull(),
+ postId: uuid('post_id')
+ .notNull()
+ .references(() => posts.id, { onDelete: 'cascade' }),
+ authorId: uuid('author_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+ createdAt: timestamp('created_at').defaultNow().notNull()
+});
+
+// Relations
+export const usersRelations = relations(users, ({ many }) => ({
+ posts: many(posts),
+ comments: many(comments)
+}));
+
+export const postsRelations = relations(posts, ({ one, many }) => ({
+ author: one(users, {
+ fields: [posts.authorId],
+ references: [users.id]
+ }),
+ comments: many(comments)
+}));
+
+export const commentsRelations = relations(comments, ({ one }) => ({
+ post: one(posts, {
+ fields: [comments.postId],
+ references: [posts.id]
+ }),
+ author: one(users, {
+ fields: [comments.authorId],
+ references: [users.id]
+ })
+}));
+
+// Type Inference
+export type User = typeof users.$inferSelect;
+export type NewUser = typeof users.$inferInsert;
+export type Post = typeof posts.$inferSelect;
+export type NewPost = typeof posts.$inferInsert;
+```
+
+---
+
+## Database Connection
+
+```typescript
+// src/db/index.ts
+import { drizzle } from 'drizzle-orm/postgres-js';
+import postgres from 'postgres';
+import * as schema from './schema';
+
+const connectionString = process.env.DATABASE_URL!;
+
+// Für Queries
+const queryClient = postgres(connectionString);
+export const db = drizzle(queryClient, { schema });
+
+// Für Migrations (separater Client)
+const migrationClient = postgres(connectionString, { max: 1 });
+export const migrationDb = drizzle(migrationClient);
+```
+
+```typescript
+// Für Serverless (Neon, Vercel)
+import { drizzle } from 'drizzle-orm/neon-http';
+import { neon } from '@neondatabase/serverless';
+import * as schema from './schema';
+
+const sql = neon(process.env.DATABASE_URL!);
+export const db = drizzle(sql, { schema });
+```
+
+```typescript
+// Für Edge (Cloudflare D1)
+import { drizzle } from 'drizzle-orm/d1';
+import * as schema from './schema';
+
+export interface Env {
+ DB: D1Database;
+}
+
+export default {
+ async fetch(request: Request, env: Env) {
+ const db = drizzle(env.DB, { schema });
+ // ...
+ }
+};
+```
+
+---
+
+## CRUD Operations
+
+```typescript
+import { db } from '@/db';
+import { users, posts, comments } from '@/db/schema';
+import { eq, and, or, desc, asc, like, sql } from 'drizzle-orm';
+
+// CREATE
+async function createUser(data: NewUser) {
+ const [user] = await db
+ .insert(users)
+ .values(data)
+ .returning();
+ return user;
+}
+
+async function createManyUsers(data: NewUser[]) {
+ return await db
+ .insert(users)
+ .values(data)
+ .returning();
+}
+
+// READ - Single
+async function getUserById(id: string) {
+ const [user] = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, id))
+ .limit(1);
+ return user;
+}
+
+// READ - Mit Relations (Query API)
+async function getUserWithPosts(id: string) {
+ const result = await db.query.users.findFirst({
+ where: eq(users.id, id),
+ with: {
+ posts: {
+ where: eq(posts.published, true),
+ orderBy: [desc(posts.createdAt)],
+ limit: 10
+ }
+ }
+ });
+ return result;
+}
+
+// READ - Liste mit Filtering
+async function getPublishedPosts(page: number = 1, pageSize: number = 10) {
+ const offset = (page - 1) * pageSize;
+
+ const results = await db
+ .select({
+ id: posts.id,
+ title: posts.title,
+ content: posts.content,
+ createdAt: posts.createdAt,
+ authorName: users.name,
+ authorEmail: users.email
+ })
+ .from(posts)
+ .innerJoin(users, eq(posts.authorId, users.id))
+ .where(eq(posts.published, true))
+ .orderBy(desc(posts.createdAt))
+ .limit(pageSize)
+ .offset(offset);
+
+ return results;
+}
+
+// UPDATE
+async function updatePost(id: string, data: Partial) {
+ const [updated] = await db
+ .update(posts)
+ .set({
+ ...data,
+ updatedAt: new Date()
+ })
+ .where(eq(posts.id, id))
+ .returning();
+ return updated;
+}
+
+// UPSERT
+async function upsertUser(email: string, data: Partial) {
+ const [user] = await db
+ .insert(users)
+ .values({ email, ...data })
+ .onConflictDoUpdate({
+ target: users.email,
+ set: { ...data, updatedAt: new Date() }
+ })
+ .returning();
+ return user;
+}
+
+// DELETE
+async function deletePost(id: string) {
+ const [deleted] = await db
+ .delete(posts)
+ .where(eq(posts.id, id))
+ .returning();
+ return deleted;
+}
+```
+
+---
+
+## Advanced Queries
+
+```typescript
+import { sql, count, avg, sum } from 'drizzle-orm';
+
+// Aggregations
+async function getPostStats() {
+ const [stats] = await db
+ .select({
+ totalPosts: count(posts.id),
+ publishedPosts: count(sql`CASE WHEN ${posts.published} THEN 1 END`),
+ })
+ .from(posts);
+ return stats;
+}
+
+// Group By
+async function getPostsByAuthor() {
+ return await db
+ .select({
+ authorId: posts.authorId,
+ authorName: users.name,
+ postCount: count(posts.id)
+ })
+ .from(posts)
+ .innerJoin(users, eq(posts.authorId, users.id))
+ .groupBy(posts.authorId, users.name)
+ .orderBy(desc(count(posts.id)));
+}
+
+// Subqueries
+async function getUsersWithPostCount() {
+ const postCountSubquery = db
+ .select({
+ authorId: posts.authorId,
+ count: count(posts.id).as('post_count')
+ })
+ .from(posts)
+ .groupBy(posts.authorId)
+ .as('post_counts');
+
+ return await db
+ .select({
+ id: users.id,
+ name: users.name,
+ postCount: postCountSubquery.count
+ })
+ .from(users)
+ .leftJoin(postCountSubquery, eq(users.id, postCountSubquery.authorId));
+}
+
+// Raw SQL
+async function searchPosts(query: string) {
+ return await db.execute(sql`
+ SELECT * FROM posts
+ WHERE to_tsvector('german', title || ' ' || content)
+ @@ plainto_tsquery('german', ${query})
+ `);
+}
+
+// Transactions
+async function createPostWithComments(
+ post: NewPost,
+ comments: { content: string; authorId: string }[]
+) {
+ return await db.transaction(async (tx) => {
+ const [newPost] = await tx
+ .insert(posts)
+ .values(post)
+ .returning();
+
+ if (comments.length > 0) {
+ await tx.insert(comments).values(
+ comments.map(c => ({
+ ...c,
+ postId: newPost.id
+ }))
+ );
+ }
+
+ return newPost;
+ });
+}
+```
+
+---
+
+## Migrations
+
+```bash
+# Schema-Änderungen generieren
+npx drizzle-kit generate
+
+# Migrations ausführen
+npx drizzle-kit migrate
+
+# Push (Development - ohne Migration Files)
+npx drizzle-kit push
+
+# Studio (GUI)
+npx drizzle-kit studio
+```
+
+```typescript
+// Programmatische Migration
+import { migrate } from 'drizzle-orm/postgres-js/migrator';
+import { migrationDb } from './db';
+
+async function runMigrations() {
+ await migrate(migrationDb, { migrationsFolder: './drizzle' });
+ console.log('Migrations complete');
+}
+```
+
+---
+
+## Zod Integration
+
+```typescript
+import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
+import { z } from 'zod';
+import { users, posts } from './schema';
+
+// Auto-generierte Schemas
+const insertUserSchema = createInsertSchema(users);
+const selectUserSchema = createSelectSchema(users);
+
+// Mit Erweiterungen
+const createUserSchema = createInsertSchema(users, {
+ email: z.string().email('Ungültige E-Mail'),
+ name: z.string().min(2, 'Name zu kurz').optional()
+}).omit({ id: true, createdAt: true, updatedAt: true });
+
+const updateUserSchema = createUserSchema.partial();
+
+// Verwendung
+async function createUserValidated(input: unknown) {
+ const data = createUserSchema.parse(input);
+ return await createUser(data);
+}
+```
+
+---
+
+## Fazit
+
+Drizzle ORM bietet:
+
+1. **Minimal Footprint**: ~7KB, zero dependencies
+2. **SQL-Kontrolle**: Transparente, vorhersagbare Queries
+3. **Type-Safety**: Volle TypeScript-Integration
+4. **Edge-Ready**: Perfekt für Serverless/Edge
+
+Für Performance-kritische und serverless Anwendungen ist Drizzle die bessere Wahl.
+
+---
+
+## Bildprompts
+
+1. "Lightweight feather next to database icons, minimal bundle size concept"
+2. "SQL code transforming into TypeScript types, type-safe ORM visualization"
+3. "Edge computing nodes with database connections, serverless architecture"
+
+---
+
+## Quellen
+
+- [Drizzle ORM Documentation](https://orm.drizzle.team/)
+- [Drizzle vs Prisma Comparison](https://www.prisma.io/docs/orm/more/comparisons/prisma-and-drizzle)
+- [Drizzle vs Prisma 2026](https://medium.com/@codabu/drizzle-vs-prisma-choosing-the-right-typescript-orm-in-2026-deep-dive-63abb6aa882b)
+- [Drizzle Kit](https://orm.drizzle.team/kit-docs/overview)
diff --git a/blog-posts/45-turso-libsql-edge.md b/blog-posts/45-turso-libsql-edge.md
new file mode 100644
index 0000000..9217727
--- /dev/null
+++ b/blog-posts/45-turso-libsql-edge.md
@@ -0,0 +1,450 @@
+# Turso & libSQL: SQLite für die Edge
+
+**Meta-Description:** Turso als Edge-Hosted SQLite mit libSQL. Embedded Replicas, Vector Search und Local-First Development.
+
+**Keywords:** Turso, libSQL, SQLite Edge, Embedded Replicas, Local-First, Edge Database, Distributed SQLite
+
+---
+
+## Einführung
+
+Turso bringt **SQLite in die Edge**. Basierend auf libSQL (einem SQLite-Fork) bietet es embedded Replicas, globale Verteilung und native Vector Search – perfekt für Low-Latency Anwendungen weltweit.
+
+---
+
+## Turso Architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ TURSO ARCHITECTURE │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Primary Database (Write): │
+│ └── Zentraler Write-Node │
+│ │
+│ Edge Replicas (Read): │
+│ ├── Frankfurt │
+│ ├── New York │
+│ ├── Singapore │
+│ └── São Paulo │
+│ └── Automatische Synchronisation │
+│ │
+│ Embedded Replicas (On-Device): │
+│ ├── In-App SQLite Kopie │
+│ ├── Offline-fähig │
+│ └── Sync bei Reconnect │
+│ │
+│ Features: │
+│ ├── libSQL (SQLite Fork) │
+│ ├── Native Vector Search │
+│ ├── Branching (wie Git) │
+│ └── MCP Server für AI Assistants │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Setup
+
+```bash
+# Turso CLI installieren
+curl -sSfL https://get.tur.so/install.sh | bash
+
+# Login
+turso auth login
+
+# Database erstellen
+turso db create my-app --location fra # Frankfurt
+
+# Replicas hinzufügen
+turso db replicas add my-app --location iad # US East
+turso db replicas add my-app --location sin # Singapore
+
+# Connection URL und Token
+turso db show my-app --url
+turso db tokens create my-app
+```
+
+```bash
+# Node.js Client installieren
+npm install @libsql/client
+```
+
+---
+
+## Basic Connection
+
+```typescript
+// lib/turso.ts
+import { createClient } from '@libsql/client';
+
+export const turso = createClient({
+ url: process.env.TURSO_DATABASE_URL!,
+ authToken: process.env.TURSO_AUTH_TOKEN!
+});
+
+// Für lokale Entwicklung (SQLite File)
+export const localDb = createClient({
+ url: 'file:local.db'
+});
+```
+
+---
+
+## CRUD Operations
+
+```typescript
+import { turso } from '@/lib/turso';
+
+// CREATE TABLE
+async function initializeSchema() {
+ await turso.execute(`
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ email TEXT UNIQUE NOT NULL,
+ name TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
+ await turso.execute(`
+ CREATE TABLE IF NOT EXISTS posts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ content TEXT,
+ author_id INTEGER NOT NULL,
+ published INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (author_id) REFERENCES users(id)
+ )
+ `);
+}
+
+// CREATE
+async function createUser(email: string, name: string) {
+ const result = await turso.execute({
+ sql: 'INSERT INTO users (email, name) VALUES (?, ?) RETURNING *',
+ args: [email, name]
+ });
+ return result.rows[0];
+}
+
+// READ
+async function getUserById(id: number) {
+ const result = await turso.execute({
+ sql: 'SELECT * FROM users WHERE id = ?',
+ args: [id]
+ });
+ return result.rows[0];
+}
+
+async function getPostsWithAuthors() {
+ const result = await turso.execute(`
+ SELECT
+ p.id,
+ p.title,
+ p.content,
+ p.created_at,
+ u.name as author_name,
+ u.email as author_email
+ FROM posts p
+ JOIN users u ON p.author_id = u.id
+ WHERE p.published = 1
+ ORDER BY p.created_at DESC
+ `);
+ return result.rows;
+}
+
+// UPDATE
+async function updatePost(id: number, title: string, content: string) {
+ const result = await turso.execute({
+ sql: 'UPDATE posts SET title = ?, content = ? WHERE id = ? RETURNING *',
+ args: [title, content, id]
+ });
+ return result.rows[0];
+}
+
+// DELETE
+async function deletePost(id: number) {
+ await turso.execute({
+ sql: 'DELETE FROM posts WHERE id = ?',
+ args: [id]
+ });
+}
+
+// BATCH (Transaction)
+async function createUserWithPosts(
+ user: { email: string; name: string },
+ posts: { title: string; content: string }[]
+) {
+ const result = await turso.batch([
+ {
+ sql: 'INSERT INTO users (email, name) VALUES (?, ?) RETURNING id',
+ args: [user.email, user.name]
+ },
+ ...posts.map(post => ({
+ sql: 'INSERT INTO posts (title, content, author_id) VALUES (?, ?, last_insert_rowid())',
+ args: [post.title, post.content]
+ }))
+ ], 'write');
+
+ return result;
+}
+```
+
+---
+
+## Embedded Replicas (Local-First)
+
+```typescript
+// lib/turso-embedded.ts
+import { createClient } from '@libsql/client';
+
+// Embedded Replica mit Sync
+export const db = createClient({
+ url: 'file:local-replica.db', // Lokale SQLite Datei
+ syncUrl: process.env.TURSO_DATABASE_URL!,
+ authToken: process.env.TURSO_AUTH_TOKEN!,
+ syncInterval: 60 // Sync alle 60 Sekunden
+});
+
+// Manuelle Synchronisation
+async function syncDatabase() {
+ await db.sync();
+ console.log('Database synced with remote');
+}
+
+// Verwendung
+async function getDataWithFallback() {
+ try {
+ // Versuche lokale Query (schnell!)
+ const result = await db.execute('SELECT * FROM posts LIMIT 10');
+ return result.rows;
+ } catch (error) {
+ // Fallback zu Remote bei Fehler
+ await db.sync();
+ const result = await db.execute('SELECT * FROM posts LIMIT 10');
+ return result.rows;
+ }
+}
+```
+
+---
+
+## Vector Search
+
+```typescript
+// libSQL unterstützt native Vector Operations
+
+// Tabelle mit Vector Column
+await turso.execute(`
+ CREATE TABLE IF NOT EXISTS documents (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ content TEXT NOT NULL,
+ embedding F32_BLOB(1536), -- OpenAI ada-002 Dimension
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+`);
+
+// Vector Index erstellen
+await turso.execute(`
+ CREATE INDEX IF NOT EXISTS documents_embedding_idx
+ ON documents (libsql_vector_idx(embedding))
+`);
+
+// Document mit Embedding speichern
+async function saveDocument(content: string, embedding: number[]) {
+ await turso.execute({
+ sql: `
+ INSERT INTO documents (content, embedding)
+ VALUES (?, vector32(?))
+ `,
+ args: [content, JSON.stringify(embedding)]
+ });
+}
+
+// Similarity Search
+async function searchSimilar(queryEmbedding: number[], limit: number = 5) {
+ const result = await turso.execute({
+ sql: `
+ SELECT
+ id,
+ content,
+ vector_distance_cos(embedding, vector32(?)) as distance
+ FROM documents
+ ORDER BY distance ASC
+ LIMIT ?
+ `,
+ args: [JSON.stringify(queryEmbedding), limit]
+ });
+
+ return result.rows.map(row => ({
+ id: row.id,
+ content: row.content,
+ similarity: 1 - (row.distance as number) // Convert distance to similarity
+ }));
+}
+```
+
+---
+
+## Drizzle ORM Integration
+
+```typescript
+// drizzle.config.ts
+import { defineConfig } from 'drizzle-kit';
+
+export default defineConfig({
+ schema: './src/db/schema.ts',
+ out: './drizzle',
+ dialect: 'turso',
+ dbCredentials: {
+ url: process.env.TURSO_DATABASE_URL!,
+ authToken: process.env.TURSO_AUTH_TOKEN!
+ }
+});
+```
+
+```typescript
+// src/db/schema.ts
+import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
+
+export const users = sqliteTable('users', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ email: text('email').notNull().unique(),
+ name: text('name'),
+ createdAt: integer('created_at', { mode: 'timestamp' })
+ .notNull()
+ .$defaultFn(() => new Date())
+});
+
+export const posts = sqliteTable('posts', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+ title: text('title').notNull(),
+ content: text('content'),
+ authorId: integer('author_id')
+ .notNull()
+ .references(() => users.id),
+ published: integer('published', { mode: 'boolean' }).default(false),
+ createdAt: integer('created_at', { mode: 'timestamp' })
+ .notNull()
+ .$defaultFn(() => new Date())
+});
+```
+
+```typescript
+// src/db/index.ts
+import { drizzle } from 'drizzle-orm/libsql';
+import { createClient } from '@libsql/client';
+import * as schema from './schema';
+
+const client = createClient({
+ url: process.env.TURSO_DATABASE_URL!,
+ authToken: process.env.TURSO_AUTH_TOKEN!
+});
+
+export const db = drizzle(client, { schema });
+
+// Queries
+import { eq } from 'drizzle-orm';
+
+async function getUserWithPosts(userId: number) {
+ return await db.query.users.findFirst({
+ where: eq(users.id, userId),
+ with: {
+ posts: true
+ }
+ });
+}
+```
+
+---
+
+## Database Branching
+
+```bash
+# Branch erstellen (wie Git)
+turso db branch create my-app feature-branch
+
+# Branch verwenden
+turso db show my-app/feature-branch --url
+
+# Branch mergen (manuell - Schema migrieren)
+turso db branch delete my-app feature-branch
+```
+
+```typescript
+// Branch in Code verwenden
+const branchDb = createClient({
+ url: process.env.TURSO_BRANCH_URL!, // Feature Branch URL
+ authToken: process.env.TURSO_AUTH_TOKEN!
+});
+
+// Schema-Änderungen testen
+await branchDb.execute(`
+ ALTER TABLE users ADD COLUMN avatar_url TEXT
+`);
+
+// Nach Test: Änderungen auf Production anwenden
+```
+
+---
+
+## Edge Functions Integration
+
+```typescript
+// Cloudflare Workers
+export default {
+ async fetch(request: Request, env: Env) {
+ const db = createClient({
+ url: env.TURSO_DATABASE_URL,
+ authToken: env.TURSO_AUTH_TOKEN
+ });
+
+ const { pathname } = new URL(request.url);
+
+ if (pathname === '/api/posts') {
+ const result = await db.execute(
+ 'SELECT * FROM posts WHERE published = 1 ORDER BY created_at DESC LIMIT 10'
+ );
+
+ return new Response(JSON.stringify(result.rows), {
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ return new Response('Not Found', { status: 404 });
+ }
+};
+```
+
+---
+
+## Fazit
+
+Turso bietet:
+
+1. **Global Edge Distribution**: Replicas weltweit für Low-Latency
+2. **Embedded Replicas**: Local-First mit automatischem Sync
+3. **SQLite Compatibility**: Bewährte Technologie, moderne Distribution
+4. **Native Vector Search**: AI-ready ohne externe Services
+
+Ideal für globale Anwendungen mit Offline-Support.
+
+---
+
+## Bildprompts
+
+1. "Globe with database nodes connected at edge locations, global distribution"
+2. "SQLite file syncing between device and cloud, embedded replica concept"
+3. "Local-first application working offline then syncing, connectivity visualization"
+
+---
+
+## Quellen
+
+- [Turso Documentation](https://docs.turso.tech/)
+- [libSQL GitHub](https://github.com/tursodatabase/libsql)
+- [Turso Embedded Replicas](https://turso.tech/blog/local-first-cloud-connected-sqlite-with-turso-embedded-replicas)
+- [Turso Vector Search](https://docs.turso.tech/features/vector-search)
diff --git a/blog-posts/46-planetscale-mysql-serverless.md b/blog-posts/46-planetscale-mysql-serverless.md
new file mode 100644
index 0000000..6666bf6
--- /dev/null
+++ b/blog-posts/46-planetscale-mysql-serverless.md
@@ -0,0 +1,433 @@
+# PlanetScale: Serverless MySQL mit Vitess
+
+**Meta-Description:** PlanetScale für skalierbares MySQL. Database Branching, Non-Blocking Schema Changes und Vitess-powered Performance.
+
+**Keywords:** PlanetScale, MySQL, Vitess, Serverless Database, Database Branching, Schema Migrations, Horizontal Scaling
+
+---
+
+## Einführung
+
+PlanetScale bringt **YouTube-Scale** zu MySQL. Powered by Vitess (entwickelt für YouTube) bietet es Horizontal Sharding, Non-Blocking Schema Changes und Database Branching – ohne die Komplexität selbst zu managen.
+
+---
+
+## PlanetScale Features
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ PLANETSCALE │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Vitess Foundation: │
+│ ├── Horizontal Sharding (Auto) │
+│ ├── Connection Pooling │
+│ ├── Query Routing │
+│ └── Petabyte-Scale Proven │
+│ │
+│ Developer Experience: │
+│ ├── Database Branching (wie Git) │
+│ ├── Non-Blocking Schema Changes │
+│ ├── Schema Revert (1-Click Rollback) │
+│ └── PlanetScale Insights (Query Analytics) │
+│ │
+│ Serverless Benefits: │
+│ ├── No Cold Starts │
+│ ├── Auto-Scaling │
+│ ├── Per-Query Pricing │
+│ └── Edge-Compatible Driver │
+│ │
+│ Security: │
+│ ├── No Foreign Keys (by Design - Performance) │
+│ ├── Branch Permissions │
+│ └── Audit Logs │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Setup
+
+```bash
+# PlanetScale CLI installieren
+brew install planetscale/tap/pscale
+
+# Login
+pscale auth login
+
+# Database erstellen
+pscale database create my-app --region eu-west
+
+# Branch erstellen
+pscale branch create my-app main
+
+# Local Proxy (Development)
+pscale connect my-app main --port 3309
+```
+
+```bash
+# Dependencies
+npm install @planetscale/database
+```
+
+---
+
+## Database Connection
+
+```typescript
+// lib/planetscale.ts
+import { connect } from '@planetscale/database';
+
+const config = {
+ host: process.env.DATABASE_HOST,
+ username: process.env.DATABASE_USERNAME,
+ password: process.env.DATABASE_PASSWORD
+};
+
+export const conn = connect(config);
+
+// Für Edge (Fetch-basiert)
+export async function query(
+ sql: string,
+ args?: unknown[]
+): Promise {
+ const results = await conn.execute(sql, args);
+ return results.rows as T[];
+}
+```
+
+```typescript
+// Mit Drizzle ORM
+import { drizzle } from 'drizzle-orm/planetscale-serverless';
+import { connect } from '@planetscale/database';
+import * as schema from './schema';
+
+const connection = connect({
+ host: process.env.DATABASE_HOST,
+ username: process.env.DATABASE_USERNAME,
+ password: process.env.DATABASE_PASSWORD
+});
+
+export const db = drizzle(connection, { schema });
+```
+
+---
+
+## Schema Definition (Drizzle)
+
+```typescript
+// src/db/schema.ts
+import {
+ mysqlTable,
+ varchar,
+ text,
+ boolean,
+ timestamp,
+ int,
+ mysqlEnum
+} from 'drizzle-orm/mysql-core';
+
+export const users = mysqlTable('users', {
+ id: varchar('id', { length: 36 }).primaryKey(),
+ email: varchar('email', { length: 255 }).notNull().unique(),
+ name: varchar('name', { length: 255 }),
+ role: mysqlEnum('role', ['user', 'admin']).default('user'),
+ createdAt: timestamp('created_at').defaultNow(),
+ updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
+});
+
+export const posts = mysqlTable('posts', {
+ id: varchar('id', { length: 36 }).primaryKey(),
+ title: varchar('title', { length: 255 }).notNull(),
+ content: text('content'),
+ slug: varchar('slug', { length: 255 }).notNull().unique(),
+ published: boolean('published').default(false),
+ authorId: varchar('author_id', { length: 36 }).notNull(),
+ // Kein FOREIGN KEY - PlanetScale Design Decision
+ createdAt: timestamp('created_at').defaultNow(),
+ updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
+});
+
+// Index für Performance
+export const postsAuthorIdx = mysqlTable('posts', {
+ authorId: varchar('author_id', { length: 36 })
+}).indexes((t) => ({
+ authorIdx: index('author_idx').on(t.authorId)
+}));
+```
+
+---
+
+## CRUD Operations
+
+```typescript
+import { db } from '@/db';
+import { users, posts } from '@/db/schema';
+import { eq, and, desc, like, sql } from 'drizzle-orm';
+import { nanoid } from 'nanoid';
+
+// CREATE
+async function createUser(email: string, name: string) {
+ const id = nanoid();
+
+ await db.insert(users).values({
+ id,
+ email,
+ name
+ });
+
+ return { id, email, name };
+}
+
+// READ
+async function getUserById(id: string) {
+ const [user] = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, id));
+ return user;
+}
+
+// Manueller JOIN (ohne Foreign Keys)
+async function getPostsWithAuthors() {
+ return await db
+ .select({
+ post: posts,
+ author: {
+ id: users.id,
+ name: users.name,
+ email: users.email
+ }
+ })
+ .from(posts)
+ .innerJoin(users, eq(posts.authorId, users.id))
+ .where(eq(posts.published, true))
+ .orderBy(desc(posts.createdAt));
+}
+
+// UPDATE
+async function updatePost(id: string, data: Partial) {
+ await db
+ .update(posts)
+ .set(data)
+ .where(eq(posts.id, id));
+}
+
+// DELETE
+async function deletePost(id: string) {
+ await db.delete(posts).where(eq(posts.id, id));
+}
+
+// Pagination
+async function getPostsPaginated(page: number, pageSize: number = 20) {
+ const offset = (page - 1) * pageSize;
+
+ const [results, countResult] = await Promise.all([
+ db
+ .select()
+ .from(posts)
+ .where(eq(posts.published, true))
+ .orderBy(desc(posts.createdAt))
+ .limit(pageSize)
+ .offset(offset),
+
+ db
+ .select({ count: sql`count(*)` })
+ .from(posts)
+ .where(eq(posts.published, true))
+ ]);
+
+ return {
+ posts: results,
+ total: countResult[0].count,
+ page,
+ pageSize,
+ totalPages: Math.ceil(countResult[0].count / pageSize)
+ };
+}
+```
+
+---
+
+## Database Branching Workflow
+
+```bash
+# 1. Feature Branch erstellen
+pscale branch create my-app add-comments
+
+# 2. Lokal verbinden
+pscale connect my-app add-comments --port 3310
+
+# 3. Schema ändern (auf Branch)
+# In Code oder SQL:
+```
+
+```typescript
+// Schema-Migration auf Branch
+await db.execute(sql`
+ CREATE TABLE comments (
+ id VARCHAR(36) PRIMARY KEY,
+ content TEXT NOT NULL,
+ post_id VARCHAR(36) NOT NULL,
+ author_id VARCHAR(36) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ INDEX post_idx (post_id),
+ INDEX author_idx (author_id)
+ )
+`);
+```
+
+```bash
+# 4. Deploy Request erstellen
+pscale deploy-request create my-app add-comments
+
+# 5. Review in Dashboard
+# Schema Diff wird angezeigt
+
+# 6. Deploy (Non-Blocking!)
+pscale deploy-request deploy my-app 1
+
+# 7. Branch löschen
+pscale branch delete my-app add-comments
+```
+
+---
+
+## Non-Blocking Schema Changes
+
+```sql
+-- Diese Änderungen blockieren NICHT die Tabelle:
+
+-- Column hinzufügen
+ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255);
+
+-- Index hinzufügen (online)
+ALTER TABLE posts ADD INDEX slug_idx (slug);
+
+-- Column umbenennen (via Ghost Tables)
+-- PlanetScale kopiert Daten im Hintergrund
+
+-- Achtung: Foreign Keys nicht unterstützt!
+-- Stattdessen: Application-Level Referential Integrity
+```
+
+---
+
+## PlanetScale Insights
+
+```typescript
+// Query Performance analysieren
+// In PlanetScale Dashboard: Insights Tab
+
+// Slow Queries finden
+// - Queries > 100ms
+// - Table Scans
+// - Missing Indexes
+
+// Beispiel: Index für häufige Query
+await db.execute(sql`
+ CREATE INDEX posts_published_created_idx
+ ON posts (published, created_at DESC)
+`);
+
+// Composite Index für Filter + Sort
+await db.execute(sql`
+ CREATE INDEX posts_author_published_idx
+ ON posts (author_id, published, created_at DESC)
+`);
+```
+
+---
+
+## Edge-Compatible Driver
+
+```typescript
+// Cloudflare Workers / Vercel Edge
+import { connect } from '@planetscale/database';
+
+export const runtime = 'edge';
+
+export async function GET() {
+ const conn = connect({
+ host: process.env.DATABASE_HOST,
+ username: process.env.DATABASE_USERNAME,
+ password: process.env.DATABASE_PASSWORD
+ });
+
+ const results = await conn.execute(
+ 'SELECT * FROM posts WHERE published = ? ORDER BY created_at DESC LIMIT 10',
+ [true]
+ );
+
+ return Response.json(results.rows);
+}
+```
+
+---
+
+## Referential Integrity ohne Foreign Keys
+
+```typescript
+// Application-Level Constraints
+
+// Bei Insert: Prüfen ob Author existiert
+async function createPost(data: NewPost) {
+ const [author] = await db
+ .select({ id: users.id })
+ .from(users)
+ .where(eq(users.id, data.authorId));
+
+ if (!author) {
+ throw new Error('Author not found');
+ }
+
+ return await db.insert(posts).values(data);
+}
+
+// Bei Delete: Cascade manuell
+async function deleteUser(userId: string) {
+ // Erst abhängige Daten löschen
+ await db.delete(posts).where(eq(posts.authorId, userId));
+ await db.delete(comments).where(eq(comments.authorId, userId));
+
+ // Dann User
+ await db.delete(users).where(eq(users.id, userId));
+}
+
+// Oder: Soft Delete Pattern
+const users = mysqlTable('users', {
+ // ...
+ deletedAt: timestamp('deleted_at')
+});
+```
+
+---
+
+## Fazit
+
+PlanetScale bietet:
+
+1. **Vitess-Powered**: YouTube-Scale für MySQL
+2. **Database Branching**: Git-Workflow für Datenbanken
+3. **Zero-Downtime Migrations**: Non-Blocking Schema Changes
+4. **Edge-Ready**: Fetch-basierter Driver für Serverless
+
+Enterprise MySQL-Performance mit Developer-Friendly Workflow.
+
+---
+
+## Bildprompts
+
+1. "Database branches merging like Git, version control for databases"
+2. "MySQL scaling horizontally across servers, Vitess sharding concept"
+3. "Schema migration without downtime, zero-interruption deployment"
+
+---
+
+## Quellen
+
+- [PlanetScale Documentation](https://planetscale.com/docs)
+- [PlanetScale Branching](https://planetscale.com/docs/vitess/schema-changes/branching)
+- [Vitess Overview](https://vitess.io/)
+- [PlanetScale Database Driver](https://github.com/planetscale/database-js)
diff --git a/blog-posts/47-hono-edge-framework.md b/blog-posts/47-hono-edge-framework.md
new file mode 100644
index 0000000..24436c8
--- /dev/null
+++ b/blog-posts/47-hono-edge-framework.md
@@ -0,0 +1,485 @@
+# Hono: Das ultraschnelle Edge-Framework
+
+**Meta-Description:** Hono Framework für Edge Computing. Web Standards, Multi-Runtime Support und nur 12KB. Von Cloudflare Workers bis Bun.
+
+**Keywords:** Hono, Edge Framework, Cloudflare Workers, Web Standards, Bun, Deno, Serverless Framework, TypeScript API
+
+---
+
+## Einführung
+
+Hono (炎 = Flamme) ist ein **ultraschnelles Web-Framework** gebaut auf Web Standards. Mit unter 12KB läuft es auf jedem JavaScript-Runtime: Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda und mehr.
+
+---
+
+## Hono Overview
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ HONO FRAMEWORK │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Core Features: │
+│ ├── Web Standards API (Request/Response) │
+│ ├── Zero Dependencies │
+│ ├── TypeScript First │
+│ └── < 12KB (hono/tiny) │
+│ │
+│ Supported Runtimes: │
+│ ├── Cloudflare Workers / Pages │
+│ ├── Deno / Deno Deploy │
+│ ├── Bun │
+│ ├── Node.js │
+│ ├── AWS Lambda │
+│ ├── Vercel Edge │
+│ ├── Fastly Compute │
+│ └── Netlify Edge │
+│ │
+│ Built-in Middleware: │
+│ ├── CORS, CSRF, ETag │
+│ ├── JWT, Basic Auth, Bearer Auth │
+│ ├── Logger, Pretty JSON │
+│ ├── Compress, Cache │
+│ └── Streaming, SSE │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Quick Start
+
+```bash
+# Neues Projekt
+npm create hono@latest my-app
+
+# Runtime wählen:
+# - cloudflare-workers
+# - cloudflare-pages
+# - deno
+# - bun
+# - nodejs
+# - vercel
+
+cd my-app
+npm install
+npm run dev
+```
+
+---
+
+## Basic Routing
+
+```typescript
+// src/index.ts
+import { Hono } from 'hono';
+
+const app = new Hono();
+
+// Basic Routes
+app.get('/', (c) => c.text('Hello Hono!'));
+
+app.get('/json', (c) => {
+ return c.json({ message: 'Hello', timestamp: Date.now() });
+});
+
+// Path Parameters
+app.get('/users/:id', (c) => {
+ const id = c.req.param('id');
+ return c.json({ userId: id });
+});
+
+// Query Parameters
+app.get('/search', (c) => {
+ const query = c.req.query('q');
+ const page = c.req.query('page') || '1';
+ return c.json({ query, page: parseInt(page) });
+});
+
+// POST mit Body
+app.post('/users', async (c) => {
+ const body = await c.req.json();
+ return c.json({ created: body }, 201);
+});
+
+// Wildcards
+app.get('/files/*', (c) => {
+ const path = c.req.path;
+ return c.text(`File path: ${path}`);
+});
+
+export default app;
+```
+
+---
+
+## Typed Routes mit RPC
+
+```typescript
+// src/index.ts
+import { Hono } from 'hono';
+import { zValidator } from '@hono/zod-validator';
+import { z } from 'zod';
+
+const app = new Hono();
+
+// Schema Definition
+const createUserSchema = z.object({
+ name: z.string().min(2),
+ email: z.string().email()
+});
+
+const userParamsSchema = z.object({
+ id: z.string().uuid()
+});
+
+// Typed Route
+const route = app
+ .get('/users', (c) => {
+ return c.json([
+ { id: '1', name: 'Alice' },
+ { id: '2', name: 'Bob' }
+ ]);
+ })
+ .post(
+ '/users',
+ zValidator('json', createUserSchema),
+ (c) => {
+ const data = c.req.valid('json');
+ // data ist typisiert: { name: string; email: string }
+ return c.json({ id: crypto.randomUUID(), ...data }, 201);
+ }
+ )
+ .get(
+ '/users/:id',
+ zValidator('param', userParamsSchema),
+ (c) => {
+ const { id } = c.req.valid('param');
+ return c.json({ id, name: 'Alice' });
+ }
+ );
+
+// Type Export für Client
+export type AppType = typeof route;
+
+export default app;
+```
+
+```typescript
+// Client-Side (hc = Hono Client)
+import { hc } from 'hono/client';
+import type { AppType } from './server';
+
+const client = hc('http://localhost:8787');
+
+// Vollständig typisiert!
+const users = await client.users.$get();
+const newUser = await client.users.$post({
+ json: { name: 'Charlie', email: 'charlie@example.com' }
+});
+```
+
+---
+
+## Middleware
+
+```typescript
+import { Hono } from 'hono';
+import { cors } from 'hono/cors';
+import { jwt } from 'hono/jwt';
+import { logger } from 'hono/logger';
+import { prettyJSON } from 'hono/pretty-json';
+import { secureHeaders } from 'hono/secure-headers';
+import { compress } from 'hono/compress';
+
+const app = new Hono();
+
+// Global Middleware
+app.use('*', logger());
+app.use('*', secureHeaders());
+app.use('*', compress());
+
+// CORS für API Routes
+app.use('/api/*', cors({
+ origin: ['https://example.com', 'https://app.example.com'],
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
+ allowHeaders: ['Content-Type', 'Authorization'],
+ credentials: true
+}));
+
+// JWT für geschützte Routes
+app.use('/api/protected/*', jwt({
+ secret: process.env.JWT_SECRET!
+}));
+
+// Pretty JSON in Development
+if (process.env.NODE_ENV === 'development') {
+ app.use('*', prettyJSON());
+}
+
+// Custom Middleware
+const timing = () => {
+ return async (c, next) => {
+ const start = Date.now();
+ await next();
+ const duration = Date.now() - start;
+ c.header('X-Response-Time', `${duration}ms`);
+ };
+};
+
+app.use('*', timing());
+
+// Protected Route
+app.get('/api/protected/me', (c) => {
+ const payload = c.get('jwtPayload');
+ return c.json({ user: payload });
+});
+```
+
+---
+
+## Error Handling
+
+```typescript
+import { Hono } from 'hono';
+import { HTTPException } from 'hono/http-exception';
+
+const app = new Hono();
+
+// Custom Error Handler
+app.onError((err, c) => {
+ if (err instanceof HTTPException) {
+ return c.json(
+ { error: err.message, status: err.status },
+ err.status
+ );
+ }
+
+ console.error(err);
+ return c.json(
+ { error: 'Internal Server Error', status: 500 },
+ 500
+ );
+});
+
+// Not Found Handler
+app.notFound((c) => {
+ return c.json({ error: 'Not Found', status: 404 }, 404);
+});
+
+// Throwing Errors
+app.get('/users/:id', async (c) => {
+ const user = await getUser(c.req.param('id'));
+
+ if (!user) {
+ throw new HTTPException(404, { message: 'User not found' });
+ }
+
+ return c.json(user);
+});
+
+// Mit Custom Response
+app.get('/admin', (c) => {
+ const isAdmin = checkAdmin(c);
+
+ if (!isAdmin) {
+ throw new HTTPException(403, {
+ message: 'Forbidden',
+ res: c.json({ error: 'Admin access required' }, 403)
+ });
+ }
+
+ return c.json({ admin: true });
+});
+```
+
+---
+
+## Cloudflare Workers Integration
+
+```typescript
+// src/index.ts
+import { Hono } from 'hono';
+
+type Bindings = {
+ DB: D1Database;
+ KV: KVNamespace;
+ AI: Ai;
+ BUCKET: R2Bucket;
+};
+
+const app = new Hono<{ Bindings: Bindings }>();
+
+// D1 Database
+app.get('/users', async (c) => {
+ const { results } = await c.env.DB
+ .prepare('SELECT * FROM users LIMIT 10')
+ .all();
+ return c.json(results);
+});
+
+// KV Storage
+app.get('/cache/:key', async (c) => {
+ const key = c.req.param('key');
+ const value = await c.env.KV.get(key);
+
+ if (!value) {
+ return c.json({ error: 'Not found' }, 404);
+ }
+
+ return c.json({ key, value: JSON.parse(value) });
+});
+
+app.put('/cache/:key', async (c) => {
+ const key = c.req.param('key');
+ const body = await c.req.json();
+
+ await c.env.KV.put(key, JSON.stringify(body), {
+ expirationTtl: 3600 // 1 Stunde
+ });
+
+ return c.json({ success: true });
+});
+
+// R2 Storage
+app.post('/upload', async (c) => {
+ const formData = await c.req.formData();
+ const file = formData.get('file') as File;
+
+ await c.env.BUCKET.put(file.name, file);
+
+ return c.json({ uploaded: file.name });
+});
+
+// Workers AI
+app.post('/ai/chat', async (c) => {
+ const { message } = await c.req.json();
+
+ const response = await c.env.AI.run('@cf/meta/llama-2-7b-chat-int8', {
+ messages: [{ role: 'user', content: message }]
+ });
+
+ return c.json(response);
+});
+
+export default app;
+```
+
+---
+
+## Streaming & SSE
+
+```typescript
+import { Hono } from 'hono';
+import { streamSSE, streamText } from 'hono/streaming';
+
+const app = new Hono();
+
+// Server-Sent Events
+app.get('/sse', (c) => {
+ return streamSSE(c, async (stream) => {
+ let id = 0;
+
+ while (true) {
+ await stream.writeSSE({
+ data: JSON.stringify({ time: new Date().toISOString() }),
+ event: 'tick',
+ id: String(id++)
+ });
+
+ await stream.sleep(1000);
+ }
+ });
+});
+
+// Text Streaming (für AI)
+app.get('/stream', (c) => {
+ return streamText(c, async (stream) => {
+ const words = 'Hello, this is a streaming response!'.split(' ');
+
+ for (const word of words) {
+ await stream.write(word + ' ');
+ await stream.sleep(100);
+ }
+ });
+});
+
+// AI Chat Streaming
+app.post('/chat', async (c) => {
+ const { message } = await c.req.json();
+
+ return streamText(c, async (stream) => {
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4',
+ messages: [{ role: 'user', content: message }],
+ stream: true
+ });
+
+ for await (const chunk of response) {
+ const content = chunk.choices[0]?.delta?.content;
+ if (content) {
+ await stream.write(content);
+ }
+ }
+ });
+});
+```
+
+---
+
+## HonoX (Meta-Framework)
+
+```typescript
+// HonoX: File-based Routing + Islands Architecture
+
+// app/routes/index.tsx
+export default function Home() {
+ return (
+
+
+ Welcome to HonoX
+ {/* Client Island */}
+
+
+ );
+}
+
+// app/routes/api/users.ts
+import { Hono } from 'hono';
+
+const app = new Hono();
+
+app.get('/', (c) => c.json({ users: [] }));
+
+export default app;
+```
+
+---
+
+## Fazit
+
+Hono bietet:
+
+1. **Ultra-Lightweight**: < 12KB, zero dependencies
+2. **Multi-Runtime**: Gleicher Code überall
+3. **Web Standards**: Request/Response API
+4. **Type-Safe RPC**: End-to-End TypeScript
+
+Das ideale Framework für Edge-First Entwicklung.
+
+---
+
+## Bildprompts
+
+1. "Flame icon transforming into web API endpoints, Hono framework concept"
+2. "Code running across multiple cloud platforms, multi-runtime visualization"
+3. "Lightweight feather compared to heavy framework boxes, minimal bundle"
+
+---
+
+## Quellen
+
+- [Hono Documentation](https://hono.dev/)
+- [Hono GitHub](https://github.com/honojs/hono)
+- [Cloudflare Hono Guide](https://developers.cloudflare.com/workers/framework-guides/web-apps/more-web-frameworks/hono/)
+- [The Story of Hono](https://blog.cloudflare.com/the-story-of-web-framework-hono-from-the-creator-of-hono/)
diff --git a/blog-posts/48-api-design-patterns.md b/blog-posts/48-api-design-patterns.md
new file mode 100644
index 0000000..160f676
--- /dev/null
+++ b/blog-posts/48-api-design-patterns.md
@@ -0,0 +1,475 @@
+# API Design Patterns für 2026
+
+**Meta-Description:** Moderne API Design Patterns. REST Best Practices, GraphQL vs tRPC, API Versioning und Error Handling Standards.
+
+**Keywords:** API Design, REST API, GraphQL, tRPC, API Versioning, Error Handling, OpenAPI, API Security
+
+---
+
+## Einführung
+
+Gutes API Design entscheidet über Developer Experience und Wartbarkeit. 2026 stehen mehrere Paradigmen zur Verfügung: **REST, GraphQL, tRPC** – jedes mit eigenen Stärken für verschiedene Use Cases.
+
+---
+
+## API Paradigmen im Vergleich
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ API PARADIGMEN 2026 │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ REST │
+│ ├── Resource-orientiert │
+│ ├── HTTP Verben (GET, POST, PUT, DELETE) │
+│ ├── Stateless │
+│ ├── Caching-freundlich │
+│ └── Best for: Public APIs, Microservices │
+│ │
+│ GraphQL │
+│ ├── Schema-first │
+│ ├── Single Endpoint │
+│ ├── Client-driven Queries │
+│ ├── Subscriptions (Real-time) │
+│ └── Best for: Complex Data, Mobile Apps │
+│ │
+│ tRPC │
+│ ├── End-to-End Type Safety │
+│ ├── No Code Generation │
+│ ├── Zod Validation │
+│ ├── React Query Integration │
+│ └── Best for: TypeScript Monorepos │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## REST Best Practices
+
+```typescript
+// 1. Resource Naming (Plural Nouns)
+// ✅ Good
+GET /users
+GET /users/:id
+POST /users
+PUT /users/:id
+DELETE /users/:id
+
+// ❌ Bad
+GET /getUser
+POST /createUser
+GET /user-list
+
+// 2. Nested Resources
+GET /users/:userId/posts
+GET /users/:userId/posts/:postId
+POST /users/:userId/posts
+
+// 3. Query Parameters für Filtering/Sorting
+GET /posts?status=published&author=123&sort=-createdAt&limit=10&offset=20
+
+// 4. HTTP Status Codes
+// 200 OK - Success
+// 201 Created - Resource created
+// 204 No Content - Success, no body (DELETE)
+// 400 Bad Request - Validation error
+// 401 Unauthorized - Authentication required
+// 403 Forbidden - Permission denied
+// 404 Not Found - Resource not found
+// 409 Conflict - Duplicate/Conflict
+// 422 Unprocessable Entity - Semantic error
+// 429 Too Many Requests - Rate limited
+// 500 Internal Server Error - Server error
+```
+
+```typescript
+// Next.js App Router REST API
+// app/api/users/route.ts
+import { NextRequest, NextResponse } from 'next/server';
+import { z } from 'zod';
+
+const createUserSchema = z.object({
+ email: z.string().email(),
+ name: z.string().min(2)
+});
+
+// GET /api/users
+export async function GET(request: NextRequest) {
+ const searchParams = request.nextUrl.searchParams;
+ const limit = parseInt(searchParams.get('limit') || '10');
+ const offset = parseInt(searchParams.get('offset') || '0');
+
+ const users = await db.user.findMany({
+ take: limit,
+ skip: offset
+ });
+
+ const total = await db.user.count();
+
+ return NextResponse.json({
+ data: users,
+ pagination: {
+ total,
+ limit,
+ offset,
+ hasMore: offset + limit < total
+ }
+ });
+}
+
+// POST /api/users
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const data = createUserSchema.parse(body);
+
+ const user = await db.user.create({ data });
+
+ return NextResponse.json(
+ { data: user },
+ { status: 201 }
+ );
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ {
+ error: 'Validation Error',
+ details: error.errors
+ },
+ { status: 400 }
+ );
+ }
+ throw error;
+ }
+}
+```
+
+---
+
+## Error Response Standard
+
+```typescript
+// RFC 7807 Problem Details
+interface ProblemDetails {
+ type: string; // URI Reference für Error Type
+ title: string; // Kurze Beschreibung
+ status: number; // HTTP Status Code
+ detail?: string; // Ausführliche Beschreibung
+ instance?: string; // URI der fehlerhaften Ressource
+ [key: string]: unknown; // Erweiterungen
+}
+
+// Beispiel Implementation
+function createErrorResponse(
+ status: number,
+ title: string,
+ detail?: string,
+ extras?: Record
+): NextResponse {
+ const body: ProblemDetails = {
+ type: `https://api.example.com/errors/${status}`,
+ title,
+ status,
+ detail,
+ instance: `/api/request-id/${crypto.randomUUID()}`,
+ timestamp: new Date().toISOString(),
+ ...extras
+ };
+
+ return NextResponse.json(body, {
+ status,
+ headers: {
+ 'Content-Type': 'application/problem+json'
+ }
+ });
+}
+
+// Verwendung
+if (!user) {
+ return createErrorResponse(
+ 404,
+ 'User Not Found',
+ `User with ID ${id} does not exist`
+ );
+}
+
+// Validation Error mit Details
+return createErrorResponse(
+ 400,
+ 'Validation Error',
+ 'The request body contains invalid data',
+ {
+ errors: [
+ { field: 'email', message: 'Invalid email format' },
+ { field: 'name', message: 'Name is required' }
+ ]
+ }
+);
+```
+
+---
+
+## API Versioning Strategies
+
+```typescript
+// 1. URL Versioning (Empfohlen für Breaking Changes)
+// /api/v1/users
+// /api/v2/users
+
+// app/api/v1/users/route.ts
+export async function GET() {
+ // V1 Response Format
+ return NextResponse.json({ users: [...] });
+}
+
+// app/api/v2/users/route.ts
+export async function GET() {
+ // V2 Response Format (z.B. neue Felder)
+ return NextResponse.json({
+ data: [...],
+ meta: { version: 'v2' }
+ });
+}
+
+// 2. Header Versioning
+// Accept: application/vnd.api+json;version=2
+export async function GET(request: NextRequest) {
+ const accept = request.headers.get('Accept') || '';
+ const version = accept.match(/version=(\d+)/)?.[1] || '1';
+
+ if (version === '2') {
+ return handleV2(request);
+ }
+ return handleV1(request);
+}
+
+// 3. Query Parameter (für Clients ohne Header-Kontrolle)
+// /api/users?version=2
+export async function GET(request: NextRequest) {
+ const version = request.nextUrl.searchParams.get('version') || '1';
+ // ...
+}
+```
+
+---
+
+## Rate Limiting
+
+```typescript
+import { Ratelimit } from '@upstash/ratelimit';
+import { Redis } from '@upstash/redis';
+
+const ratelimit = new Ratelimit({
+ redis: Redis.fromEnv(),
+ limiter: Ratelimit.slidingWindow(10, '10s'), // 10 Requests pro 10s
+ analytics: true
+});
+
+// Middleware
+export async function middleware(request: NextRequest) {
+ const ip = request.ip ?? '127.0.0.1';
+ const { success, limit, reset, remaining } = await ratelimit.limit(ip);
+
+ if (!success) {
+ return NextResponse.json(
+ {
+ error: 'Too Many Requests',
+ retryAfter: Math.ceil((reset - Date.now()) / 1000)
+ },
+ {
+ status: 429,
+ headers: {
+ 'X-RateLimit-Limit': limit.toString(),
+ 'X-RateLimit-Remaining': remaining.toString(),
+ 'X-RateLimit-Reset': reset.toString(),
+ 'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString()
+ }
+ }
+ );
+ }
+
+ const response = NextResponse.next();
+ response.headers.set('X-RateLimit-Limit', limit.toString());
+ response.headers.set('X-RateLimit-Remaining', remaining.toString());
+ return response;
+}
+```
+
+---
+
+## Pagination Patterns
+
+```typescript
+// 1. Offset Pagination (Einfach, aber langsam bei großen Datasets)
+interface OffsetPagination {
+ data: User[];
+ pagination: {
+ total: number;
+ limit: number;
+ offset: number;
+ hasMore: boolean;
+ };
+}
+
+// 2. Cursor Pagination (Empfohlen für große Datasets)
+interface CursorPagination {
+ data: T[];
+ pagination: {
+ cursor: string | null;
+ hasMore: boolean;
+ };
+}
+
+async function getUsersWithCursor(cursor?: string, limit = 20) {
+ const users = await db.user.findMany({
+ take: limit + 1, // +1 um hasMore zu prüfen
+ cursor: cursor ? { id: cursor } : undefined,
+ orderBy: { createdAt: 'desc' }
+ });
+
+ const hasMore = users.length > limit;
+ const data = hasMore ? users.slice(0, -1) : users;
+ const nextCursor = hasMore ? data[data.length - 1].id : null;
+
+ return {
+ data,
+ pagination: {
+ cursor: nextCursor,
+ hasMore
+ }
+ };
+}
+
+// 3. Keyset Pagination (Für sortierte Daten)
+// /api/posts?after=2024-01-15T10:00:00Z&limit=20
+```
+
+---
+
+## OpenAPI Specification
+
+```typescript
+// Mit Zod + zod-to-openapi
+import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
+
+const app = new OpenAPIHono();
+
+const UserSchema = z.object({
+ id: z.string().uuid(),
+ email: z.string().email(),
+ name: z.string()
+}).openapi('User');
+
+const route = createRoute({
+ method: 'get',
+ path: '/users/{id}',
+ request: {
+ params: z.object({
+ id: z.string().uuid()
+ })
+ },
+ responses: {
+ 200: {
+ content: {
+ 'application/json': {
+ schema: UserSchema
+ }
+ },
+ description: 'User found'
+ },
+ 404: {
+ content: {
+ 'application/json': {
+ schema: z.object({
+ error: z.string()
+ })
+ }
+ },
+ description: 'User not found'
+ }
+ }
+});
+
+app.openapi(route, (c) => {
+ const { id } = c.req.valid('param');
+ // ...
+});
+
+// OpenAPI Spec generieren
+app.doc('/doc', {
+ openapi: '3.1.0',
+ info: {
+ title: 'My API',
+ version: '1.0.0'
+ }
+});
+```
+
+---
+
+## API Security Checklist
+
+```typescript
+// 1. Authentication
+// - JWT mit kurzer Expiration
+// - Refresh Token Rotation
+// - Secure Cookie Storage
+
+// 2. Authorization
+// - RBAC oder ABAC
+// - Resource-level Permissions
+// - Rate Limiting per User
+
+// 3. Input Validation
+// - Zod für alle Inputs
+// - Sanitize User Input
+// - File Upload Restrictions
+
+// 4. Headers
+const securityHeaders = {
+ 'Content-Security-Policy': "default-src 'self'",
+ 'X-Content-Type-Options': 'nosniff',
+ 'X-Frame-Options': 'DENY',
+ 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
+ 'X-XSS-Protection': '1; mode=block'
+};
+
+// 5. CORS
+const corsConfig = {
+ origin: ['https://app.example.com'],
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
+ credentials: true,
+ maxAge: 86400
+};
+```
+
+---
+
+## Fazit
+
+Gutes API Design 2026 bedeutet:
+
+1. **Konsistenz**: Einheitliche Naming, Responses, Errors
+2. **Type Safety**: Zod/OpenAPI für Contracts
+3. **Performance**: Pagination, Caching, Rate Limiting
+4. **Security**: Validation, Auth, Headers
+
+Die Wahl zwischen REST/GraphQL/tRPC hängt vom Use Case ab.
+
+---
+
+## Bildprompts
+
+1. "API endpoints connecting client and server, clean architecture diagram"
+2. "REST vs GraphQL vs tRPC comparison, three paths to data"
+3. "Security shield protecting API gateway, authentication visualization"
+
+---
+
+## Quellen
+
+- [REST API Design Best Practices](https://restfulapi.net/)
+- [RFC 7807 - Problem Details](https://datatracker.ietf.org/doc/html/rfc7807)
+- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)
+- [tRPC Documentation](https://trpc.io/)
diff --git a/blog-posts/49-authentication-patterns.md b/blog-posts/49-authentication-patterns.md
new file mode 100644
index 0000000..657d187
--- /dev/null
+++ b/blog-posts/49-authentication-patterns.md
@@ -0,0 +1,559 @@
+# Authentication Patterns für moderne Apps
+
+**Meta-Description:** Moderne Authentication Patterns. JWT, Session Auth, OAuth 2.0, Passkeys und Auth.js Integration für Next.js.
+
+**Keywords:** Authentication, JWT, OAuth, Passkeys, Session Auth, Auth.js, NextAuth, Security, OIDC
+
+---
+
+## Einführung
+
+Authentication ist die erste Verteidigungslinie jeder Anwendung. 2026 stehen mehrere Patterns zur Verfügung: **JWT, Sessions, OAuth, Passkeys** – jedes mit eigenen Trade-offs für Security und UX.
+
+---
+
+## Auth Patterns Overview
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ AUTHENTICATION PATTERNS │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Session-Based (Stateful): │
+│ ├── Server speichert Session │
+│ ├── Cookie mit Session ID │
+│ ├── Einfach zu invalidieren │
+│ └── Best for: Traditional Web Apps │
+│ │
+│ JWT (Stateless): │
+│ ├── Self-contained Token │
+│ ├── Keine Server-Speicherung │
+│ ├── Schwer zu invalidieren │
+│ └── Best for: APIs, Microservices │
+│ │
+│ OAuth 2.0 / OIDC: │
+│ ├── Delegierte Authentifizierung │
+│ ├── Google, GitHub, etc. │
+│ ├── Refresh Token Flow │
+│ └── Best for: Social Login │
+│ │
+│ Passkeys (WebAuthn): │
+│ ├── Passwordless │
+│ ├── Biometric/Device-based │
+│ ├── Phishing-resistant │
+│ └── Best for: High Security, Modern UX │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Auth.js (NextAuth) v5 Setup
+
+```bash
+npm install next-auth@beta
+```
+
+```typescript
+// auth.ts
+import NextAuth from 'next-auth';
+import GitHub from 'next-auth/providers/github';
+import Google from 'next-auth/providers/google';
+import Credentials from 'next-auth/providers/credentials';
+import { PrismaAdapter } from '@auth/prisma-adapter';
+import { prisma } from '@/lib/prisma';
+import { z } from 'zod';
+import bcrypt from 'bcryptjs';
+
+export const { handlers, signIn, signOut, auth } = NextAuth({
+ adapter: PrismaAdapter(prisma),
+ providers: [
+ GitHub({
+ clientId: process.env.GITHUB_ID!,
+ clientSecret: process.env.GITHUB_SECRET!
+ }),
+ Google({
+ clientId: process.env.GOOGLE_ID!,
+ clientSecret: process.env.GOOGLE_SECRET!
+ }),
+ Credentials({
+ credentials: {
+ email: { label: 'Email', type: 'email' },
+ password: { label: 'Password', type: 'password' }
+ },
+ async authorize(credentials) {
+ const parsed = z.object({
+ email: z.string().email(),
+ password: z.string().min(8)
+ }).safeParse(credentials);
+
+ if (!parsed.success) return null;
+
+ const user = await prisma.user.findUnique({
+ where: { email: parsed.data.email }
+ });
+
+ if (!user?.password) return null;
+
+ const valid = await bcrypt.compare(
+ parsed.data.password,
+ user.password
+ );
+
+ if (!valid) return null;
+
+ return {
+ id: user.id,
+ email: user.email,
+ name: user.name
+ };
+ }
+ })
+ ],
+ session: {
+ strategy: 'jwt' // oder 'database'
+ },
+ callbacks: {
+ async jwt({ token, user }) {
+ if (user) {
+ token.id = user.id;
+ token.role = user.role;
+ }
+ return token;
+ },
+ async session({ session, token }) {
+ session.user.id = token.id as string;
+ session.user.role = token.role as string;
+ return session;
+ }
+ },
+ pages: {
+ signIn: '/login',
+ error: '/login'
+ }
+});
+```
+
+```typescript
+// app/api/auth/[...nextauth]/route.ts
+import { handlers } from '@/auth';
+export const { GET, POST } = handlers;
+
+// middleware.ts
+import { auth } from '@/auth';
+
+export default auth((req) => {
+ if (!req.auth && req.nextUrl.pathname.startsWith('/dashboard')) {
+ return Response.redirect(new URL('/login', req.nextUrl));
+ }
+});
+
+export const config = {
+ matcher: ['/dashboard/:path*', '/api/protected/:path*']
+};
+```
+
+---
+
+## JWT Implementation (Custom)
+
+```typescript
+// lib/jwt.ts
+import { SignJWT, jwtVerify } from 'jose';
+
+const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
+
+interface TokenPayload {
+ userId: string;
+ email: string;
+ role: string;
+}
+
+// Access Token (kurze Lebenszeit)
+export async function createAccessToken(payload: TokenPayload) {
+ return new SignJWT(payload)
+ .setProtectedHeader({ alg: 'HS256' })
+ .setIssuedAt()
+ .setExpirationTime('15m') // 15 Minuten
+ .sign(secret);
+}
+
+// Refresh Token (lange Lebenszeit)
+export async function createRefreshToken(userId: string) {
+ return new SignJWT({ userId })
+ .setProtectedHeader({ alg: 'HS256' })
+ .setIssuedAt()
+ .setExpirationTime('7d') // 7 Tage
+ .sign(secret);
+}
+
+export async function verifyToken(token: string) {
+ try {
+ const { payload } = await jwtVerify(token, secret);
+ return payload as TokenPayload & { exp: number };
+ } catch {
+ return null;
+ }
+}
+
+// Token Rotation Pattern
+export async function refreshTokens(refreshToken: string) {
+ const payload = await verifyToken(refreshToken);
+ if (!payload) throw new Error('Invalid refresh token');
+
+ // Optional: Refresh Token im DB invalidieren (Rotation)
+ await db.refreshToken.delete({
+ where: { token: refreshToken }
+ });
+
+ const user = await db.user.findUnique({
+ where: { id: payload.userId }
+ });
+
+ if (!user) throw new Error('User not found');
+
+ const newAccessToken = await createAccessToken({
+ userId: user.id,
+ email: user.email,
+ role: user.role
+ });
+
+ const newRefreshToken = await createRefreshToken(user.id);
+
+ // Neuen Refresh Token speichern
+ await db.refreshToken.create({
+ data: {
+ token: newRefreshToken,
+ userId: user.id,
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
+ }
+ });
+
+ return { accessToken: newAccessToken, refreshToken: newRefreshToken };
+}
+```
+
+---
+
+## Passkeys (WebAuthn)
+
+```typescript
+// lib/passkeys.ts
+import {
+ generateRegistrationOptions,
+ verifyRegistrationResponse,
+ generateAuthenticationOptions,
+ verifyAuthenticationResponse
+} from '@simplewebauthn/server';
+
+const rpName = 'My App';
+const rpID = 'example.com';
+const origin = 'https://example.com';
+
+// Registration
+export async function startPasskeyRegistration(userId: string, email: string) {
+ const existingCredentials = await db.credential.findMany({
+ where: { userId }
+ });
+
+ const options = await generateRegistrationOptions({
+ rpName,
+ rpID,
+ userID: userId,
+ userName: email,
+ attestationType: 'none',
+ excludeCredentials: existingCredentials.map(c => ({
+ id: c.credentialId,
+ type: 'public-key'
+ })),
+ authenticatorSelection: {
+ residentKey: 'preferred',
+ userVerification: 'preferred'
+ }
+ });
+
+ // Challenge speichern
+ await db.user.update({
+ where: { id: userId },
+ data: { currentChallenge: options.challenge }
+ });
+
+ return options;
+}
+
+export async function finishPasskeyRegistration(
+ userId: string,
+ response: RegistrationResponseJSON
+) {
+ const user = await db.user.findUnique({ where: { id: userId } });
+ if (!user?.currentChallenge) throw new Error('No challenge');
+
+ const verification = await verifyRegistrationResponse({
+ response,
+ expectedChallenge: user.currentChallenge,
+ expectedOrigin: origin,
+ expectedRPID: rpID
+ });
+
+ if (verification.verified && verification.registrationInfo) {
+ await db.credential.create({
+ data: {
+ userId,
+ credentialId: verification.registrationInfo.credentialID,
+ publicKey: Buffer.from(verification.registrationInfo.credentialPublicKey),
+ counter: verification.registrationInfo.counter
+ }
+ });
+ }
+
+ return verification.verified;
+}
+
+// Authentication
+export async function startPasskeyAuth(email: string) {
+ const user = await db.user.findUnique({
+ where: { email },
+ include: { credentials: true }
+ });
+
+ if (!user) throw new Error('User not found');
+
+ const options = await generateAuthenticationOptions({
+ rpID,
+ allowCredentials: user.credentials.map(c => ({
+ id: c.credentialId,
+ type: 'public-key'
+ })),
+ userVerification: 'preferred'
+ });
+
+ await db.user.update({
+ where: { id: user.id },
+ data: { currentChallenge: options.challenge }
+ });
+
+ return options;
+}
+
+export async function finishPasskeyAuth(
+ email: string,
+ response: AuthenticationResponseJSON
+) {
+ const user = await db.user.findUnique({
+ where: { email },
+ include: { credentials: true }
+ });
+
+ if (!user?.currentChallenge) throw new Error('No challenge');
+
+ const credential = user.credentials.find(
+ c => c.credentialId === response.id
+ );
+
+ if (!credential) throw new Error('Unknown credential');
+
+ const verification = await verifyAuthenticationResponse({
+ response,
+ expectedChallenge: user.currentChallenge,
+ expectedOrigin: origin,
+ expectedRPID: rpID,
+ authenticator: {
+ credentialID: credential.credentialId,
+ credentialPublicKey: credential.publicKey,
+ counter: credential.counter
+ }
+ });
+
+ if (verification.verified) {
+ // Counter updaten
+ await db.credential.update({
+ where: { id: credential.id },
+ data: { counter: verification.authenticationInfo.newCounter }
+ });
+
+ return user;
+ }
+
+ throw new Error('Verification failed');
+}
+```
+
+```typescript
+// Client-Side (React)
+'use client';
+
+import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
+
+function PasskeyLogin() {
+ const handleRegister = async () => {
+ const options = await fetch('/api/passkey/register/start', {
+ method: 'POST'
+ }).then(r => r.json());
+
+ const result = await startRegistration(options);
+
+ await fetch('/api/passkey/register/finish', {
+ method: 'POST',
+ body: JSON.stringify(result)
+ });
+ };
+
+ const handleLogin = async () => {
+ const options = await fetch('/api/passkey/login/start', {
+ method: 'POST',
+ body: JSON.stringify({ email })
+ }).then(r => r.json());
+
+ const result = await startAuthentication(options);
+
+ await fetch('/api/passkey/login/finish', {
+ method: 'POST',
+ body: JSON.stringify(result)
+ });
+ };
+
+ return (
+
+
+
+
+ );
+}
+```
+
+---
+
+## Magic Links
+
+```typescript
+// Magic Link / Email Auth
+import { Resend } from 'resend';
+import crypto from 'crypto';
+
+const resend = new Resend(process.env.RESEND_API_KEY);
+
+export async function sendMagicLink(email: string) {
+ const token = crypto.randomBytes(32).toString('hex');
+ const expires = new Date(Date.now() + 15 * 60 * 1000); // 15 min
+
+ await db.verificationToken.create({
+ data: {
+ identifier: email,
+ token: await hash(token),
+ expires
+ }
+ });
+
+ const url = `${process.env.NEXTAUTH_URL}/api/auth/verify?token=${token}&email=${email}`;
+
+ await resend.emails.send({
+ from: 'noreply@example.com',
+ to: email,
+ subject: 'Login to My App',
+ html: `
+ Click the link below to sign in:
+ Sign in to My App
+ This link expires in 15 minutes.
+ `
+ });
+}
+
+export async function verifyMagicLink(token: string, email: string) {
+ const storedToken = await db.verificationToken.findFirst({
+ where: {
+ identifier: email,
+ expires: { gt: new Date() }
+ }
+ });
+
+ if (!storedToken || !await verify(token, storedToken.token)) {
+ throw new Error('Invalid or expired token');
+ }
+
+ await db.verificationToken.delete({
+ where: { id: storedToken.id }
+ });
+
+ const user = await db.user.upsert({
+ where: { email },
+ update: { emailVerified: new Date() },
+ create: { email, emailVerified: new Date() }
+ });
+
+ return user;
+}
+```
+
+---
+
+## Security Best Practices
+
+```typescript
+// 1. Password Hashing
+import bcrypt from 'bcryptjs';
+
+const SALT_ROUNDS = 12;
+
+export async function hashPassword(password: string) {
+ return bcrypt.hash(password, SALT_ROUNDS);
+}
+
+// 2. CSRF Protection (automatisch bei Auth.js)
+
+// 3. Secure Cookie Settings
+const cookieOptions = {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax' as const,
+ path: '/',
+ maxAge: 60 * 60 * 24 * 7 // 7 Tage
+};
+
+// 4. Rate Limiting für Auth Endpoints
+// Siehe API Design Patterns Artikel
+
+// 5. Account Lockout
+async function checkLoginAttempts(email: string) {
+ const attempts = await db.loginAttempt.count({
+ where: {
+ email,
+ success: false,
+ createdAt: { gt: new Date(Date.now() - 15 * 60 * 1000) }
+ }
+ });
+
+ if (attempts >= 5) {
+ throw new Error('Account temporarily locked. Try again later.');
+ }
+}
+```
+
+---
+
+## Fazit
+
+Authentication 2026:
+
+1. **Auth.js v5**: Standard für Next.js
+2. **Passkeys**: Die Zukunft des Logins
+3. **JWT + Refresh**: Für APIs
+4. **MFA**: Immer aktivieren wenn möglich
+
+Wähle basierend auf Security-Anforderungen und UX.
+
+---
+
+## Bildprompts
+
+1. "Multiple authentication methods merging into single secure access, login concept"
+2. "Passkey biometric authentication on device, passwordless future"
+3. "Security layers protecting user identity, authentication shield"
+
+---
+
+## Quellen
+
+- [Auth.js Documentation](https://authjs.dev/)
+- [WebAuthn Guide](https://webauthn.guide/)
+- [SimpleWebAuthn](https://simplewebauthn.dev/)
+- [OWASP Authentication Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
diff --git a/blog-posts/50-background-jobs-queues.md b/blog-posts/50-background-jobs-queues.md
new file mode 100644
index 0000000..482b3f7
--- /dev/null
+++ b/blog-posts/50-background-jobs-queues.md
@@ -0,0 +1,532 @@
+# Background Jobs & Queues mit BullMQ
+
+**Meta-Description:** Background Job Processing mit BullMQ und Redis. Queues, Workers, Retries und Job Scheduling für Node.js.
+
+**Keywords:** BullMQ, Background Jobs, Queue Processing, Redis Queue, Job Scheduling, Worker Threads, Async Processing
+
+---
+
+## Einführung
+
+Nicht jede Aufgabe sollte im Request-Response-Zyklus laufen. **Background Jobs** ermöglichen Email-Versand, Image Processing, Reports – alles asynchron mit Retries, Prioritäten und Scheduling.
+
+---
+
+## Warum Background Jobs?
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ SYNCHRON vs ASYNCHRON │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Synchron (schlecht für): │
+│ ├── Email-Versand (3-5s) │
+│ ├── Image/Video Processing (10s+) │
+│ ├── PDF Generation (2-5s) │
+│ ├── Externe API Calls (variabel) │
+│ └── Batch Operations (Minuten) │
+│ │
+│ User wartet... ⏳ │
+│ │
+│ Asynchron (Background): │
+│ ├── Request → Response (50ms) │
+│ ├── Job in Queue → Worker verarbeitet │
+│ ├── Retries bei Fehlern │
+│ ├── Rate Limiting │
+│ └── Scheduling (Cron) │
+│ │
+│ User bekommt sofort Antwort! ✓ │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## BullMQ Setup
+
+```bash
+npm install bullmq ioredis
+```
+
+```typescript
+// lib/queue.ts
+import { Queue, Worker, QueueEvents } from 'bullmq';
+import IORedis from 'ioredis';
+
+const connection = new IORedis(process.env.REDIS_URL!, {
+ maxRetriesPerRequest: null
+});
+
+// Queue definieren
+export const emailQueue = new Queue('email', { connection });
+export const imageQueue = new Queue('image-processing', { connection });
+export const reportQueue = new Queue('reports', { connection });
+
+// Queue Events (für Monitoring)
+export const emailQueueEvents = new QueueEvents('email', { connection });
+```
+
+---
+
+## Job Types definieren
+
+```typescript
+// types/jobs.ts
+export interface EmailJob {
+ type: 'welcome' | 'reset-password' | 'notification';
+ to: string;
+ subject: string;
+ template: string;
+ data: Record;
+}
+
+export interface ImageProcessingJob {
+ imageId: string;
+ operations: Array<{
+ type: 'resize' | 'crop' | 'watermark';
+ params: Record;
+ }>;
+ outputFormat: 'webp' | 'avif' | 'jpeg';
+}
+
+export interface ReportJob {
+ reportType: 'daily' | 'weekly' | 'monthly';
+ userId: string;
+ dateRange: {
+ start: Date;
+ end: Date;
+ };
+}
+```
+
+---
+
+## Jobs hinzufügen
+
+```typescript
+// services/email.ts
+import { emailQueue } from '@/lib/queue';
+import { EmailJob } from '@/types/jobs';
+
+export async function sendWelcomeEmail(userId: string, email: string) {
+ const job = await emailQueue.add('send-email', {
+ type: 'welcome',
+ to: email,
+ subject: 'Welcome to Our Platform!',
+ template: 'welcome',
+ data: { userId }
+ } satisfies EmailJob, {
+ // Job Options
+ attempts: 3,
+ backoff: {
+ type: 'exponential',
+ delay: 1000 // 1s, 2s, 4s
+ },
+ removeOnComplete: {
+ count: 1000 // Behalte letzte 1000 completed Jobs
+ },
+ removeOnFail: {
+ count: 5000 // Behalte letzte 5000 failed Jobs
+ }
+ });
+
+ return job.id;
+}
+
+// Mit Priorität
+export async function sendUrgentEmail(data: EmailJob) {
+ await emailQueue.add('send-email', data, {
+ priority: 1 // Niedrigere Zahl = höhere Priorität
+ });
+}
+
+// Verzögerter Job
+export async function scheduleEmail(data: EmailJob, delay: number) {
+ await emailQueue.add('send-email', data, {
+ delay // Millisekunden
+ });
+}
+
+// Geplanter Job (Cron)
+export async function scheduleReports() {
+ await reportQueue.add('generate-report', {
+ reportType: 'daily',
+ // ...
+ }, {
+ repeat: {
+ pattern: '0 8 * * *' // Täglich um 8:00
+ }
+ });
+}
+```
+
+---
+
+## Worker implementieren
+
+```typescript
+// workers/email.worker.ts
+import { Worker, Job } from 'bullmq';
+import IORedis from 'ioredis';
+import { Resend } from 'resend';
+import { EmailJob } from '@/types/jobs';
+
+const connection = new IORedis(process.env.REDIS_URL!, {
+ maxRetriesPerRequest: null
+});
+
+const resend = new Resend(process.env.RESEND_API_KEY);
+
+const emailWorker = new Worker(
+ 'email',
+ async (job: Job) => {
+ console.log(`Processing job ${job.id}: ${job.data.type}`);
+
+ const { to, subject, template, data } = job.data;
+
+ // Template rendern
+ const html = await renderTemplate(template, data);
+
+ // Email senden
+ const result = await resend.emails.send({
+ from: 'noreply@example.com',
+ to,
+ subject,
+ html
+ });
+
+ console.log(`Email sent: ${result.id}`);
+
+ return { emailId: result.id };
+ },
+ {
+ connection,
+ concurrency: 5, // 5 Jobs parallel
+ limiter: {
+ max: 100, // Max 100 Jobs
+ duration: 60000 // Pro Minute (Rate Limiting)
+ }
+ }
+);
+
+// Event Handlers
+emailWorker.on('completed', (job, result) => {
+ console.log(`Job ${job.id} completed with result:`, result);
+});
+
+emailWorker.on('failed', (job, error) => {
+ console.error(`Job ${job?.id} failed:`, error.message);
+});
+
+emailWorker.on('progress', (job, progress) => {
+ console.log(`Job ${job.id} progress: ${progress}%`);
+});
+
+export { emailWorker };
+```
+
+```typescript
+// workers/image.worker.ts
+import { Worker, Job } from 'bullmq';
+import sharp from 'sharp';
+import { ImageProcessingJob } from '@/types/jobs';
+
+const imageWorker = new Worker(
+ 'image-processing',
+ async (job: Job) => {
+ const { imageId, operations, outputFormat } = job.data;
+
+ // Bild laden
+ const image = await loadImage(imageId);
+ let pipeline = sharp(image);
+
+ // Operationen anwenden
+ for (let i = 0; i < operations.length; i++) {
+ const op = operations[i];
+
+ // Progress updaten
+ await job.updateProgress(Math.round((i / operations.length) * 100));
+
+ switch (op.type) {
+ case 'resize':
+ pipeline = pipeline.resize(op.params.width, op.params.height);
+ break;
+ case 'crop':
+ pipeline = pipeline.extract(op.params);
+ break;
+ case 'watermark':
+ pipeline = pipeline.composite([{
+ input: await loadWatermark(),
+ gravity: 'southeast'
+ }]);
+ break;
+ }
+ }
+
+ // Output
+ const output = await pipeline[outputFormat]().toBuffer();
+
+ // Speichern
+ const url = await uploadToStorage(output, `${imageId}.${outputFormat}`);
+
+ return { url, size: output.length };
+ },
+ {
+ connection,
+ concurrency: 2 // CPU-intensive, weniger parallel
+ }
+);
+```
+
+---
+
+## Job Progress & Events
+
+```typescript
+// API Route: Job Status
+// app/api/jobs/[id]/route.ts
+import { emailQueue } from '@/lib/queue';
+
+export async function GET(
+ request: Request,
+ { params }: { params: { id: string } }
+) {
+ const job = await emailQueue.getJob(params.id);
+
+ if (!job) {
+ return Response.json({ error: 'Job not found' }, { status: 404 });
+ }
+
+ const state = await job.getState();
+ const progress = job.progress;
+
+ return Response.json({
+ id: job.id,
+ state,
+ progress,
+ data: job.data,
+ returnValue: job.returnvalue,
+ failedReason: job.failedReason,
+ timestamp: job.timestamp,
+ processedOn: job.processedOn,
+ finishedOn: job.finishedOn
+ });
+}
+```
+
+```typescript
+// Real-time Updates mit SSE
+// app/api/jobs/[id]/stream/route.ts
+import { emailQueueEvents } from '@/lib/queue';
+
+export async function GET(
+ request: Request,
+ { params }: { params: { id: string } }
+) {
+ const encoder = new TextEncoder();
+
+ const stream = new ReadableStream({
+ start(controller) {
+ const handleCompleted = ({ jobId, returnvalue }) => {
+ if (jobId === params.id) {
+ controller.enqueue(
+ encoder.encode(`data: ${JSON.stringify({
+ event: 'completed',
+ result: returnvalue
+ })}\n\n`)
+ );
+ controller.close();
+ }
+ };
+
+ const handleFailed = ({ jobId, failedReason }) => {
+ if (jobId === params.id) {
+ controller.enqueue(
+ encoder.encode(`data: ${JSON.stringify({
+ event: 'failed',
+ error: failedReason
+ })}\n\n`)
+ );
+ controller.close();
+ }
+ };
+
+ const handleProgress = ({ jobId, data }) => {
+ if (jobId === params.id) {
+ controller.enqueue(
+ encoder.encode(`data: ${JSON.stringify({
+ event: 'progress',
+ progress: data
+ })}\n\n`)
+ );
+ }
+ };
+
+ emailQueueEvents.on('completed', handleCompleted);
+ emailQueueEvents.on('failed', handleFailed);
+ emailQueueEvents.on('progress', handleProgress);
+
+ return () => {
+ emailQueueEvents.off('completed', handleCompleted);
+ emailQueueEvents.off('failed', handleFailed);
+ emailQueueEvents.off('progress', handleProgress);
+ };
+ }
+ });
+
+ return new Response(stream, {
+ headers: {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ 'Connection': 'keep-alive'
+ }
+ });
+}
+```
+
+---
+
+## Job Scheduling (Cron)
+
+```typescript
+// Wiederkehrende Jobs einrichten
+async function setupScheduledJobs() {
+ // Täglich um 8:00 UTC
+ await reportQueue.add('daily-report', {
+ reportType: 'daily'
+ }, {
+ repeat: {
+ pattern: '0 8 * * *'
+ },
+ jobId: 'daily-report' // Verhindert Duplikate
+ });
+
+ // Wöchentlich Montag 9:00
+ await reportQueue.add('weekly-report', {
+ reportType: 'weekly'
+ }, {
+ repeat: {
+ pattern: '0 9 * * 1'
+ },
+ jobId: 'weekly-report'
+ });
+
+ // Alle 5 Minuten
+ await emailQueue.add('process-email-queue', {}, {
+ repeat: {
+ every: 5 * 60 * 1000
+ }
+ });
+}
+
+// Scheduled Jobs verwalten
+async function listScheduledJobs() {
+ const repeatableJobs = await emailQueue.getRepeatableJobs();
+ return repeatableJobs;
+}
+
+async function removeScheduledJob(key: string) {
+ await emailQueue.removeRepeatableByKey(key);
+}
+```
+
+---
+
+## Dashboard & Monitoring
+
+```typescript
+// Bull Board Integration
+import { createBullBoard } from '@bull-board/api';
+import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
+import { ExpressAdapter } from '@bull-board/express';
+
+const serverAdapter = new ExpressAdapter();
+serverAdapter.setBasePath('/admin/queues');
+
+createBullBoard({
+ queues: [
+ new BullMQAdapter(emailQueue),
+ new BullMQAdapter(imageQueue),
+ new BullMQAdapter(reportQueue)
+ ],
+ serverAdapter
+});
+
+// In Express/Hono einbinden
+app.use('/admin/queues', serverAdapter.getRouter());
+```
+
+---
+
+## Best Practices
+
+```typescript
+// 1. Idempotente Jobs (können mehrfach ausgeführt werden)
+async function processOrder(job: Job<{ orderId: string }>) {
+ const order = await db.order.findUnique({
+ where: { id: job.data.orderId }
+ });
+
+ // Prüfen ob schon verarbeitet
+ if (order?.status === 'processed') {
+ return { skipped: true, reason: 'Already processed' };
+ }
+
+ // Verarbeiten
+ await db.order.update({
+ where: { id: order.id },
+ data: { status: 'processed' }
+ });
+}
+
+// 2. Graceful Shutdown
+process.on('SIGTERM', async () => {
+ console.log('Shutting down workers...');
+ await emailWorker.close();
+ await imageWorker.close();
+ process.exit(0);
+});
+
+// 3. Dead Letter Queue
+const dlQueue = new Queue('dead-letter', { connection });
+
+emailWorker.on('failed', async (job, error) => {
+ if (job.attemptsMade >= job.opts.attempts!) {
+ await dlQueue.add('failed-email', {
+ originalJob: job.data,
+ error: error.message,
+ failedAt: new Date()
+ });
+ }
+});
+```
+
+---
+
+## Fazit
+
+BullMQ bietet:
+
+1. **Zuverlässigkeit**: Retries, Persistence, Acknowledgement
+2. **Skalierbarkeit**: Mehrere Worker, Rate Limiting
+3. **Flexibilität**: Scheduling, Prioritäten, Progress
+4. **Monitoring**: Bull Board, Events, Metrics
+
+Unverzichtbar für Production-Grade Async Processing.
+
+---
+
+## Bildprompts
+
+1. "Queue of tasks being processed by workers, assembly line concept"
+2. "Background process running while user continues, async workflow"
+3. "Redis connecting multiple workers, distributed job processing"
+
+---
+
+## Quellen
+
+- [BullMQ Documentation](https://docs.bullmq.io/)
+- [BullMQ GitHub](https://github.com/taskforcesh/bullmq)
+- [Bull Board](https://github.com/felixmosh/bull-board)
+- [Redis Pub/Sub](https://redis.io/docs/latest/develop/interact/pubsub/)
diff --git a/blog-posts/51-playwright-web-scraping.md b/blog-posts/51-playwright-web-scraping.md
new file mode 100644
index 0000000..64cf18a
--- /dev/null
+++ b/blog-posts/51-playwright-web-scraping.md
@@ -0,0 +1,494 @@
+# Playwright Web Scraping: Stealth & Performance Guide
+
+**Meta-Description:** Playwright für Web Scraping 2026. Stealth-Techniken, Anti-Bot Detection, Browser Fingerprinting und Performance-Optimierung.
+
+**Keywords:** Playwright, Web Scraping, Stealth, Browser Automation, Anti-Bot, Headless Browser, Data Extraction
+
+---
+
+## Einführung
+
+Playwright ist das **moderne Standard-Tool für Web Scraping**. Mit WebSocket-basierter Kommunikation, Native Network Interception und Multi-Browser-Support bietet es alles für skalierbare Datenextraktion.
+
+---
+
+## Playwright vs Alternativen
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ SCRAPING TOOLS 2026 │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ PLAYWRIGHT │
+│ ├── WebSocket-First (schneller als HTTP) │
+│ ├── Multi-Browser (Chrome, Firefox, WebKit) │
+│ ├── Native Request Interception │
+│ ├── Auto-Waiting │
+│ └── Best for: Modern JS Sites, Stealth │
+│ │
+│ PUPPETEER │
+│ ├── Chrome DevTools Protocol │
+│ ├── Chrome/Firefox Support │
+│ ├── Größere Community │
+│ └── Best for: Chrome-specific Features │
+│ │
+│ CHEERIO │
+│ ├── Kein Browser (nur HTML Parsing) │
+│ ├── Extrem schnell │
+│ ├── Niedrige Ressourcen │
+│ └── Best for: Static Sites │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Basic Setup
+
+```bash
+npm install playwright
+npx playwright install # Browser installieren
+```
+
+```typescript
+// scraper.ts
+import { chromium, Browser, Page } from 'playwright';
+
+async function scrape() {
+ const browser = await chromium.launch({
+ headless: true // 'new' ist jetzt default
+ });
+
+ const context = await browser.newContext({
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ viewport: { width: 1920, height: 1080 },
+ locale: 'de-DE',
+ timezoneId: 'Europe/Berlin'
+ });
+
+ const page = await context.newPage();
+
+ try {
+ await page.goto('https://example.com', {
+ waitUntil: 'networkidle',
+ timeout: 30000
+ });
+
+ // Scraping Logic
+ const data = await page.evaluate(() => {
+ return {
+ title: document.title,
+ headings: Array.from(document.querySelectorAll('h1, h2'))
+ .map(h => h.textContent)
+ };
+ });
+
+ return data;
+ } finally {
+ await browser.close();
+ }
+}
+```
+
+---
+
+## Stealth Mode
+
+```typescript
+// playwright-extra für Stealth Plugins
+import { chromium } from 'playwright-extra';
+import stealth from 'puppeteer-extra-plugin-stealth';
+
+chromium.use(stealth());
+
+async function stealthScrape(url: string) {
+ const browser = await chromium.launch({ headless: true });
+
+ const context = await browser.newContext({
+ // Realistische Browser-Einstellungen
+ userAgent: getRandomUserAgent(),
+ viewport: getRandomViewport(),
+ locale: 'de-DE',
+ timezoneId: 'Europe/Berlin',
+ geolocation: { latitude: 52.52, longitude: 13.405 },
+ permissions: ['geolocation'],
+
+ // WebGL Fingerprint
+ deviceScaleFactor: 1,
+ hasTouch: false,
+ isMobile: false
+ });
+
+ // WebDriver Flag entfernen
+ await context.addInitScript(() => {
+ Object.defineProperty(navigator, 'webdriver', {
+ get: () => undefined
+ });
+
+ // Chrome-spezifische Properties
+ Object.defineProperty(navigator, 'plugins', {
+ get: () => [1, 2, 3, 4, 5]
+ });
+
+ Object.defineProperty(navigator, 'languages', {
+ get: () => ['de-DE', 'de', 'en-US', 'en']
+ });
+
+ // Automation Detection Override
+ delete (window as any).cdc_adoQpoasnfa76pfcZLmcfl_Array;
+ delete (window as any).cdc_adoQpoasnfa76pfcZLmcfl_Promise;
+ delete (window as any).cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
+ });
+
+ const page = await context.newPage();
+ await page.goto(url);
+
+ return { page, browser, context };
+}
+
+// Random User Agents
+function getRandomUserAgent(): string {
+ const userAgents = [
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'
+ ];
+ return userAgents[Math.floor(Math.random() * userAgents.length)];
+}
+
+function getRandomViewport() {
+ const viewports = [
+ { width: 1920, height: 1080 },
+ { width: 1366, height: 768 },
+ { width: 1536, height: 864 },
+ { width: 1440, height: 900 }
+ ];
+ return viewports[Math.floor(Math.random() * viewports.length)];
+}
+```
+
+---
+
+## Network Interception
+
+```typescript
+// Ressourcen blockieren für Speed
+async function fastScrape(url: string) {
+ const browser = await chromium.launch();
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ // Unnötige Ressourcen blockieren
+ await page.route('**/*', (route) => {
+ const resourceType = route.request().resourceType();
+
+ if (['image', 'stylesheet', 'font', 'media'].includes(resourceType)) {
+ return route.abort();
+ }
+
+ // Tracking Scripts blockieren
+ const url = route.request().url();
+ if (
+ url.includes('analytics') ||
+ url.includes('tracking') ||
+ url.includes('ads')
+ ) {
+ return route.abort();
+ }
+
+ return route.continue();
+ });
+
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
+ return page;
+}
+
+// API Responses abfangen
+async function interceptApi(page: Page) {
+ const apiResponses: any[] = [];
+
+ page.on('response', async (response) => {
+ const url = response.url();
+
+ if (url.includes('/api/') && response.ok()) {
+ try {
+ const json = await response.json();
+ apiResponses.push({
+ url,
+ data: json
+ });
+ } catch {}
+ }
+ });
+
+ return apiResponses;
+}
+
+// Request modifizieren
+await page.route('**/api/**', (route) => {
+ const headers = {
+ ...route.request().headers(),
+ 'Authorization': 'Bearer token123',
+ 'X-Custom-Header': 'value'
+ };
+
+ route.continue({ headers });
+});
+```
+
+---
+
+## Selektoren & Datenextraktion
+
+```typescript
+// Moderne Selektoren
+async function extractData(page: Page) {
+ // CSS Selektoren
+ const title = await page.locator('h1').textContent();
+
+ // Text-basierte Selektoren
+ const loginButton = page.getByRole('button', { name: 'Login' });
+ const emailInput = page.getByLabel('E-Mail');
+ const link = page.getByText('Mehr erfahren');
+
+ // Multiple Elemente
+ const prices = await page.locator('.price').allTextContents();
+
+ // Attribute extrahieren
+ const links = await page.locator('a').evaluateAll(
+ (elements) => elements.map(el => ({
+ href: el.getAttribute('href'),
+ text: el.textContent?.trim()
+ }))
+ );
+
+ // Tabellen scrapen
+ const tableData = await page.evaluate(() => {
+ const rows = document.querySelectorAll('table tbody tr');
+ return Array.from(rows).map(row => {
+ const cells = row.querySelectorAll('td');
+ return Array.from(cells).map(cell => cell.textContent?.trim());
+ });
+ });
+
+ // Strukturierte Daten (JSON-LD)
+ const jsonLd = await page.evaluate(() => {
+ const script = document.querySelector('script[type="application/ld+json"]');
+ return script ? JSON.parse(script.textContent || '{}') : null;
+ });
+
+ return { title, prices, links, tableData, jsonLd };
+}
+
+// Warten auf dynamischen Content
+async function waitForContent(page: Page) {
+ // Auf Element warten
+ await page.waitForSelector('.product-list', { state: 'visible' });
+
+ // Auf Network Idle
+ await page.waitForLoadState('networkidle');
+
+ // Auf bestimmte Anzahl Elemente
+ await page.locator('.product-card').first().waitFor();
+
+ // Custom Condition
+ await page.waitForFunction(() => {
+ return document.querySelectorAll('.product-card').length >= 10;
+ });
+}
+```
+
+---
+
+## Pagination & Infinite Scroll
+
+```typescript
+// Pagination
+async function scrapePaginated(baseUrl: string, maxPages: number = 10) {
+ const browser = await chromium.launch();
+ const page = await browser.newPage();
+
+ const allData: any[] = [];
+
+ for (let i = 1; i <= maxPages; i++) {
+ await page.goto(`${baseUrl}?page=${i}`);
+
+ const pageData = await page.evaluate(() => {
+ return Array.from(document.querySelectorAll('.item')).map(el => ({
+ title: el.querySelector('.title')?.textContent,
+ price: el.querySelector('.price')?.textContent
+ }));
+ });
+
+ if (pageData.length === 0) break; // Keine Daten mehr
+
+ allData.push(...pageData);
+
+ // Rate Limiting
+ await page.waitForTimeout(1000 + Math.random() * 2000);
+ }
+
+ await browser.close();
+ return allData;
+}
+
+// Infinite Scroll
+async function scrapeInfiniteScroll(url: string, maxScrolls: number = 20) {
+ const browser = await chromium.launch();
+ const page = await browser.newPage();
+ await page.goto(url);
+
+ let previousHeight = 0;
+ let scrollCount = 0;
+
+ while (scrollCount < maxScrolls) {
+ // Scroll to bottom
+ await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
+
+ // Auf neue Inhalte warten
+ await page.waitForTimeout(2000);
+
+ const currentHeight = await page.evaluate(() => document.body.scrollHeight);
+
+ if (currentHeight === previousHeight) {
+ break; // Keine neuen Inhalte
+ }
+
+ previousHeight = currentHeight;
+ scrollCount++;
+ }
+
+ // Alle Daten extrahieren
+ const data = await page.evaluate(() => {
+ return Array.from(document.querySelectorAll('.item')).map(/* ... */);
+ });
+
+ await browser.close();
+ return data;
+}
+```
+
+---
+
+## Proxy & Session Management
+
+```typescript
+// Proxy Setup
+const browser = await chromium.launch({
+ proxy: {
+ server: 'http://proxy.example.com:8080',
+ username: 'user',
+ password: 'pass'
+ }
+});
+
+// Rotating Proxies
+async function withRotatingProxy(urls: string[]) {
+ const proxies = [
+ 'http://proxy1.example.com:8080',
+ 'http://proxy2.example.com:8080',
+ 'http://proxy3.example.com:8080'
+ ];
+
+ for (const url of urls) {
+ const proxy = proxies[Math.floor(Math.random() * proxies.length)];
+
+ const browser = await chromium.launch({
+ proxy: { server: proxy }
+ });
+
+ try {
+ const page = await browser.newPage();
+ await page.goto(url);
+ // Scrape...
+ } finally {
+ await browser.close();
+ }
+ }
+}
+
+// Session/Cookie Persistence
+async function persistSession() {
+ // Session speichern
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ await page.goto('https://example.com/login');
+ // Login durchführen...
+
+ // Cookies speichern
+ const cookies = await context.cookies();
+ await fs.writeFile('cookies.json', JSON.stringify(cookies));
+
+ // Session wiederherstellen
+ const newContext = await browser.newContext();
+ const savedCookies = JSON.parse(await fs.readFile('cookies.json', 'utf8'));
+ await newContext.addCookies(savedCookies);
+}
+```
+
+---
+
+## Parallel Scraping
+
+```typescript
+import { chromium, Browser } from 'playwright';
+import pLimit from 'p-limit';
+
+async function parallelScrape(urls: string[], concurrency: number = 5) {
+ const browser = await chromium.launch();
+ const limit = pLimit(concurrency);
+
+ const results = await Promise.all(
+ urls.map(url =>
+ limit(async () => {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+
+ try {
+ await page.goto(url, { timeout: 30000 });
+ const data = await extractData(page);
+ return { url, data, success: true };
+ } catch (error) {
+ return { url, error: (error as Error).message, success: false };
+ } finally {
+ await context.close();
+ }
+ })
+ )
+ );
+
+ await browser.close();
+ return results;
+}
+```
+
+---
+
+## Fazit
+
+Playwright Web Scraping 2026:
+
+1. **Stealth First**: Anti-Detection von Anfang an
+2. **Performance**: Resource Blocking, Parallel Scraping
+3. **Robustheit**: Auto-Waiting, Retries, Error Handling
+4. **Compliance**: Robots.txt respektieren, Rate Limiting
+
+Immer rechtliche Aspekte und Terms of Service beachten.
+
+---
+
+## Bildprompts
+
+1. "Spider crawling through web pages extracting data, web scraping concept"
+2. "Browser automation with invisible robot, stealth scraping"
+3. "Multiple parallel processes scraping different websites, concurrent extraction"
+
+---
+
+## Quellen
+
+- [Playwright Documentation](https://playwright.dev/)
+- [Playwright Web Scraping 2026](https://brightdata.com/blog/how-tos/playwright-web-scraping)
+- [Playwright Stealth](https://www.zenrows.com/blog/playwright-stealth)
+- [Browserless Scraping Guide](https://www.browserless.io/blog/scraping-with-playwright-a-developer-s-guide-to-scalable-undetectable-data-extraction)
diff --git a/blog-posts/52-puppeteer-browser-automation.md b/blog-posts/52-puppeteer-browser-automation.md
new file mode 100644
index 0000000..b031f6e
--- /dev/null
+++ b/blog-posts/52-puppeteer-browser-automation.md
@@ -0,0 +1,479 @@
+# Puppeteer: Browser Automation mit Chrome DevTools Protocol
+
+**Meta-Description:** Puppeteer für Browser Automation. Chrome DevTools Protocol, Headless Mode, Screenshots, PDF Generation und Testing.
+
+**Keywords:** Puppeteer, Browser Automation, Chrome DevTools Protocol, Headless Chrome, Screenshot, PDF, Web Testing
+
+---
+
+## Einführung
+
+Puppeteer ist die **offizielle Node.js Library von Google** für Chrome/Firefox Automation. Über das Chrome DevTools Protocol (CDP) bietet es direkten Zugang zu Browser-Funktionen, die über normale APIs nicht erreichbar sind.
+
+---
+
+## 2026 Updates
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ PUPPETEER 2026 │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Neue Features: │
+│ ├── WebDriver BiDi Protocol Support │
+│ ├── Firefox vollständig unterstützt │
+│ ├── Neuer Headless Mode (schwerer zu detecten) │
+│ ├── Verbesserte Locator API │
+│ └── 15-20% weniger Memory Usage │
+│ │
+│ DevTools Protocol: │
+│ ├── Network Interception │
+│ ├── Performance Metrics │
+│ ├── Coverage Reports │
+│ ├── Console Logs │
+│ └── Security/Certificate Handling │
+│ │
+│ Use Cases: │
+│ ├── Web Scraping │
+│ ├── Automated Testing │
+│ ├── PDF Generation │
+│ ├── Screenshot Services │
+│ └── Performance Auditing │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Setup
+
+```bash
+# Mit Chrome Download
+npm install puppeteer
+
+# Ohne Chrome (für eigene Installation)
+npm install puppeteer-core
+```
+
+```typescript
+// basic.ts
+import puppeteer from 'puppeteer';
+
+async function main() {
+ const browser = await puppeteer.launch({
+ headless: true, // 'new' Headless Mode ist default
+ args: [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ '--disable-dev-shm-usage'
+ ]
+ });
+
+ const page = await browser.newPage();
+ await page.setViewport({ width: 1920, height: 1080 });
+
+ await page.goto('https://example.com');
+
+ const title = await page.title();
+ console.log('Title:', title);
+
+ await browser.close();
+}
+```
+
+---
+
+## Screenshots & PDFs
+
+```typescript
+// Screenshot
+async function captureScreenshot(url: string, outputPath: string) {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+
+ await page.setViewport({ width: 1920, height: 1080 });
+ await page.goto(url, { waitUntil: 'networkidle0' });
+
+ // Full Page Screenshot
+ await page.screenshot({
+ path: outputPath,
+ fullPage: true,
+ type: 'png'
+ });
+
+ // Specific Element
+ const element = await page.$('.hero-section');
+ if (element) {
+ await element.screenshot({ path: 'hero.png' });
+ }
+
+ // Mit Clip (Ausschnitt)
+ await page.screenshot({
+ path: 'clip.png',
+ clip: {
+ x: 0,
+ y: 0,
+ width: 800,
+ height: 600
+ }
+ });
+
+ await browser.close();
+}
+
+// PDF Generation
+async function generatePDF(url: string, outputPath: string) {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+
+ await page.goto(url, { waitUntil: 'networkidle0' });
+
+ // Print-Styles laden
+ await page.emulateMediaType('print');
+
+ await page.pdf({
+ path: outputPath,
+ format: 'A4',
+ printBackground: true,
+ margin: {
+ top: '20mm',
+ right: '20mm',
+ bottom: '20mm',
+ left: '20mm'
+ },
+ displayHeaderFooter: true,
+ headerTemplate: 'Header
',
+ footerTemplate: '/
'
+ });
+
+ await browser.close();
+}
+
+// Invoice PDF aus HTML
+async function generateInvoicePDF(html: string): Promise {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+
+ await page.setContent(html, { waitUntil: 'networkidle0' });
+
+ const pdf = await page.pdf({
+ format: 'A4',
+ printBackground: true
+ });
+
+ await browser.close();
+ return pdf;
+}
+```
+
+---
+
+## Form Handling & Interaction
+
+```typescript
+async function fillAndSubmitForm(page: Page) {
+ // Text Input
+ await page.type('#email', 'user@example.com', { delay: 50 });
+ await page.type('#password', 'secretpassword', { delay: 50 });
+
+ // Checkbox
+ await page.click('#terms');
+
+ // Select Dropdown
+ await page.select('#country', 'DE');
+
+ // Radio Button
+ await page.click('input[name="plan"][value="premium"]');
+
+ // File Upload
+ const fileInput = await page.$('input[type="file"]');
+ await fileInput?.uploadFile('./document.pdf');
+
+ // Submit
+ await Promise.all([
+ page.waitForNavigation(),
+ page.click('button[type="submit"]')
+ ]);
+}
+
+// Keyboard & Mouse
+async function advancedInteraction(page: Page) {
+ // Keyboard
+ await page.keyboard.press('Tab');
+ await page.keyboard.type('Hello World');
+ await page.keyboard.down('Shift');
+ await page.keyboard.press('ArrowLeft');
+ await page.keyboard.up('Shift');
+ await page.keyboard.press('Backspace');
+
+ // Mouse
+ await page.mouse.move(100, 200);
+ await page.mouse.click(100, 200);
+ await page.mouse.wheel({ deltaY: 500 });
+
+ // Drag & Drop
+ const source = await page.$('#drag-source');
+ const target = await page.$('#drop-target');
+
+ if (source && target) {
+ const sourceBox = await source.boundingBox();
+ const targetBox = await target.boundingBox();
+
+ if (sourceBox && targetBox) {
+ await page.mouse.move(
+ sourceBox.x + sourceBox.width / 2,
+ sourceBox.y + sourceBox.height / 2
+ );
+ await page.mouse.down();
+ await page.mouse.move(
+ targetBox.x + targetBox.width / 2,
+ targetBox.y + targetBox.height / 2
+ );
+ await page.mouse.up();
+ }
+ }
+}
+```
+
+---
+
+## Network Interception
+
+```typescript
+import { Page, HTTPRequest } from 'puppeteer';
+
+async function interceptNetwork(page: Page) {
+ await page.setRequestInterception(true);
+
+ page.on('request', (request: HTTPRequest) => {
+ const resourceType = request.resourceType();
+ const url = request.url();
+
+ // Block Images, CSS, Fonts
+ if (['image', 'stylesheet', 'font'].includes(resourceType)) {
+ request.abort();
+ return;
+ }
+
+ // Block Tracking
+ if (url.includes('analytics') || url.includes('tracking')) {
+ request.abort();
+ return;
+ }
+
+ // Modify Headers
+ const headers = {
+ ...request.headers(),
+ 'X-Custom-Header': 'value'
+ };
+
+ request.continue({ headers });
+ });
+
+ // Response Interception
+ page.on('response', async (response) => {
+ if (response.url().includes('/api/')) {
+ try {
+ const json = await response.json();
+ console.log('API Response:', json);
+ } catch {}
+ }
+ });
+}
+
+// Mock API Responses
+async function mockApi(page: Page) {
+ await page.setRequestInterception(true);
+
+ page.on('request', (request) => {
+ if (request.url().includes('/api/users')) {
+ request.respond({
+ status: 200,
+ contentType: 'application/json',
+ body: JSON.stringify([
+ { id: 1, name: 'Mock User' }
+ ])
+ });
+ } else {
+ request.continue();
+ }
+ });
+}
+```
+
+---
+
+## Chrome DevTools Protocol Direct Access
+
+```typescript
+async function cdpAccess(page: Page) {
+ const client = await page.target().createCDPSession();
+
+ // Network Conditions (Throttling)
+ await client.send('Network.emulateNetworkConditions', {
+ offline: false,
+ downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps
+ uploadThroughput: 750 * 1024 / 8, // 750 Kbps
+ latency: 40 // 40ms
+ });
+
+ // CPU Throttling
+ await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
+
+ // Performance Metrics
+ await client.send('Performance.enable');
+ const metrics = await client.send('Performance.getMetrics');
+ console.log('Performance Metrics:', metrics);
+
+ // Coverage (CSS/JS Usage)
+ await client.send('CSS.startRuleUsageTracking');
+ // ... navigate and interact
+ const coverage = await client.send('CSS.stopRuleUsageTracking');
+ console.log('CSS Coverage:', coverage);
+
+ // Clear Browser Data
+ await client.send('Network.clearBrowserCache');
+ await client.send('Network.clearBrowserCookies');
+
+ // Console Messages
+ client.on('Runtime.consoleAPICalled', (event) => {
+ console.log('Console:', event.type, event.args);
+ });
+}
+```
+
+---
+
+## Performance Testing
+
+```typescript
+async function measurePerformance(url: string) {
+ const browser = await puppeteer.launch();
+ const page = await browser.newPage();
+
+ // Performance Observer
+ await page.evaluateOnNewDocument(() => {
+ window.performanceEntries = [];
+
+ const observer = new PerformanceObserver((list) => {
+ window.performanceEntries.push(...list.getEntries());
+ });
+
+ observer.observe({ entryTypes: ['navigation', 'resource', 'paint', 'largest-contentful-paint'] });
+ });
+
+ await page.goto(url, { waitUntil: 'networkidle0' });
+
+ // Metrics abrufen
+ const metrics = await page.metrics();
+ console.log('Puppeteer Metrics:', {
+ JSHeapUsedSize: metrics.JSHeapUsedSize,
+ LayoutCount: metrics.LayoutCount,
+ RecalcStyleCount: metrics.RecalcStyleCount
+ });
+
+ // Performance Timing
+ const timing = await page.evaluate(() => {
+ const t = performance.timing;
+ return {
+ dns: t.domainLookupEnd - t.domainLookupStart,
+ tcp: t.connectEnd - t.connectStart,
+ ttfb: t.responseStart - t.requestStart,
+ download: t.responseEnd - t.responseStart,
+ domInteractive: t.domInteractive - t.navigationStart,
+ domComplete: t.domComplete - t.navigationStart,
+ load: t.loadEventEnd - t.navigationStart
+ };
+ });
+
+ console.log('Timing:', timing);
+
+ // Core Web Vitals
+ const cwv = await page.evaluate(() => {
+ return (window as any).performanceEntries.filter(
+ e => ['largest-contentful-paint', 'first-input', 'layout-shift'].includes(e.entryType)
+ );
+ });
+
+ await browser.close();
+ return { metrics, timing, cwv };
+}
+```
+
+---
+
+## Stealth Mode
+
+```typescript
+import puppeteer from 'puppeteer-extra';
+import StealthPlugin from 'puppeteer-extra-plugin-stealth';
+
+puppeteer.use(StealthPlugin());
+
+async function stealthBrowse() {
+ const browser = await puppeteer.launch({
+ headless: true,
+ args: [
+ '--disable-blink-features=AutomationControlled',
+ '--no-sandbox'
+ ]
+ });
+
+ const page = await browser.newPage();
+
+ // Additional Stealth
+ await page.evaluateOnNewDocument(() => {
+ // Chrome Runtime
+ Object.defineProperty(navigator, 'webdriver', {
+ get: () => false
+ });
+
+ // Permissions
+ const originalQuery = window.navigator.permissions.query;
+ window.navigator.permissions.query = (parameters: any) =>
+ parameters.name === 'notifications'
+ ? Promise.resolve({ state: 'denied' } as PermissionStatus)
+ : originalQuery(parameters);
+
+ // Plugins
+ Object.defineProperty(navigator, 'plugins', {
+ get: () => [1, 2, 3, 4, 5]
+ });
+ });
+
+ await page.goto('https://bot.sannysoft.com');
+ await page.screenshot({ path: 'stealth-test.png', fullPage: true });
+
+ await browser.close();
+}
+```
+
+---
+
+## Fazit
+
+Puppeteer 2026 bietet:
+
+1. **CDP Direct Access**: Volle Browser-Kontrolle
+2. **Multi-Browser**: Chrome + Firefox (WebDriver BiDi)
+3. **New Headless**: Schwerer zu detecten
+4. **Performance Tools**: Metrics, Coverage, Auditing
+
+Ideal für Testing, PDF-Generation und Chrome-spezifische Features.
+
+---
+
+## Bildprompts
+
+1. "Puppet master controlling browser strings, automation concept"
+2. "Chrome DevTools with code connections, developer tools visualization"
+3. "PDF documents being generated from web pages, document automation"
+
+---
+
+## Quellen
+
+- [Puppeteer Documentation](https://pptr.dev/)
+- [Puppeteer GitHub](https://github.com/puppeteer/puppeteer)
+- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
+- [Puppeteer Web Scraping 2026](https://roundproxies.com/blog/puppeteer-web-scraping/)
diff --git a/blog-posts/53-cheerio-html-parsing.md b/blog-posts/53-cheerio-html-parsing.md
new file mode 100644
index 0000000..fd1d359
--- /dev/null
+++ b/blog-posts/53-cheerio-html-parsing.md
@@ -0,0 +1,538 @@
+# Cheerio: Schnelles HTML Parsing ohne Browser
+
+**Meta-Description:** Cheerio für effizientes Web Scraping. jQuery-Syntax für Server-Side HTML Parsing ohne Browser-Overhead.
+
+**Keywords:** Cheerio, HTML Parsing, Web Scraping, jQuery, Node.js, DOM Manipulation, Data Extraction
+
+---
+
+## Einführung
+
+Cheerio ist eine **ultraschnelle HTML Parsing Library** für Node.js. Mit jQuery-ähnlicher Syntax parst es HTML serverseitig – ohne Browser, ohne JavaScript-Rendering, nur pure Geschwindigkeit.
+
+---
+
+## Cheerio vs Browser-basierte Tools
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ CHEERIO CHARAKTERISTIKEN │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Vorteile: │
+│ ├── Extrem schnell (kein Browser-Overhead) │
+│ ├── Geringer Memory-Footprint │
+│ ├── Vertraute jQuery-Syntax │
+│ ├── Serverless-freundlich │
+│ └── Ideal für Static HTML │
+│ │
+│ Einschränkungen: │
+│ ├── Kein JavaScript-Rendering │
+│ ├── Keine dynamischen Inhalte │
+│ ├── Keine Browser-APIs │
+│ └── Kein Visual Rendering │
+│ │
+│ Use Cases: │
+│ ├── Static Website Scraping │
+│ ├── HTML Email Parsing │
+│ ├── RSS/XML Feed Processing │
+│ ├── HTML Sanitization │
+│ └── Content Extraction │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Setup
+
+```bash
+npm install cheerio axios
+```
+
+```typescript
+// scraper.ts
+import * as cheerio from 'cheerio';
+import axios from 'axios';
+
+async function scrape(url: string) {
+ // HTML fetchen
+ const response = await axios.get(url, {
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ });
+
+ // Cheerio laden
+ const $ = cheerio.load(response.data);
+
+ // jQuery-Syntax für Selektion
+ const title = $('h1').text();
+ const description = $('meta[name="description"]').attr('content');
+
+ return { title, description };
+}
+```
+
+---
+
+## Selektoren & Navigation
+
+```typescript
+import * as cheerio from 'cheerio';
+
+const html = `
+
+
+
+
Title
+
Introduction text
+
+ - Item 1
+ - Item 2
+ - Item 3
+
+
Link 1
+
Link 2
+
+
+
+`;
+
+const $ = cheerio.load(html);
+
+// Basis Selektoren
+const title = $('h1').text(); // "Title"
+const intro = $('.intro').text(); // "Introduction text"
+const firstItem = $('#items li').first().text(); // "Item 1"
+
+// Attribute
+const href = $('a').attr('href'); // "/page1"
+const dataId = $('li').first().data('id'); // 1
+
+// Traversierung
+const items = $('li').map((i, el) => $(el).text()).get();
+// ["Item 1", "Item 2", "Item 3"]
+
+// Parent/Child Navigation
+const parent = $('li').first().parent().attr('id'); // "items"
+const children = $('#items').children().length; // 3
+const siblings = $('li').first().siblings().length; // 2
+
+// Filtering
+const externalLinks = $('a.external').map((i, el) => $(el).attr('href')).get();
+
+// Contains
+const itemWith2 = $('li:contains("2")').text(); // "Item 2"
+
+// Multiple Selectors
+const headings = $('h1, h2, h3').map((i, el) => $(el).text()).get();
+```
+
+---
+
+## Datenextraktion
+
+```typescript
+import * as cheerio from 'cheerio';
+import axios from 'axios';
+
+interface Product {
+ name: string;
+ price: number;
+ description: string;
+ image: string;
+ url: string;
+}
+
+async function scrapeProducts(url: string): Promise {
+ const { data: html } = await axios.get(url);
+ const $ = cheerio.load(html);
+
+ const products: Product[] = [];
+
+ $('.product-card').each((index, element) => {
+ const $el = $(element);
+
+ products.push({
+ name: $el.find('.product-name').text().trim(),
+ price: parseFloat(
+ $el.find('.price')
+ .text()
+ .replace('€', '')
+ .replace(',', '.')
+ .trim()
+ ),
+ description: $el.find('.description').text().trim(),
+ image: $el.find('img').attr('src') || '',
+ url: $el.find('a.product-link').attr('href') || ''
+ });
+ });
+
+ return products;
+}
+
+// Tabellen extrahieren
+function extractTable($: cheerio.CheerioAPI, selector: string) {
+ const headers: string[] = [];
+ const rows: Record[] = [];
+
+ // Header extrahieren
+ $(`${selector} thead th`).each((i, el) => {
+ headers.push($(el).text().trim());
+ });
+
+ // Rows extrahieren
+ $(`${selector} tbody tr`).each((i, row) => {
+ const rowData: Record = {};
+
+ $(row).find('td').each((j, cell) => {
+ const header = headers[j] || `col${j}`;
+ rowData[header] = $(cell).text().trim();
+ });
+
+ rows.push(rowData);
+ });
+
+ return rows;
+}
+
+// Strukturierte Daten (JSON-LD)
+function extractJsonLd($: cheerio.CheerioAPI) {
+ const scripts = $('script[type="application/ld+json"]');
+ const data: any[] = [];
+
+ scripts.each((i, el) => {
+ try {
+ const content = $(el).html();
+ if (content) {
+ data.push(JSON.parse(content));
+ }
+ } catch {}
+ });
+
+ return data;
+}
+```
+
+---
+
+## HTML Manipulation
+
+```typescript
+import * as cheerio from 'cheerio';
+
+const $ = cheerio.load('');
+
+// Text ändern
+$('p').text('Hello World');
+
+// HTML ändern
+$('div').html('New Content');
+
+// Elemente hinzufügen
+$('div').append('Appended
');
+$('div').prepend('Prepended
');
+$('p').after('After');
+$('p').before('Before');
+
+// Attribute setzen
+$('div').attr('id', 'container');
+$('div').addClass('active');
+$('div').removeClass('hidden');
+
+// Elemente entfernen
+$('.ads').remove();
+$('.tracking').empty();
+
+// Wrap
+$('p').wrap('');
+
+// Clone
+const cloned = $('p').clone();
+$('div').append(cloned);
+
+// Output
+const finalHtml = $.html();
+const bodyOnly = $('body').html();
+```
+
+---
+
+## HTML Sanitization
+
+```typescript
+import * as cheerio from 'cheerio';
+
+function sanitizeHtml(dirtyHtml: string): string {
+ const $ = cheerio.load(dirtyHtml);
+
+ // Scripts entfernen
+ $('script').remove();
+
+ // Event Handler entfernen
+ $('*').each((i, el) => {
+ const element = $(el);
+ const attrs = (el as any).attribs || {};
+
+ Object.keys(attrs).forEach(attr => {
+ if (attr.startsWith('on')) {
+ element.removeAttr(attr);
+ }
+ });
+ });
+
+ // Gefährliche Attribute entfernen
+ $('a[href^="javascript:"]').removeAttr('href');
+ $('*[style*="expression"]').removeAttr('style');
+
+ // Iframe entfernen
+ $('iframe').remove();
+
+ // Erlaubte Tags whitelist
+ const allowedTags = ['p', 'a', 'b', 'i', 'u', 'ul', 'ol', 'li', 'br', 'h1', 'h2', 'h3'];
+
+ $('*').each((i, el) => {
+ const tagName = (el as any).tagName?.toLowerCase();
+ if (tagName && !allowedTags.includes(tagName) && tagName !== 'html' && tagName !== 'body') {
+ $(el).replaceWith($(el).html() || '');
+ }
+ });
+
+ return $('body').html() || '';
+}
+
+// Email-safe HTML
+function makeEmailSafe(html: string): string {
+ const $ = cheerio.load(html);
+
+ // Relative URLs zu Absoluten
+ $('img[src^="/"]').each((i, el) => {
+ const src = $(el).attr('src');
+ $(el).attr('src', `https://example.com${src}`);
+ });
+
+ // CSS Inlinen (vereinfacht)
+ $('*[class]').each((i, el) => {
+ const element = $(el);
+ const classes = element.attr('class');
+ // Hier würde man CSS Styles inlinen
+ element.removeAttr('class');
+ });
+
+ return $.html();
+}
+```
+
+---
+
+## Pagination & Crawling
+
+```typescript
+import * as cheerio from 'cheerio';
+import axios from 'axios';
+
+interface CrawlResult {
+ url: string;
+ title: string;
+ links: string[];
+}
+
+async function crawlWithPagination(startUrl: string, maxPages: number = 10) {
+ const visited = new Set();
+ const results: CrawlResult[] = [];
+ let currentUrl: string | null = startUrl;
+ let pageCount = 0;
+
+ while (currentUrl && pageCount < maxPages) {
+ if (visited.has(currentUrl)) break;
+ visited.add(currentUrl);
+
+ try {
+ const { data: html } = await axios.get(currentUrl, {
+ headers: { 'User-Agent': 'Mozilla/5.0' },
+ timeout: 10000
+ });
+
+ const $ = cheerio.load(html);
+
+ results.push({
+ url: currentUrl,
+ title: $('title').text(),
+ links: $('a[href]')
+ .map((i, el) => $(el).attr('href'))
+ .get()
+ .filter(href => href && !href.startsWith('#'))
+ });
+
+ // Nächste Seite finden
+ const nextLink = $('a.next, a[rel="next"], .pagination a:last-child')
+ .attr('href');
+
+ currentUrl = nextLink ? new URL(nextLink, currentUrl).toString() : null;
+ pageCount++;
+
+ // Rate Limiting
+ await new Promise(r => setTimeout(r, 1000));
+ } catch (error) {
+ console.error(`Error crawling ${currentUrl}:`, error);
+ break;
+ }
+ }
+
+ return results;
+}
+
+// Sitemap Parsing
+async function parseSitemap(sitemapUrl: string): Promise {
+ const { data: xml } = await axios.get(sitemapUrl);
+ const $ = cheerio.load(xml, { xmlMode: true });
+
+ const urls: string[] = [];
+
+ // Standard Sitemap
+ $('url loc').each((i, el) => {
+ urls.push($(el).text());
+ });
+
+ // Sitemap Index
+ $('sitemap loc').each((i, el) => {
+ urls.push($(el).text());
+ });
+
+ return urls;
+}
+```
+
+---
+
+## RSS/Atom Feed Parsing
+
+```typescript
+import * as cheerio from 'cheerio';
+import axios from 'axios';
+
+interface FeedItem {
+ title: string;
+ link: string;
+ description: string;
+ pubDate: Date;
+ author?: string;
+}
+
+async function parseRssFeed(feedUrl: string): Promise {
+ const { data: xml } = await axios.get(feedUrl);
+ const $ = cheerio.load(xml, { xmlMode: true });
+
+ const items: FeedItem[] = [];
+
+ // RSS 2.0
+ $('item').each((i, el) => {
+ const $item = $(el);
+ items.push({
+ title: $item.find('title').text(),
+ link: $item.find('link').text(),
+ description: $item.find('description').text(),
+ pubDate: new Date($item.find('pubDate').text()),
+ author: $item.find('author, dc\\:creator').text() || undefined
+ });
+ });
+
+ // Atom
+ $('entry').each((i, el) => {
+ const $entry = $(el);
+ items.push({
+ title: $entry.find('title').text(),
+ link: $entry.find('link').attr('href') || '',
+ description: $entry.find('summary, content').text(),
+ pubDate: new Date($entry.find('updated, published').text()),
+ author: $entry.find('author name').text() || undefined
+ });
+ });
+
+ return items;
+}
+```
+
+---
+
+## Performance-Optimierung
+
+```typescript
+import * as cheerio from 'cheerio';
+
+// Lazy Loading für große Dokumente
+function processLargeHtml(html: string) {
+ const $ = cheerio.load(html, {
+ // Nur parsen was nötig ist
+ xml: false,
+ decodeEntities: true
+ });
+
+ // Selektiv arbeiten
+ const relevantSection = $('#main-content').html();
+
+ if (relevantSection) {
+ const $section = cheerio.load(relevantSection);
+ // Weiterverarbeiten...
+ }
+}
+
+// Batch Processing
+async function batchScrape(urls: string[], concurrency: number = 5) {
+ const results: any[] = [];
+
+ for (let i = 0; i < urls.length; i += concurrency) {
+ const batch = urls.slice(i, i + concurrency);
+
+ const batchResults = await Promise.all(
+ batch.map(async (url) => {
+ const { data } = await axios.get(url);
+ const $ = cheerio.load(data);
+
+ return {
+ url,
+ title: $('title').text()
+ };
+ })
+ );
+
+ results.push(...batchResults);
+
+ // Rate Limiting zwischen Batches
+ await new Promise(r => setTimeout(r, 1000));
+ }
+
+ return results;
+}
+```
+
+---
+
+## Fazit
+
+Cheerio ist ideal für:
+
+1. **Static Content**: HTML ohne JavaScript-Rendering
+2. **Speed**: Kein Browser-Overhead
+3. **Serverless**: Geringer Memory-Footprint
+4. **Vertrautheit**: jQuery-Syntax
+
+Für dynamische Seiten kombiniere mit Playwright/Puppeteer.
+
+---
+
+## Bildprompts
+
+1. "HTML document being parsed into structured data, document processing"
+2. "jQuery selector finding elements in page structure, DOM navigation"
+3. "Fast processing of multiple HTML pages, batch extraction"
+
+---
+
+## Quellen
+
+- [Cheerio Documentation](https://cheerio.js.org/)
+- [Cheerio npm](https://www.npmjs.com/package/cheerio)
+- [Web Scraping with Cheerio 2026](https://www.capsolver.com/blog/The%20other%20captcha/web-scraping-with-cheerio)
+- [Cheerio Tutorial](https://zetcode.com/javascript/cheerio/)
diff --git a/blog-posts/54-anti-bot-detection.md b/blog-posts/54-anti-bot-detection.md
new file mode 100644
index 0000000..9f83fd9
--- /dev/null
+++ b/blog-posts/54-anti-bot-detection.md
@@ -0,0 +1,513 @@
+# Anti-Bot Detection: Scraping ohne Blockaden
+
+**Meta-Description:** Anti-Bot Detection Techniken überwinden. Fingerprinting, CAPTCHAs, Rate Limiting und Stealth-Strategien für Web Scraping.
+
+**Keywords:** Anti-Bot Detection, Bot Detection, CAPTCHA Bypass, Browser Fingerprinting, Web Scraping, Stealth, Proxy Rotation
+
+---
+
+## Einführung
+
+Moderne Websites setzen ausgefeilte **Anti-Bot-Systeme** ein: Cloudflare, Akamai, PerimeterX, DataDome. Erfolgreiches Scraping erfordert Wissen über Detection-Mechanismen und ethische Umgehungsstrategien.
+
+---
+
+## Detection-Mechanismen
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ BOT DETECTION LAYERS │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ Layer 1: IP-basiert │
+│ ├── Datacenter IP Ranges (bekannt) │
+│ ├── Request Rate pro IP │
+│ ├── Geografische Inkonsistenzen │
+│ └── IP Reputation Databases │
+│ │
+│ Layer 2: Browser Fingerprinting │
+│ ├── navigator.webdriver │
+│ ├── Chrome Automation Extensions │
+│ ├── WebGL Fingerprint │
+│ ├── Canvas Fingerprint │
+│ ├── Audio Context Fingerprint │
+│ └── Plugin/Font Enumeration │
+│ │
+│ Layer 3: Behavioral Analysis │
+│ ├── Mouse Movement Patterns │
+│ ├── Scroll Behavior │
+│ ├── Click Timing │
+│ ├── Keystroke Dynamics │
+│ └── Session Duration │
+│ │
+│ Layer 4: JavaScript Challenges │
+│ ├── CAPTCHAs (reCAPTCHA, hCaptcha) │
+│ ├── JavaScript Execution Tests │
+│ ├── Cookie/LocalStorage Checks │
+│ └── TLS Fingerprinting │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Browser Fingerprinting umgehen
+
+```typescript
+import { chromium } from 'playwright';
+
+async function createStealthBrowser() {
+ const browser = await chromium.launch({
+ headless: true,
+ args: [
+ '--disable-blink-features=AutomationControlled',
+ '--disable-features=IsolateOrigins,site-per-process',
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-accelerated-2d-canvas',
+ '--disable-gpu'
+ ]
+ });
+
+ const context = await browser.newContext({
+ userAgent: getRealisticUserAgent(),
+ viewport: getRealisticViewport(),
+ locale: 'de-DE',
+ timezoneId: 'Europe/Berlin',
+ geolocation: { latitude: 52.52, longitude: 13.405 },
+ permissions: ['geolocation'],
+ colorScheme: 'light',
+ deviceScaleFactor: 1
+ });
+
+ // Stealth Scripts
+ await context.addInitScript(() => {
+ // WebDriver Flag
+ Object.defineProperty(navigator, 'webdriver', {
+ get: () => undefined
+ });
+
+ // Chrome Automation
+ delete (window as any).cdc_adoQpoasnfa76pfcZLmcfl_Array;
+ delete (window as any).cdc_adoQpoasnfa76pfcZLmcfl_Promise;
+ delete (window as any).cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
+
+ // Languages
+ Object.defineProperty(navigator, 'languages', {
+ get: () => ['de-DE', 'de', 'en-US', 'en']
+ });
+
+ // Plugins (nicht leer)
+ Object.defineProperty(navigator, 'plugins', {
+ get: () => {
+ const plugins = [
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
+ { name: 'Native Client', filename: 'internal-nacl-plugin' }
+ ];
+ plugins.length = 3;
+ return plugins;
+ }
+ });
+
+ // Platform
+ Object.defineProperty(navigator, 'platform', {
+ get: () => 'Win32'
+ });
+
+ // Hardware Concurrency
+ Object.defineProperty(navigator, 'hardwareConcurrency', {
+ get: () => 8
+ });
+
+ // Device Memory
+ Object.defineProperty(navigator, 'deviceMemory', {
+ get: () => 8
+ });
+
+ // Permissions Query Override
+ const originalQuery = window.navigator.permissions.query;
+ window.navigator.permissions.query = (parameters: any) =>
+ parameters.name === 'notifications'
+ ? Promise.resolve({ state: 'prompt' } as PermissionStatus)
+ : originalQuery(parameters);
+
+ // WebGL Vendor/Renderer
+ const getParameter = WebGLRenderingContext.prototype.getParameter;
+ WebGLRenderingContext.prototype.getParameter = function(parameter) {
+ if (parameter === 37445) return 'Intel Inc.';
+ if (parameter === 37446) return 'Intel Iris OpenGL Engine';
+ return getParameter.call(this, parameter);
+ };
+ });
+
+ return { browser, context };
+}
+
+function getRealisticUserAgent(): string {
+ const userAgents = [
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0'
+ ];
+ return userAgents[Math.floor(Math.random() * userAgents.length)];
+}
+
+function getRealisticViewport() {
+ const viewports = [
+ { width: 1920, height: 1080 },
+ { width: 1366, height: 768 },
+ { width: 1536, height: 864 },
+ { width: 1440, height: 900 },
+ { width: 1280, height: 720 }
+ ];
+ return viewports[Math.floor(Math.random() * viewports.length)];
+}
+```
+
+---
+
+## Proxy Rotation
+
+```typescript
+interface Proxy {
+ server: string;
+ username?: string;
+ password?: string;
+}
+
+class ProxyRotator {
+ private proxies: Proxy[];
+ private currentIndex = 0;
+ private failedProxies = new Set();
+
+ constructor(proxies: Proxy[]) {
+ this.proxies = proxies;
+ }
+
+ getNext(): Proxy | null {
+ const startIndex = this.currentIndex;
+
+ do {
+ const proxy = this.proxies[this.currentIndex];
+ this.currentIndex = (this.currentIndex + 1) % this.proxies.length;
+
+ if (!this.failedProxies.has(proxy.server)) {
+ return proxy;
+ }
+ } while (this.currentIndex !== startIndex);
+
+ return null; // Alle Proxies fehlgeschlagen
+ }
+
+ markFailed(proxy: Proxy) {
+ this.failedProxies.add(proxy.server);
+ }
+
+ reset() {
+ this.failedProxies.clear();
+ }
+}
+
+// Verwendung
+async function scrapeWithProxy(url: string, proxyRotator: ProxyRotator) {
+ const proxy = proxyRotator.getNext();
+ if (!proxy) throw new Error('No available proxies');
+
+ try {
+ const browser = await chromium.launch({
+ proxy: {
+ server: proxy.server,
+ username: proxy.username,
+ password: proxy.password
+ }
+ });
+
+ const page = await browser.newPage();
+ await page.goto(url);
+
+ // Scraping Logic...
+
+ await browser.close();
+ } catch (error) {
+ proxyRotator.markFailed(proxy);
+ throw error;
+ }
+}
+
+// Residential Proxies (Empfohlen für aggressive Sites)
+const residentialProxies: Proxy[] = [
+ { server: 'http://residential.proxy.com:8080', username: 'user', password: 'pass' },
+ // Residential Proxies sind teurer, aber schwerer zu detecten
+];
+
+// Datacenter Proxies (Günstiger, aber leichter zu erkennen)
+const datacenterProxies: Proxy[] = [
+ { server: 'http://dc.proxy.com:8080' },
+];
+```
+
+---
+
+## Rate Limiting & Request Timing
+
+```typescript
+class RateLimiter {
+ private timestamps: number[] = [];
+ private maxRequests: number;
+ private windowMs: number;
+
+ constructor(maxRequests: number, windowMs: number) {
+ this.maxRequests = maxRequests;
+ this.windowMs = windowMs;
+ }
+
+ async waitForSlot(): Promise {
+ const now = Date.now();
+
+ // Alte Timestamps entfernen
+ this.timestamps = this.timestamps.filter(t => now - t < this.windowMs);
+
+ if (this.timestamps.length >= this.maxRequests) {
+ const oldestTimestamp = this.timestamps[0];
+ const waitTime = this.windowMs - (now - oldestTimestamp);
+
+ if (waitTime > 0) {
+ await new Promise(r => setTimeout(r, waitTime));
+ }
+ }
+
+ this.timestamps.push(Date.now());
+ }
+}
+
+// Human-like Delays
+function humanDelay(min: number = 1000, max: number = 3000): Promise {
+ const delay = min + Math.random() * (max - min);
+ return new Promise(r => setTimeout(r, delay));
+}
+
+// Exponential Backoff bei Errors
+async function withRetry(
+ fn: () => Promise,
+ maxRetries: number = 3,
+ baseDelay: number = 1000
+): Promise {
+ let lastError: Error;
+
+ for (let i = 0; i < maxRetries; i++) {
+ try {
+ return await fn();
+ } catch (error) {
+ lastError = error as Error;
+
+ if (i < maxRetries - 1) {
+ const delay = baseDelay * Math.pow(2, i) + Math.random() * 1000;
+ console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
+ await new Promise(r => setTimeout(r, delay));
+ }
+ }
+ }
+
+ throw lastError!;
+}
+```
+
+---
+
+## Human-like Behavior
+
+```typescript
+import { Page } from 'playwright';
+
+async function humanLikeBrowsing(page: Page) {
+ // Random Mouse Movement
+ await randomMouseMovement(page);
+
+ // Natural Scrolling
+ await naturalScroll(page);
+
+ // Realistic Click Behavior
+ await humanClick(page, 'button.submit');
+}
+
+async function randomMouseMovement(page: Page) {
+ const viewport = page.viewportSize()!;
+
+ for (let i = 0; i < 3; i++) {
+ const x = Math.floor(Math.random() * viewport.width);
+ const y = Math.floor(Math.random() * viewport.height);
+
+ await page.mouse.move(x, y, {
+ steps: Math.floor(Math.random() * 10) + 5
+ });
+
+ await humanDelay(100, 300);
+ }
+}
+
+async function naturalScroll(page: Page) {
+ const scrollSteps = Math.floor(Math.random() * 5) + 2;
+
+ for (let i = 0; i < scrollSteps; i++) {
+ const scrollAmount = Math.floor(Math.random() * 300) + 100;
+
+ await page.mouse.wheel({ deltaY: scrollAmount });
+ await humanDelay(200, 500);
+ }
+}
+
+async function humanClick(page: Page, selector: string) {
+ const element = await page.$(selector);
+ if (!element) return;
+
+ const box = await element.boundingBox();
+ if (!box) return;
+
+ // Nicht exakt in der Mitte klicken
+ const x = box.x + box.width * (0.3 + Math.random() * 0.4);
+ const y = box.y + box.height * (0.3 + Math.random() * 0.4);
+
+ // Move then click
+ await page.mouse.move(x, y, { steps: 10 });
+ await humanDelay(50, 150);
+ await page.mouse.click(x, y);
+}
+
+// Realistic Typing
+async function humanType(page: Page, selector: string, text: string) {
+ await page.click(selector);
+ await humanDelay(100, 200);
+
+ for (const char of text) {
+ await page.keyboard.type(char);
+ await humanDelay(50, 150); // Variable Typing Speed
+
+ // Gelegentliche Pausen
+ if (Math.random() < 0.1) {
+ await humanDelay(200, 500);
+ }
+ }
+}
+```
+
+---
+
+## Detection Testing
+
+```typescript
+// Test gegen Detection Sites
+async function testDetection(page: Page) {
+ const detectionSites = [
+ 'https://bot.sannysoft.com',
+ 'https://arh.antoinevastel.com/bots/areyouheadless',
+ 'https://infosimples.github.io/detect-headless',
+ 'https://browserleaks.com/canvas'
+ ];
+
+ for (const site of detectionSites) {
+ await page.goto(site);
+ await page.screenshot({ path: `detection-${new URL(site).hostname}.png`, fullPage: true });
+ console.log(`Tested: ${site}`);
+ }
+}
+
+// Fingerprint Check
+async function getFingerprint(page: Page) {
+ return await page.evaluate(() => {
+ return {
+ userAgent: navigator.userAgent,
+ webdriver: (navigator as any).webdriver,
+ languages: navigator.languages,
+ plugins: navigator.plugins.length,
+ platform: navigator.platform,
+ hardwareConcurrency: navigator.hardwareConcurrency,
+ deviceMemory: (navigator as any).deviceMemory,
+ cookieEnabled: navigator.cookieEnabled,
+ doNotTrack: navigator.doNotTrack,
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
+ };
+ });
+}
+```
+
+---
+
+## CAPTCHA Handling
+
+```typescript
+// CAPTCHA Detection
+async function detectCaptcha(page: Page): Promise {
+ const captchaSelectors = [
+ { selector: '.g-recaptcha, [data-sitekey]', type: 'reCAPTCHA' },
+ { selector: '.h-captcha', type: 'hCaptcha' },
+ { selector: '#cf-turnstile', type: 'Turnstile' },
+ { selector: '.captcha, #captcha', type: 'Generic' }
+ ];
+
+ for (const { selector, type } of captchaSelectors) {
+ if (await page.$(selector)) {
+ return type;
+ }
+ }
+
+ return null;
+}
+
+// CAPTCHA Solving Service Integration
+async function solveCaptcha(page: Page, captchaType: string) {
+ // Integration mit Services wie 2captcha, Anti-Captcha
+ // Ethische Verwendung beachten!
+
+ if (captchaType === 'reCAPTCHA') {
+ const sitekey = await page.$eval(
+ '[data-sitekey]',
+ el => el.getAttribute('data-sitekey')
+ );
+
+ // API Call zu Solving Service
+ const solution = await callCaptchaSolver({
+ type: 'recaptcha',
+ sitekey,
+ pageUrl: page.url()
+ });
+
+ // Solution einfügen
+ await page.evaluate((token) => {
+ (document.querySelector('#g-recaptcha-response') as HTMLTextAreaElement).value = token;
+ (window as any).___grecaptcha_cfg.clients[0].K.K.callback(token);
+ }, solution);
+ }
+}
+```
+
+---
+
+## Fazit
+
+Erfolgreiche Anti-Detection erfordert:
+
+1. **Layered Approach**: Fingerprint + Behavior + Proxies
+2. **Realismus**: Human-like Patterns
+3. **Rotation**: Proxies, User-Agents, Fingerprints
+4. **Respekt**: Robots.txt, ToS, Rate Limits
+
+Immer ethisch und legal scrapen!
+
+---
+
+## Bildprompts
+
+1. "Shield blocking bot detection attempts, stealth browsing concept"
+2. "Human hand controlling browser puppet, behavior simulation"
+3. "Multiple masks representing different browser identities, fingerprint rotation"
+
+---
+
+## Quellen
+
+- [Playwright Stealth](https://www.zenrows.com/blog/playwright-stealth)
+- [Undetectable Scraping](https://scrapingant.com/blog/playwright-scraping-undetectable)
+- [Browser Fingerprinting](https://browserleaks.com/)
+- [Bot Detection Testing](https://bot.sannysoft.com)
diff --git a/blog-posts/55-data-extraction-pipelines.md b/blog-posts/55-data-extraction-pipelines.md
new file mode 100644
index 0000000..1f6cfa5
--- /dev/null
+++ b/blog-posts/55-data-extraction-pipelines.md
@@ -0,0 +1,549 @@
+# Data Extraction Pipelines: Von Roh-Daten zu Insights
+
+**Meta-Description:** Robuste Data Extraction Pipelines. ETL Patterns, Data Validation, Error Handling und Scalable Architecture.
+
+**Keywords:** Data Extraction, ETL Pipeline, Data Processing, Web Scraping Pipeline, Data Validation, Stream Processing
+
+---
+
+## Einführung
+
+Web Scraping ist nur der erste Schritt. **Data Extraction Pipelines** transformieren Rohdaten in nutzbare, validierte Datasets – mit Error Handling, Deduplication und Monitoring.
+
+---
+
+## Pipeline Architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ DATA EXTRACTION PIPELINE │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ 1. EXTRACT │
+│ ├── Web Scraping (Playwright/Puppeteer) │
+│ ├── API Calls │
+│ ├── File Imports (CSV, JSON, XML) │
+│ └── Database Queries │
+│ ↓ │
+│ 2. TRANSFORM │
+│ ├── Data Cleaning │
+│ ├── Normalization │
+│ ├── Validation (Zod) │
+│ ├── Enrichment │
+│ └── Deduplication │
+│ ↓ │
+│ 3. LOAD │
+│ ├── Database Insert │
+│ ├── File Export │
+│ ├── API Push │
+│ └── Event Stream │
+│ ↓ │
+│ 4. MONITOR │
+│ ├── Success/Failure Rates │
+│ ├── Data Quality Metrics │
+│ └── Alerting │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Pipeline Class Structure
+
+```typescript
+import { z } from 'zod';
+import { EventEmitter } from 'events';
+
+// Base Types
+interface PipelineResult {
+ success: boolean;
+ data?: T;
+ errors?: string[];
+ metadata: {
+ duration: number;
+ recordsProcessed: number;
+ recordsFailed: number;
+ };
+}
+
+// Abstract Pipeline
+abstract class DataPipeline extends EventEmitter {
+ protected name: string;
+
+ constructor(name: string) {
+ super();
+ this.name = name;
+ }
+
+ async run(input: TInput): Promise> {
+ const startTime = Date.now();
+ let recordsProcessed = 0;
+ let recordsFailed = 0;
+ const errors: string[] = [];
+
+ try {
+ this.emit('start', { pipeline: this.name });
+
+ // Extract
+ this.emit('phase', { phase: 'extract' });
+ const rawData = await this.extract(input);
+
+ // Transform
+ this.emit('phase', { phase: 'transform' });
+ const transformedData: TOutput[] = [];
+
+ for (const item of rawData) {
+ try {
+ const transformed = await this.transform(item);
+ if (transformed) {
+ transformedData.push(transformed);
+ recordsProcessed++;
+ }
+ } catch (error) {
+ recordsFailed++;
+ errors.push(`Transform error: ${(error as Error).message}`);
+ this.emit('error', { phase: 'transform', error });
+ }
+ }
+
+ // Load
+ this.emit('phase', { phase: 'load' });
+ await this.load(transformedData);
+
+ this.emit('complete', {
+ pipeline: this.name,
+ recordsProcessed,
+ recordsFailed
+ });
+
+ return {
+ success: true,
+ data: transformedData as any,
+ errors: errors.length > 0 ? errors : undefined,
+ metadata: {
+ duration: Date.now() - startTime,
+ recordsProcessed,
+ recordsFailed
+ }
+ };
+ } catch (error) {
+ this.emit('error', { phase: 'pipeline', error });
+
+ return {
+ success: false,
+ errors: [(error as Error).message],
+ metadata: {
+ duration: Date.now() - startTime,
+ recordsProcessed,
+ recordsFailed
+ }
+ };
+ }
+ }
+
+ protected abstract extract(input: TInput): Promise;
+ protected abstract transform(item: any): Promise;
+ protected abstract load(data: TOutput[]): Promise;
+}
+```
+
+---
+
+## Concrete Pipeline Implementation
+
+```typescript
+import * as cheerio from 'cheerio';
+import axios from 'axios';
+
+// Schema Definition
+const ProductSchema = z.object({
+ id: z.string(),
+ name: z.string().min(1),
+ price: z.number().positive(),
+ currency: z.enum(['EUR', 'USD', 'GBP']),
+ description: z.string().optional(),
+ category: z.string(),
+ imageUrl: z.string().url().optional(),
+ sourceUrl: z.string().url(),
+ scrapedAt: z.date()
+});
+
+type Product = z.infer;
+
+// Product Scraping Pipeline
+class ProductScrapingPipeline extends DataPipeline {
+ private seenIds = new Set();
+
+ constructor() {
+ super('ProductScrapingPipeline');
+ }
+
+ protected async extract(urls: string[]): Promise {
+ const rawProducts: any[] = [];
+
+ for (const url of urls) {
+ try {
+ const { data: html } = await axios.get(url, {
+ headers: { 'User-Agent': 'Mozilla/5.0' },
+ timeout: 10000
+ });
+
+ const $ = cheerio.load(html);
+
+ $('.product-card').each((_, element) => {
+ const $el = $(element);
+
+ rawProducts.push({
+ id: $el.data('product-id'),
+ name: $el.find('.product-name').text().trim(),
+ priceText: $el.find('.price').text().trim(),
+ description: $el.find('.description').text().trim(),
+ category: $el.find('.category').text().trim(),
+ imageUrl: $el.find('img').attr('src'),
+ sourceUrl: url
+ });
+ });
+
+ // Rate Limiting
+ await new Promise(r => setTimeout(r, 1000));
+ } catch (error) {
+ this.emit('error', { phase: 'extract', url, error });
+ }
+ }
+
+ return rawProducts;
+ }
+
+ protected async transform(item: any): Promise