Compare commits

..
Author SHA1 Message Date
damjan_savic 39697ae857 auto-claude: subtask-3-1 - Update blog page to import legacy posts from data file 2026-01-25 06:34:39 +01:00
damjan_savic 4cc19386fe auto-claude: subtask-2-1 - Add caching to getAllBlogPosts using existing Cache 2026-01-25 06:32:09 +01:00
damjan_savicandClaude Sonnet 4.5 2bb0003902 auto-claude: subtask-1-1 - Create legacyBlogPosts data file with TypeScript types
- Created src/data/legacyBlogPosts.ts following pattern from cities.ts and services.ts
- Exported LegacyBlogPostsData type and legacyBlogPosts constant
- Includes all 8 legacy blog posts with translations (de, en, sr)
- Build succeeds with no type errors

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-25 06:30:23 +01:00
14 changed files with 251 additions and 1834 deletions
-227
View File
@@ -1,227 +0,0 @@
{
"base_commands": [
".",
"[",
"[[",
"ag",
"awk",
"basename",
"bash",
"bc",
"break",
"cat",
"cd",
"chmod",
"clear",
"cmp",
"column",
"comm",
"command",
"continue",
"cp",
"curl",
"cut",
"date",
"df",
"diff",
"dig",
"dirname",
"du",
"echo",
"egrep",
"env",
"eval",
"exec",
"exit",
"expand",
"export",
"expr",
"false",
"fd",
"fgrep",
"file",
"find",
"fmt",
"fold",
"gawk",
"gh",
"git",
"grep",
"gunzip",
"gzip",
"head",
"help",
"host",
"iconv",
"id",
"jobs",
"join",
"jq",
"kill",
"killall",
"less",
"let",
"ln",
"ls",
"lsof",
"man",
"mkdir",
"mktemp",
"more",
"mv",
"nl",
"paste",
"pgrep",
"ping",
"pkill",
"popd",
"printenv",
"printf",
"ps",
"pushd",
"pwd",
"read",
"readlink",
"realpath",
"reset",
"return",
"rev",
"rg",
"rm",
"rmdir",
"sed",
"seq",
"set",
"sh",
"shuf",
"sleep",
"sort",
"source",
"split",
"stat",
"tail",
"tar",
"tee",
"test",
"time",
"timeout",
"touch",
"tr",
"tree",
"true",
"type",
"uname",
"unexpand",
"uniq",
"unset",
"unzip",
"watch",
"wc",
"wget",
"whereis",
"which",
"whoami",
"xargs",
"yes",
"yq",
"zip",
"zsh"
],
"stack_commands": [
"ar",
"clang",
"clang++",
"cmake",
"composer",
"dive",
"docker",
"docker-buildx",
"docker-compose",
"dockerfile",
"eslint",
"g++",
"gcc",
"ipython",
"jupyter",
"ld",
"make",
"meson",
"next",
"ninja",
"nm",
"node",
"notebook",
"npm",
"npx",
"objdump",
"pdb",
"php",
"pip",
"pip3",
"pipx",
"pnpm",
"pnpx",
"pudb",
"python",
"python3",
"react-scripts",
"strip",
"ts-node",
"tsc",
"tsx",
"vitest"
],
"script_commands": [
"bun",
"npm",
"pnpm",
"yarn"
],
"custom_commands": [],
"detected_stack": {
"languages": [
"python",
"javascript",
"typescript",
"php",
"c"
],
"package_managers": [
"pnpm"
],
"frameworks": [
"nextjs",
"react",
"vitest",
"eslint"
],
"databases": [],
"infrastructure": [
"docker"
],
"cloud_providers": [],
"code_quality_tools": [],
"version_managers": []
},
"custom_scripts": {
"npm_scripts": [
"dev",
"build",
"start",
"lint",
"build:images",
"generate:images",
"generate:images:dry",
"test",
"test:coverage"
],
"make_targets": [],
"poetry_scripts": [],
"cargo_aliases": [],
"shell_scripts": []
},
"project_dir": "C:\\Users\\damja\\WebstormProjects\\Portfolio",
"created_at": "2026-01-22T15:28:38.237190",
"project_hash": "c4ad399e16be367eb4e6b076fe1d9ee3",
"inherited_from": "C:\\Users\\damja\\WebstormProjects\\Portfolio"
}
-25
View File
@@ -1,25 +0,0 @@
{
"active": true,
"spec": "030-add-unit-tests-vitest-configured-but-no-tests-exis",
"state": "building",
"subtasks": {
"completed": 2,
"total": 8,
"in_progress": 1,
"failed": 0
},
"phase": {
"current": "Utility Functions Tests",
"id": null,
"total": 2
},
"workers": {
"active": 0,
"max": 1
},
"session": {
"number": 4,
"started_at": "2026-01-25T06:18:19.433683"
},
"last_update": "2026-01-25T06:32:23.146357"
}
-39
View File
@@ -1,39 +0,0 @@
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true
},
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Read(./**)",
"Write(./**)",
"Edit(./**)",
"Glob(./**)",
"Grep(./**)",
"Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)",
"Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)",
"Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)",
"Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)",
"Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)",
"Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis\\.auto-claude\\specs\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)",
"Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis\\.auto-claude\\specs\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)",
"Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude\\worktrees\\tasks\\030-add-unit-tests-vitest-configured-but-no-tests-exis\\.auto-claude\\specs\\030-add-unit-tests-vitest-configured-but-no-tests-exis/**)",
"Read(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)",
"Write(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)",
"Edit(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)",
"Glob(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)",
"Grep(C:\\Users\\damja\\WebstormProjects\\Portfolio\\.auto-claude/**)",
"Bash(*)",
"WebFetch(*)",
"WebSearch(*)",
"mcp__context7__resolve-library-id(*)",
"mcp__context7__get-library-docs(*)",
"mcp__graphiti-memory__search_nodes(*)",
"mcp__graphiti-memory__search_facts(*)",
"mcp__graphiti-memory__add_episode(*)",
"mcp__graphiti-memory__get_episodes(*)",
"mcp__graphiti-memory__get_entity_edge(*)"
]
}
}
-3
View File
@@ -82,6 +82,3 @@ supabase/.temp/
# Source images (originals before optimization)
source-images/
# Auto Claude data directory
.auto-claude/
-4
View File
@@ -38,17 +38,13 @@
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/mdx": "^2.0.13",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitest/coverage-v8": "1.6.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.9.1",
"eslint-config-next": "^15.1.0",
"jsdom": "^23.2.0",
"postcss": "^8.4.35",
"sharp": "^0.34.3",
"tailwindcss": "^3.4.1",
+1 -226
View File
@@ -5,6 +5,7 @@ import Link from 'next/link';
import Image from 'next/image';
import type { Metadata } from 'next';
import { getAllBlogPosts, BlogPost } from '@/lib/blog';
import { legacyBlogPosts } from '@/data/legacyBlogPosts';
const POSTS_PER_PAGE = 12;
@@ -13,232 +14,6 @@ type Props = {
searchParams: Promise<{ page?: string }>;
};
// Legacy blog posts with translations (these have manual translations)
const legacyBlogPosts: Record<string, BlogPost[]> = {
de: [
{
slug: 'n8n-automatisierung-tutorial',
title: 'n8n Tutorial: Workflows automatisieren ohne Code - Schritt für Schritt',
date: '2026-01-18',
excerpt: 'Lernen Sie, wie Sie mit n8n leistungsstarke Automatisierungen erstellen. Von der Installation bis zum ersten produktiven Workflow - inklusive Best Practices.',
category: 'Automatisierung',
tags: ['n8n', 'Tutorial', 'Automatisierung', 'Workflow', 'No-Code', 'Self-Hosted'],
coverImage: '/images/posts/n8n-automatisierung-tutorial/cover.jpg',
},
{
slug: 'geo-generative-engine-optimization',
title: 'GEO: Generative Engine Optimization - SEO für die KI-Ära',
date: '2026-01-17',
excerpt: 'Wie Sie Ihre Inhalte für ChatGPT, Perplexity und andere KI-Suchmaschinen optimieren. Der neue Standard nach klassischem SEO.',
category: 'Marketing',
tags: ['GEO', 'SEO', 'KI', 'ChatGPT', 'Perplexity', 'Content Marketing', 'AEO'],
coverImage: '/images/posts/geo-generative-engine-optimization/cover.jpg',
},
{
slug: 'gpt-api-integration-praxisguide',
title: 'GPT API Integration: Vom API-Key zur produktionsreifen Anwendung',
date: '2026-01-16',
excerpt: 'Praktische Anleitung zur Integration von OpenAI GPT-4 in Ihre Anwendungen. Token-Optimierung, Fehlerbehandlung und Best Practices.',
category: 'KI-Entwicklung',
tags: ['GPT-4', 'OpenAI', 'API', 'Integration', 'Python', 'LLM', 'Prompt Engineering'],
coverImage: '/images/posts/gpt-api-integration-praxisguide/cover.jpg',
},
{
slug: 'ki-agenten-unternehmen-guide',
title: 'KI-Agenten für Unternehmen: Der ultimative Praxisguide 2026',
date: '2026-01-15',
excerpt: 'Wie Unternehmen KI-Agenten einsetzen können, um Prozesse zu automatisieren, Kosten zu senken und Wettbewerbsvorteile zu gewinnen.',
category: 'KI-Entwicklung',
tags: ['KI-Agenten', 'Automatisierung', 'GPT-4', 'Claude', 'LLM', 'Unternehmen'],
coverImage: '/images/posts/ki-agenten-unternehmen-guide/cover.jpg',
},
{
slug: 'n8n-make-zapier-vergleich',
title: 'n8n vs Make vs Zapier: Welches Automatisierungstool passt zu Ihnen?',
date: '2026-01-10',
excerpt: 'Ein detaillierter Vergleich der führenden Workflow-Automatisierungstools mit Vor- und Nachteilen für verschiedene Anwendungsfälle.',
category: 'Automatisierung',
tags: ['n8n', 'Make', 'Zapier', 'Workflow', 'Automatisierung', 'No-Code'],
coverImage: '/images/posts/n8n-make-zapier-vergleich/cover.jpg',
},
{
slug: 'voice-ai-kundenservice-trends',
title: 'Voice AI im Kundenservice: Trends und Best Practices 2026',
date: '2026-01-05',
excerpt: 'Wie Voice AI den Kundenservice revolutioniert und welche Technologien Sie für Ihr Unternehmen evaluieren sollten.',
category: 'Voice AI',
tags: ['Voice AI', 'Sprachassistent', 'Kundenservice', 'NLP', 'Chatbot', 'Vapi'],
coverImage: '/images/posts/voice-ai-kundenservice-trends/cover.jpg',
},
{
slug: 'saas-mvp-entwicklung-4-wochen',
title: 'SaaS MVP in 4 Wochen: Von der Idee zum Launch',
date: '2025-12-20',
excerpt: 'Schritt-für-Schritt Anleitung zur schnellen Entwicklung eines SaaS MVP mit Next.js, Prisma und modernen Cloud-Diensten.',
category: 'SaaS-Entwicklung',
tags: ['SaaS', 'MVP', 'Next.js', 'Startup', 'Produktentwicklung', 'React'],
coverImage: '/images/posts/saas-mvp-entwicklung-4-wochen/cover.jpg',
},
{
slug: 'erp-integration-breuninger',
title: 'ApparelMagic und TradeByte: Analyse komplexer Integrationen',
date: '2024-02-09',
excerpt: 'Detaillierte Analyse einer E-Commerce-Integration zwischen Apparel Magic und Breuninger via TradeByte',
category: 'System Integration',
tags: ['ERP', 'Apparel Magic', 'TradeByte', 'API Integration', 'Python', 'MariaDB'],
coverImage: '/images/posts/erp-integration-breuninger/cover.jpg',
},
],
en: [
{
slug: 'n8n-automatisierung-tutorial',
title: 'n8n Tutorial: Automate Workflows Without Code - Step by Step',
date: '2026-01-18',
excerpt: 'Learn how to create powerful automations with n8n. From installation to your first production workflow - including best practices.',
category: 'Automation',
tags: ['n8n', 'Tutorial', 'Automation', 'Workflow', 'No-Code', 'Self-Hosted'],
coverImage: '/images/posts/n8n-automatisierung-tutorial/cover.jpg',
},
{
slug: 'geo-generative-engine-optimization',
title: 'GEO: Generative Engine Optimization - SEO for the AI Era',
date: '2026-01-17',
excerpt: 'How to optimize your content for ChatGPT, Perplexity and other AI search engines. The new standard after traditional SEO.',
category: 'Marketing',
tags: ['GEO', 'SEO', 'AI', 'ChatGPT', 'Perplexity', 'Content Marketing', 'AEO'],
coverImage: '/images/posts/geo-generative-engine-optimization/cover.jpg',
},
{
slug: 'gpt-api-integration-praxisguide',
title: 'GPT API Integration: From API Key to Production-Ready Application',
date: '2026-01-16',
excerpt: 'Practical guide to integrating OpenAI GPT-4 into your applications. Token optimization, error handling and best practices.',
category: 'AI Development',
tags: ['GPT-4', 'OpenAI', 'API', 'Integration', 'Python', 'LLM', 'Prompt Engineering'],
coverImage: '/images/posts/gpt-api-integration-praxisguide/cover.jpg',
},
{
slug: 'ki-agenten-unternehmen-guide',
title: 'AI Agents for Business: The Ultimate Practical Guide 2026',
date: '2026-01-15',
excerpt: 'How companies can use AI agents to automate processes, reduce costs and gain competitive advantages.',
category: 'AI Development',
tags: ['AI Agents', 'Automation', 'GPT-4', 'Claude', 'LLM', 'Enterprise'],
coverImage: '/images/posts/ki-agenten-unternehmen-guide/cover.jpg',
},
{
slug: 'n8n-make-zapier-vergleich',
title: 'n8n vs Make vs Zapier: Which Automation Tool is Right for You?',
date: '2026-01-10',
excerpt: 'A detailed comparison of leading workflow automation tools with pros and cons for different use cases.',
category: 'Automation',
tags: ['n8n', 'Make', 'Zapier', 'Workflow', 'Automation', 'No-Code'],
coverImage: '/images/posts/n8n-make-zapier-vergleich/cover.jpg',
},
{
slug: 'voice-ai-kundenservice-trends',
title: 'Voice AI in Customer Service: Trends and Best Practices 2026',
date: '2026-01-05',
excerpt: 'How Voice AI is revolutionizing customer service and which technologies you should evaluate for your business.',
category: 'Voice AI',
tags: ['Voice AI', 'Voice Assistant', 'Customer Service', 'NLP', 'Chatbot', 'Vapi'],
coverImage: '/images/posts/voice-ai-kundenservice-trends/cover.jpg',
},
{
slug: 'saas-mvp-entwicklung-4-wochen',
title: 'SaaS MVP in 4 Weeks: From Idea to Launch',
date: '2025-12-20',
excerpt: 'Step-by-step guide to quickly developing a SaaS MVP with Next.js, Prisma and modern cloud services.',
category: 'SaaS Development',
tags: ['SaaS', 'MVP', 'Next.js', 'Startup', 'Product Development', 'React'],
coverImage: '/images/posts/saas-mvp-entwicklung-4-wochen/cover.jpg',
},
{
slug: 'erp-integration-breuninger',
title: 'ApparelMagic and TradeByte: Complex Integration Analysis',
date: '2024-02-09',
excerpt: 'Detailed analysis of an e-commerce integration between Apparel Magic and Breuninger via TradeByte',
category: 'System Integration',
tags: ['ERP', 'Apparel Magic', 'TradeByte', 'API Integration', 'Python', 'MariaDB'],
coverImage: '/images/posts/erp-integration-breuninger/cover.jpg',
},
],
sr: [
{
slug: 'n8n-automatisierung-tutorial',
title: 'n8n Tutorial: Automatizujte Radne Tokove Bez Koda - Korak po Korak',
date: '2026-01-18',
excerpt: 'Naučite kako da kreirate moćne automatizacije sa n8n. Od instalacije do prvog produktivnog radnog toka - uključujući najbolje prakse.',
category: 'Automatizacija',
tags: ['n8n', 'Tutorial', 'Automatizacija', 'Workflow', 'No-Code', 'Self-Hosted'],
coverImage: '/images/posts/n8n-automatisierung-tutorial/cover.jpg',
},
{
slug: 'geo-generative-engine-optimization',
title: 'GEO: Generative Engine Optimization - SEO za AI Eru',
date: '2026-01-17',
excerpt: 'Kako da optimizujete sadržaj za ChatGPT, Perplexity i druge AI pretraživače. Novi standard posle klasičnog SEO-a.',
category: 'Marketing',
tags: ['GEO', 'SEO', 'AI', 'ChatGPT', 'Perplexity', 'Content Marketing', 'AEO'],
coverImage: '/images/posts/geo-generative-engine-optimization/cover.jpg',
},
{
slug: 'gpt-api-integration-praxisguide',
title: 'GPT API Integracija: Od API Ključa do Produkcione Aplikacije',
date: '2026-01-16',
excerpt: 'Praktični vodič za integraciju OpenAI GPT-4 u vaše aplikacije. Optimizacija tokena, rukovanje greškama i najbolje prakse.',
category: 'AI Razvoj',
tags: ['GPT-4', 'OpenAI', 'API', 'Integracija', 'Python', 'LLM', 'Prompt Engineering'],
coverImage: '/images/posts/gpt-api-integration-praxisguide/cover.jpg',
},
{
slug: 'ki-agenten-unternehmen-guide',
title: 'AI Agenti za Preduzeća: Ultimativni Praktični Vodič 2026',
date: '2026-01-15',
excerpt: 'Kako kompanije mogu koristiti AI agente za automatizaciju procesa, smanjenje troškova i sticanje konkurentskih prednosti.',
category: 'AI Razvoj',
tags: ['AI Agenti', 'Automatizacija', 'GPT-4', 'Claude', 'LLM', 'Preduzeća'],
coverImage: '/images/posts/ki-agenten-unternehmen-guide/cover.jpg',
},
{
slug: 'n8n-make-zapier-vergleich',
title: 'n8n vs Make vs Zapier: Koji Alat za Automatizaciju je Pravi za Vas?',
date: '2026-01-10',
excerpt: 'Detaljna uporedna analiza vodećih alata za automatizaciju radnih tokova sa prednostima i manama za različite slučajeve upotrebe.',
category: 'Automatizacija',
tags: ['n8n', 'Make', 'Zapier', 'Workflow', 'Automatizacija', 'No-Code'],
coverImage: '/images/posts/n8n-make-zapier-vergleich/cover.jpg',
},
{
slug: 'voice-ai-kundenservice-trends',
title: 'Voice AI u Korisničkoj Službi: Trendovi i Najbolje Prakse 2026',
date: '2026-01-05',
excerpt: 'Kako Voice AI revolucioniše korisničku službu i koje tehnologije treba da evaluirate za vaš posao.',
category: 'Voice AI',
tags: ['Voice AI', 'Glasovni Asistent', 'Korisnička Služba', 'NLP', 'Chatbot', 'Vapi'],
coverImage: '/images/posts/voice-ai-kundenservice-trends/cover.jpg',
},
{
slug: 'saas-mvp-entwicklung-4-wochen',
title: 'SaaS MVP za 4 Nedelje: Od Ideje do Lansiranja',
date: '2025-12-20',
excerpt: 'Korak-po-korak vodič za brz razvoj SaaS MVP-a sa Next.js, Prisma i modernim cloud servisima.',
category: 'SaaS Razvoj',
tags: ['SaaS', 'MVP', 'Next.js', 'Startup', 'Razvoj Proizvoda', 'React'],
coverImage: '/images/posts/saas-mvp-entwicklung-4-wochen/cover.jpg',
},
{
slug: 'erp-integration-breuninger',
title: 'ApparelMagic i TradeByte: Analiza kompleksnih integracija',
date: '2024-02-09',
excerpt: 'Detaljna analiza e-commerce integracije izmedu Apparel Magic i Breuninger putem TradeByte',
category: 'Sistemska Integracija',
tags: ['ERP', 'Apparel Magic', 'TradeByte', 'API Integracija', 'Python', 'MariaDB'],
coverImage: '/images/posts/erp-integration-breuninger/cover.jpg',
},
],
};
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
+228
View File
@@ -0,0 +1,228 @@
import type { BlogPost } from '@/lib/blog';
export type LegacyBlogPostsData = Record<string, BlogPost[]>;
export const legacyBlogPosts: LegacyBlogPostsData = {
de: [
{
slug: 'n8n-automatisierung-tutorial',
title: 'n8n Tutorial: Workflows automatisieren ohne Code - Schritt für Schritt',
date: '2026-01-18',
excerpt: 'Lernen Sie, wie Sie mit n8n leistungsstarke Automatisierungen erstellen. Von der Installation bis zum ersten produktiven Workflow - inklusive Best Practices.',
category: 'Automatisierung',
tags: ['n8n', 'Tutorial', 'Automatisierung', 'Workflow', 'No-Code', 'Self-Hosted'],
coverImage: '/images/posts/n8n-automatisierung-tutorial/cover.jpg',
},
{
slug: 'geo-generative-engine-optimization',
title: 'GEO: Generative Engine Optimization - SEO für die KI-Ära',
date: '2026-01-17',
excerpt: 'Wie Sie Ihre Inhalte für ChatGPT, Perplexity und andere KI-Suchmaschinen optimieren. Der neue Standard nach klassischem SEO.',
category: 'Marketing',
tags: ['GEO', 'SEO', 'KI', 'ChatGPT', 'Perplexity', 'Content Marketing', 'AEO'],
coverImage: '/images/posts/geo-generative-engine-optimization/cover.jpg',
},
{
slug: 'gpt-api-integration-praxisguide',
title: 'GPT API Integration: Vom API-Key zur produktionsreifen Anwendung',
date: '2026-01-16',
excerpt: 'Praktische Anleitung zur Integration von OpenAI GPT-4 in Ihre Anwendungen. Token-Optimierung, Fehlerbehandlung und Best Practices.',
category: 'KI-Entwicklung',
tags: ['GPT-4', 'OpenAI', 'API', 'Integration', 'Python', 'LLM', 'Prompt Engineering'],
coverImage: '/images/posts/gpt-api-integration-praxisguide/cover.jpg',
},
{
slug: 'ki-agenten-unternehmen-guide',
title: 'KI-Agenten für Unternehmen: Der ultimative Praxisguide 2026',
date: '2026-01-15',
excerpt: 'Wie Unternehmen KI-Agenten einsetzen können, um Prozesse zu automatisieren, Kosten zu senken und Wettbewerbsvorteile zu gewinnen.',
category: 'KI-Entwicklung',
tags: ['KI-Agenten', 'Automatisierung', 'GPT-4', 'Claude', 'LLM', 'Unternehmen'],
coverImage: '/images/posts/ki-agenten-unternehmen-guide/cover.jpg',
},
{
slug: 'n8n-make-zapier-vergleich',
title: 'n8n vs Make vs Zapier: Welches Automatisierungstool passt zu Ihnen?',
date: '2026-01-10',
excerpt: 'Ein detaillierter Vergleich der führenden Workflow-Automatisierungstools mit Vor- und Nachteilen für verschiedene Anwendungsfälle.',
category: 'Automatisierung',
tags: ['n8n', 'Make', 'Zapier', 'Workflow', 'Automatisierung', 'No-Code'],
coverImage: '/images/posts/n8n-make-zapier-vergleich/cover.jpg',
},
{
slug: 'voice-ai-kundenservice-trends',
title: 'Voice AI im Kundenservice: Trends und Best Practices 2026',
date: '2026-01-05',
excerpt: 'Wie Voice AI den Kundenservice revolutioniert und welche Technologien Sie für Ihr Unternehmen evaluieren sollten.',
category: 'Voice AI',
tags: ['Voice AI', 'Sprachassistent', 'Kundenservice', 'NLP', 'Chatbot', 'Vapi'],
coverImage: '/images/posts/voice-ai-kundenservice-trends/cover.jpg',
},
{
slug: 'saas-mvp-entwicklung-4-wochen',
title: 'SaaS MVP in 4 Wochen: Von der Idee zum Launch',
date: '2025-12-20',
excerpt: 'Schritt-für-Schritt Anleitung zur schnellen Entwicklung eines SaaS MVP mit Next.js, Prisma und modernen Cloud-Diensten.',
category: 'SaaS-Entwicklung',
tags: ['SaaS', 'MVP', 'Next.js', 'Startup', 'Produktentwicklung', 'React'],
coverImage: '/images/posts/saas-mvp-entwicklung-4-wochen/cover.jpg',
},
{
slug: 'erp-integration-breuninger',
title: 'ApparelMagic und TradeByte: Analyse komplexer Integrationen',
date: '2024-02-09',
excerpt: 'Detaillierte Analyse einer E-Commerce-Integration zwischen Apparel Magic und Breuninger via TradeByte',
category: 'System Integration',
tags: ['ERP', 'Apparel Magic', 'TradeByte', 'API Integration', 'Python', 'MariaDB'],
coverImage: '/images/posts/erp-integration-breuninger/cover.jpg',
},
],
en: [
{
slug: 'n8n-automatisierung-tutorial',
title: 'n8n Tutorial: Automate Workflows Without Code - Step by Step',
date: '2026-01-18',
excerpt: 'Learn how to create powerful automations with n8n. From installation to your first production workflow - including best practices.',
category: 'Automation',
tags: ['n8n', 'Tutorial', 'Automation', 'Workflow', 'No-Code', 'Self-Hosted'],
coverImage: '/images/posts/n8n-automatisierung-tutorial/cover.jpg',
},
{
slug: 'geo-generative-engine-optimization',
title: 'GEO: Generative Engine Optimization - SEO for the AI Era',
date: '2026-01-17',
excerpt: 'How to optimize your content for ChatGPT, Perplexity and other AI search engines. The new standard after traditional SEO.',
category: 'Marketing',
tags: ['GEO', 'SEO', 'AI', 'ChatGPT', 'Perplexity', 'Content Marketing', 'AEO'],
coverImage: '/images/posts/geo-generative-engine-optimization/cover.jpg',
},
{
slug: 'gpt-api-integration-praxisguide',
title: 'GPT API Integration: From API Key to Production-Ready Application',
date: '2026-01-16',
excerpt: 'Practical guide to integrating OpenAI GPT-4 into your applications. Token optimization, error handling and best practices.',
category: 'AI Development',
tags: ['GPT-4', 'OpenAI', 'API', 'Integration', 'Python', 'LLM', 'Prompt Engineering'],
coverImage: '/images/posts/gpt-api-integration-praxisguide/cover.jpg',
},
{
slug: 'ki-agenten-unternehmen-guide',
title: 'AI Agents for Business: The Ultimate Practical Guide 2026',
date: '2026-01-15',
excerpt: 'How companies can use AI agents to automate processes, reduce costs and gain competitive advantages.',
category: 'AI Development',
tags: ['AI Agents', 'Automation', 'GPT-4', 'Claude', 'LLM', 'Enterprise'],
coverImage: '/images/posts/ki-agenten-unternehmen-guide/cover.jpg',
},
{
slug: 'n8n-make-zapier-vergleich',
title: 'n8n vs Make vs Zapier: Which Automation Tool is Right for You?',
date: '2026-01-10',
excerpt: 'A detailed comparison of leading workflow automation tools with pros and cons for different use cases.',
category: 'Automation',
tags: ['n8n', 'Make', 'Zapier', 'Workflow', 'Automation', 'No-Code'],
coverImage: '/images/posts/n8n-make-zapier-vergleich/cover.jpg',
},
{
slug: 'voice-ai-kundenservice-trends',
title: 'Voice AI in Customer Service: Trends and Best Practices 2026',
date: '2026-01-05',
excerpt: 'How Voice AI is revolutionizing customer service and which technologies you should evaluate for your business.',
category: 'Voice AI',
tags: ['Voice AI', 'Voice Assistant', 'Customer Service', 'NLP', 'Chatbot', 'Vapi'],
coverImage: '/images/posts/voice-ai-kundenservice-trends/cover.jpg',
},
{
slug: 'saas-mvp-entwicklung-4-wochen',
title: 'SaaS MVP in 4 Weeks: From Idea to Launch',
date: '2025-12-20',
excerpt: 'Step-by-step guide to quickly developing a SaaS MVP with Next.js, Prisma and modern cloud services.',
category: 'SaaS Development',
tags: ['SaaS', 'MVP', 'Next.js', 'Startup', 'Product Development', 'React'],
coverImage: '/images/posts/saas-mvp-entwicklung-4-wochen/cover.jpg',
},
{
slug: 'erp-integration-breuninger',
title: 'ApparelMagic and TradeByte: Complex Integration Analysis',
date: '2024-02-09',
excerpt: 'Detailed analysis of an e-commerce integration between Apparel Magic and Breuninger via TradeByte',
category: 'System Integration',
tags: ['ERP', 'Apparel Magic', 'TradeByte', 'API Integration', 'Python', 'MariaDB'],
coverImage: '/images/posts/erp-integration-breuninger/cover.jpg',
},
],
sr: [
{
slug: 'n8n-automatisierung-tutorial',
title: 'n8n Tutorial: Automatizujte Radne Tokove Bez Koda - Korak po Korak',
date: '2026-01-18',
excerpt: 'Naučite kako da kreirate moćne automatizacije sa n8n. Od instalacije do prvog produktivnog radnog toka - uključujući najbolje prakse.',
category: 'Automatizacija',
tags: ['n8n', 'Tutorial', 'Automatizacija', 'Workflow', 'No-Code', 'Self-Hosted'],
coverImage: '/images/posts/n8n-automatisierung-tutorial/cover.jpg',
},
{
slug: 'geo-generative-engine-optimization',
title: 'GEO: Generative Engine Optimization - SEO za AI Eru',
date: '2026-01-17',
excerpt: 'Kako da optimizujete sadržaj za ChatGPT, Perplexity i druge AI pretraživače. Novi standard posle klasičnog SEO-a.',
category: 'Marketing',
tags: ['GEO', 'SEO', 'AI', 'ChatGPT', 'Perplexity', 'Content Marketing', 'AEO'],
coverImage: '/images/posts/geo-generative-engine-optimization/cover.jpg',
},
{
slug: 'gpt-api-integration-praxisguide',
title: 'GPT API Integracija: Od API Ključa do Produkcione Aplikacije',
date: '2026-01-16',
excerpt: 'Praktični vodič za integraciju OpenAI GPT-4 u vaše aplikacije. Optimizacija tokena, rukovanje greškama i najbolje prakse.',
category: 'AI Razvoj',
tags: ['GPT-4', 'OpenAI', 'API', 'Integracija', 'Python', 'LLM', 'Prompt Engineering'],
coverImage: '/images/posts/gpt-api-integration-praxisguide/cover.jpg',
},
{
slug: 'ki-agenten-unternehmen-guide',
title: 'AI Agenti za Preduzeća: Ultimativni Praktični Vodič 2026',
date: '2026-01-15',
excerpt: 'Kako kompanije mogu koristiti AI agente za automatizaciju procesa, smanjenje troškova i sticanje konkurentskih prednosti.',
category: 'AI Razvoj',
tags: ['AI Agenti', 'Automatizacija', 'GPT-4', 'Claude', 'LLM', 'Preduzeća'],
coverImage: '/images/posts/ki-agenten-unternehmen-guide/cover.jpg',
},
{
slug: 'n8n-make-zapier-vergleich',
title: 'n8n vs Make vs Zapier: Koji Alat za Automatizaciju je Pravi za Vas?',
date: '2026-01-10',
excerpt: 'Detaljna uporedna analiza vodećih alata za automatizaciju radnih tokova sa prednostima i manama za različite slučajeve upotrebe.',
category: 'Automatizacija',
tags: ['n8n', 'Make', 'Zapier', 'Workflow', 'Automatizacija', 'No-Code'],
coverImage: '/images/posts/n8n-make-zapier-vergleich/cover.jpg',
},
{
slug: 'voice-ai-kundenservice-trends',
title: 'Voice AI u Korisničkoj Službi: Trendovi i Najbolje Prakse 2026',
date: '2026-01-05',
excerpt: 'Kako Voice AI revolucioniše korisničku službu i koje tehnologije treba da evaluirate za vaš posao.',
category: 'Voice AI',
tags: ['Voice AI', 'Glasovni Asistent', 'Korisnička Služba', 'NLP', 'Chatbot', 'Vapi'],
coverImage: '/images/posts/voice-ai-kundenservice-trends/cover.jpg',
},
{
slug: 'saas-mvp-entwicklung-4-wochen',
title: 'SaaS MVP za 4 Nedelje: Od Ideje do Lansiranja',
date: '2025-12-20',
excerpt: 'Korak-po-korak vodič za brz razvoj SaaS MVP-a sa Next.js, Prisma i modernim cloud servisima.',
category: 'SaaS Razvoj',
tags: ['SaaS', 'MVP', 'Next.js', 'Startup', 'Razvoj Proizvoda', 'React'],
coverImage: '/images/posts/saas-mvp-entwicklung-4-wochen/cover.jpg',
},
{
slug: 'erp-integration-breuninger',
title: 'ApparelMagic i TradeByte: Analiza kompleksnih integracija',
date: '2024-02-09',
excerpt: 'Detaljna analiza e-commerce integracije izmedu Apparel Magic i Breuninger putem TradeByte',
category: 'Sistemska Integracija',
tags: ['ERP', 'Apparel Magic', 'TradeByte', 'API Integracija', 'Python', 'MariaDB'],
coverImage: '/images/posts/erp-integration-breuninger/cover.jpg',
},
],
};
-348
View File
@@ -1,348 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useAsync } from './useAsync';
import * as errorHandling from '../utils/errorHandling';
// Mock the errorHandling module
vi.mock('../utils/errorHandling', async () => {
const actual = await vi.importActual('../utils/errorHandling');
return {
...actual,
handleError: vi.fn((error: unknown) => {
if (error instanceof Error) {
return error;
}
return new Error('Handled error');
})
};
});
describe('useAsync', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('initial state', () => {
it('should initialize with null data, null error, and loading false', () => {
const { result } = renderHook(() => useAsync<string>());
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();
expect(result.current.loading).toBe(false);
});
it('should provide an execute function', () => {
const { result } = renderHook(() => useAsync<string>());
expect(result.current.execute).toBeInstanceOf(Function);
});
});
describe('loading states', () => {
it('should set loading to true when executing', async () => {
const { result } = renderHook(() => useAsync<string>());
const asyncFn = vi.fn(() => new Promise<string>((resolve) => {
setTimeout(() => resolve('test data'), 100);
}));
result.current.execute(asyncFn);
// Loading should be true immediately after execute is called
expect(result.current.loading).toBe(true);
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
});
it('should set loading to false after successful execution', async () => {
const { result } = renderHook(() => useAsync<string>());
const asyncFn = vi.fn(async () => 'test data');
await result.current.execute(asyncFn);
expect(result.current.loading).toBe(false);
expect(result.current.data).toBe('test data');
expect(result.current.error).toBeNull();
});
it('should set loading to false after failed execution', async () => {
const { result } = renderHook(() => useAsync<string>());
const error = new Error('Test error');
const asyncFn = vi.fn(async () => {
throw error;
});
try {
await result.current.execute(asyncFn);
} catch (e) {
// Expected to throw
}
expect(result.current.loading).toBe(false);
expect(result.current.data).toBeNull();
expect(result.current.error).toBe(error);
});
it('should reset data and error when starting new execution', async () => {
const { result } = renderHook(() => useAsync<string>());
// First execution with data
await result.current.execute(async () => 'first data');
expect(result.current.data).toBe('first data');
expect(result.current.error).toBeNull();
// Second execution that will take some time
const slowAsyncFn = vi.fn(() => new Promise<string>((resolve) => {
setTimeout(() => resolve('second data'), 100);
}));
result.current.execute(slowAsyncFn);
// Immediately after calling execute, state should be reset
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
});
});
describe('data flow', () => {
it('should set data on successful execution', async () => {
const { result } = renderHook(() => useAsync<string>());
const testData = 'success data';
const asyncFn = vi.fn(async () => testData);
await result.current.execute(asyncFn);
expect(result.current.data).toBe(testData);
expect(result.current.error).toBeNull();
expect(result.current.loading).toBe(false);
expect(asyncFn).toHaveBeenCalledTimes(1);
});
it('should handle different data types', async () => {
// Test with number
const { result: numberResult } = renderHook(() => useAsync<number>());
await numberResult.current.execute(async () => 42);
expect(numberResult.current.data).toBe(42);
// Test with object
const { result: objectResult } = renderHook(() => useAsync<{ id: number; name: string }>());
const testObject = { id: 1, name: 'test' };
await objectResult.current.execute(async () => testObject);
expect(objectResult.current.data).toEqual(testObject);
// Test with array
const { result: arrayResult } = renderHook(() => useAsync<string[]>());
const testArray = ['a', 'b', 'c'];
await arrayResult.current.execute(async () => testArray);
expect(arrayResult.current.data).toEqual(testArray);
});
it('should return data from execute function', async () => {
const { result } = renderHook(() => useAsync<string>());
const testData = 'returned data';
const asyncFn = vi.fn(async () => testData);
const returnedData = await result.current.execute(asyncFn);
expect(returnedData).toBe(testData);
});
it('should handle null data', async () => {
const { result } = renderHook(() => useAsync<string | null>());
const asyncFn = vi.fn(async () => null);
await result.current.execute(asyncFn);
expect(result.current.data).toBeNull();
expect(result.current.error).toBeNull();
expect(result.current.loading).toBe(false);
});
it('should replace previous data with new data', async () => {
const { result } = renderHook(() => useAsync<string>());
await result.current.execute(async () => 'first');
expect(result.current.data).toBe('first');
await result.current.execute(async () => 'second');
expect(result.current.data).toBe('second');
await result.current.execute(async () => 'third');
expect(result.current.data).toBe('third');
});
});
describe('error handling', () => {
it('should set error on failed execution', async () => {
const { result } = renderHook(() => useAsync<string>());
const error = new Error('Test error');
const asyncFn = vi.fn(async () => {
throw error;
});
try {
await result.current.execute(asyncFn);
} catch (e) {
// Expected to throw
}
expect(result.current.data).toBeNull();
expect(result.current.error).toBe(error);
expect(result.current.loading).toBe(false);
});
it('should call handleError with error and context', async () => {
const { result } = renderHook(() => useAsync<string>());
const error = new Error('Test error');
const asyncFn = vi.fn(async () => {
throw error;
});
try {
await result.current.execute(asyncFn);
} catch (e) {
// Expected to throw
}
expect(errorHandling.handleError).toHaveBeenCalledWith(error, 'Async Operation');
});
it('should re-throw the handled error', async () => {
const { result } = renderHook(() => useAsync<string>());
const originalError = new Error('Test error');
const asyncFn = vi.fn(async () => {
throw originalError;
});
await expect(result.current.execute(asyncFn)).rejects.toThrow();
});
it('should handle errors of different types', async () => {
const { result } = renderHook(() => useAsync<string>());
// Test with Error object
const error1 = new Error('Error object');
await expect(result.current.execute(async () => {
throw error1;
})).rejects.toThrow();
// Test with string error
await expect(result.current.execute(async () => {
throw 'String error';
})).rejects.toThrow();
// Test with number error
await expect(result.current.execute(async () => {
throw 404;
})).rejects.toThrow();
});
it('should clear error on successful subsequent execution', async () => {
const { result } = renderHook(() => useAsync<string>());
// First execution fails
try {
await result.current.execute(async () => {
throw new Error('First error');
});
} catch (e) {
// Expected to throw
}
expect(result.current.error).not.toBeNull();
// Second execution succeeds
await result.current.execute(async () => 'success');
expect(result.current.error).toBeNull();
expect(result.current.data).toBe('success');
});
it('should replace previous error with new error', async () => {
const { result } = renderHook(() => useAsync<string>());
const error1 = new Error('First error');
const error2 = new Error('Second error');
// First execution fails
try {
await result.current.execute(async () => {
throw error1;
});
} catch (e) {
// Expected to throw
}
expect(result.current.error).toBe(error1);
// Second execution also fails
try {
await result.current.execute(async () => {
throw error2;
});
} catch (e) {
// Expected to throw
}
expect(result.current.error).toBe(error2);
});
});
describe('execute function stability', () => {
it('should maintain the same execute function reference', () => {
const { result, rerender } = renderHook(() => useAsync<string>());
const firstExecute = result.current.execute;
rerender();
const secondExecute = result.current.execute;
expect(firstExecute).toBe(secondExecute);
});
});
describe('concurrent executions', () => {
it('should handle multiple concurrent executions', async () => {
const { result } = renderHook(() => useAsync<string>());
const slowAsyncFn = vi.fn(() => new Promise<string>((resolve) => {
setTimeout(() => resolve('slow result'), 100);
}));
const fastAsyncFn = vi.fn(() => new Promise<string>((resolve) => {
setTimeout(() => resolve('fast result'), 10);
}));
// Start slow execution
const slowPromise = result.current.execute(slowAsyncFn);
// Start fast execution (will reset state)
const fastPromise = result.current.execute(fastAsyncFn);
// Fast one should complete
await fastPromise;
expect(result.current.data).toBe('fast result');
// Slow one should also complete, but will override the fast result
await slowPromise;
// Note: In the current implementation, the last executed function will set the final state
});
});
});
-463
View File
@@ -1,463 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { getAllBlogPosts, getBlogPostBySlug, getBlogPostSlugs, type BlogPost } from './blog';
import fs from 'fs';
import path from 'path';
// Mock fs and path modules
vi.mock('fs');
vi.mock('path');
describe('blog', () => {
const mockBlogPostsDir = '/mock/blog-posts';
beforeEach(() => {
vi.clearAllMocks();
// Mock path.join to return predictable paths
vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
// Mock process.cwd
vi.stubGlobal('process', {
...process,
cwd: () => '/mock'
});
});
describe('getAllBlogPosts', () => {
it('should return empty array when directory does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const posts = getAllBlogPosts();
expect(posts).toEqual([]);
expect(fs.existsSync).toHaveBeenCalled();
});
it('should return empty array when directory is empty', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([]);
const posts = getAllBlogPosts();
expect(posts).toEqual([]);
});
it('should parse and return blog posts from markdown files', () => {
const mockContent = `# Test Blog Post
**Meta-Description:** This is a test blog post about testing.
**Keywords:** testing, vitest, typescript
## Einführung
This is the introduction to the blog post. It provides an overview of what will be covered.
## Main Content
Here is the main content of the blog post.`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test-blog-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts).toHaveLength(1);
expect(posts[0]).toMatchObject({
slug: 'test-blog-post',
title: 'Test Blog Post',
excerpt: 'This is a test blog post about testing.',
tags: ['testing', 'vitest', 'typescript'],
category: 'Web Development'
});
expect(posts[0].date).toBe('2026-01-19');
expect(posts[0].coverImage).toBe('/images/posts/test-blog-post/cover.jpg');
expect(posts[0].content).toBe(mockContent);
});
it('should sort posts by date descending (newest first)', () => {
const mockContent1 = '# Post 1\n\n**Meta-Description:** First post';
const mockContent2 = '# Post 2\n\n**Meta-Description:** Second post';
const mockContent3 = '# Post 3\n\n**Meta-Description:** Third post';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-first-post.md',
'002-second-post.md',
'003-third-post.md'
] as any);
vi.mocked(fs.readFileSync)
.mockReturnValueOnce(mockContent1)
.mockReturnValueOnce(mockContent2)
.mockReturnValueOnce(mockContent3);
const posts = getAllBlogPosts();
expect(posts).toHaveLength(3);
// Posts should be sorted newest first (higher numbers = older posts)
expect(posts[0].slug).toBe('first-post');
expect(posts[1].slug).toBe('second-post');
expect(posts[2].slug).toBe('third-post');
expect(new Date(posts[0].date).getTime()).toBeGreaterThanOrEqual(new Date(posts[1].date).getTime());
expect(new Date(posts[1].date).getTime()).toBeGreaterThanOrEqual(new Date(posts[2].date).getTime());
});
it('should filter out non-markdown files', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-test-post.md',
'README.txt',
'image.png',
'002-another-post.md',
'config.json'
] as any);
const mockContent = '# Test\n\n**Meta-Description:** Test';
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts).toHaveLength(2);
expect(fs.readFileSync).toHaveBeenCalledTimes(2);
});
it('should detect category from filename - AI Agents', () => {
const mockContent = '# AI Agent Post\n\n**Meta-Description:** About AI agents';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-agentic-workflow.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].category).toBe('KI-Agenten');
});
it('should detect category from filename - Voice AI', () => {
const mockContent = '# Voice AI Post\n\n**Meta-Description:** About voice technology';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-vapi-integration.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].category).toBe('Voice AI');
});
it('should detect category from content when not in filename', () => {
const mockContent = `# My Post
**Meta-Description:** A post about automation
I love using n8n for automation workflows.`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-my-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].category).toBe('Automatisierung');
});
it('should use default category when no keywords match', () => {
const mockContent = '# Generic Post\n\n**Meta-Description:** Generic content';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-generic-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].category).toBe('Technologie');
});
it('should extract tags from keywords field', () => {
const mockContent = `# Test Post
**Meta-Description:** Test description
**Keywords:** javascript, typescript, testing, vitest, unit-tests, integration-tests, extra-tag`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
// Should limit to 6 tags
expect(posts[0].tags).toHaveLength(6);
expect(posts[0].tags).toEqual([
'javascript',
'typescript',
'testing',
'vitest',
'unit-tests',
'integration-tests'
]);
});
it('should handle posts without keywords', () => {
const mockContent = '# Test Post\n\n**Meta-Description:** Test';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].tags).toEqual([]);
});
it('should use introduction as excerpt when meta description is missing', () => {
const mockContent = `# Test Post
## Einführung
This is a very long introduction that should be truncated to 200 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
## Main Content`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].excerpt).toHaveLength(203); // 200 chars + '...'
expect(posts[0].excerpt).toContain('This is a very long introduction');
expect(posts[0].excerpt).toMatch(/\.\.\.$/);
});
it('should use title as excerpt when both meta description and introduction are missing', () => {
const mockContent = '# Test Post Title\n\nSome content';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].excerpt).toBe('Test Post Title');
});
it('should handle posts without H1 title', () => {
const mockContent = 'No title here\n\n**Meta-Description:** Test';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-fallback-title.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].title).toBe('001-fallback-title');
});
it('should skip posts that fail to parse', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-good-post.md',
'002-bad-post.md'
] as any);
vi.mocked(fs.readFileSync)
.mockReturnValueOnce('# Good Post\n\n**Meta-Description:** Good')
.mockImplementationOnce(() => {
throw new Error('File read error');
});
const posts = getAllBlogPosts();
expect(posts).toHaveLength(1);
expect(posts[0].title).toBe('Good Post');
});
it('should generate correct dates based on post numbers', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-first.md',
'005-fifth.md',
'010-tenth.md'
] as any);
const mockContent = '# Post\n\n**Meta-Description:** Test';
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
// Post 001 should be 2026-01-19
expect(posts.find(p => p.slug === 'first')?.date).toBe('2026-01-19');
// Post 005 should be 4 days earlier (2026-01-15)
expect(posts.find(p => p.slug === 'fifth')?.date).toBe('2026-01-15');
// Post 010 should be 9 days earlier (2026-01-10)
expect(posts.find(p => p.slug === 'tenth')?.date).toBe('2026-01-10');
});
it('should trim whitespace from extracted fields', () => {
const mockContent = `# Test Post With Spaces
**Meta-Description:** Description with spaces
**Keywords:** tag1 , tag2 , tag3 `;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const posts = getAllBlogPosts();
expect(posts[0].title).toBe('Test Post With Spaces');
expect(posts[0].excerpt).toBe('Description with spaces');
expect(posts[0].tags).toEqual(['tag1', 'tag2', 'tag3']);
});
});
describe('getBlogPostBySlug', () => {
it('should return post when slug matches', () => {
const mockContent = '# Test Post\n\n**Meta-Description:** Test';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const post = getBlogPostBySlug('test-post');
expect(post).not.toBeNull();
expect(post?.slug).toBe('test-post');
expect(post?.title).toBe('Test Post');
});
it('should return null when slug does not match', () => {
const mockContent = '# Test Post\n\n**Meta-Description:** Test';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-test-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const post = getBlogPostBySlug('non-existent-slug');
expect(post).toBeNull();
});
it('should return null when no posts exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([]);
const post = getBlogPostBySlug('any-slug');
expect(post).toBeNull();
});
it('should return null when directory does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const post = getBlogPostBySlug('any-slug');
expect(post).toBeNull();
});
it('should return correct post when multiple posts exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-first-post.md',
'002-second-post.md',
'003-third-post.md'
] as any);
vi.mocked(fs.readFileSync)
.mockReturnValueOnce('# First Post\n\n**Meta-Description:** First')
.mockReturnValueOnce('# Second Post\n\n**Meta-Description:** Second')
.mockReturnValueOnce('# Third Post\n\n**Meta-Description:** Third');
const post = getBlogPostBySlug('second-post');
expect(post).not.toBeNull();
expect(post?.slug).toBe('second-post');
expect(post?.title).toBe('Second Post');
expect(post?.excerpt).toBe('Second');
});
it('should return post with all content included', () => {
const mockContent = `# Full Post
**Meta-Description:** Description
**Keywords:** tag1, tag2
## Einführung
Introduction text
## Main Content
This is the main content of the post with lots of details.`;
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue(['001-full-post.md'] as any);
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const post = getBlogPostBySlug('full-post');
expect(post?.content).toBe(mockContent);
});
});
describe('getBlogPostSlugs', () => {
it('should return all post slugs', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'001-first-post.md',
'002-second-post.md',
'003-third-post.md'
] as any);
const mockContent = '# Post\n\n**Meta-Description:** Test';
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const slugs = getBlogPostSlugs();
expect(slugs).toHaveLength(3);
expect(slugs).toContain('first-post');
expect(slugs).toContain('second-post');
expect(slugs).toContain('third-post');
});
it('should return empty array when no posts exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([]);
const slugs = getBlogPostSlugs();
expect(slugs).toEqual([]);
});
it('should return empty array when directory does not exist', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const slugs = getBlogPostSlugs();
expect(slugs).toEqual([]);
});
it('should return slugs in date order (newest first)', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
'003-third.md',
'001-first.md',
'002-second.md'
] as any);
const mockContent = '# Post\n\n**Meta-Description:** Test';
vi.mocked(fs.readFileSync).mockReturnValue(mockContent);
const slugs = getBlogPostSlugs();
expect(slugs).toEqual(['first', 'second', 'third']);
});
});
});
+20 -12
View File
@@ -1,5 +1,6 @@
import fs from 'fs';
import path from 'path';
import { cache } from '@/utils/cache';
export interface BlogPost {
slug: string;
@@ -130,6 +131,14 @@ function parseMarkdownPost(filename: string, content: string): BlogPost | null {
}
export function getAllBlogPosts(): BlogPost[] {
const cacheKey = 'blog:all-posts';
// Check cache first
const cachedPosts = cache.get<BlogPost[]>(cacheKey);
if (cachedPosts) {
return cachedPosts;
}
if (!fs.existsSync(BLOG_POSTS_DIR)) {
console.warn('Blog posts directory not found:', BLOG_POSTS_DIR);
return [];
@@ -142,22 +151,21 @@ export function getAllBlogPosts(): BlogPost[] {
const posts: BlogPost[] = [];
for (const file of files) {
try {
const filePath = path.join(BLOG_POSTS_DIR, file);
const content = fs.readFileSync(filePath, 'utf-8');
const post = parseMarkdownPost(file, content);
if (post) {
posts.push(post);
}
} catch (error) {
// Skip files that fail to parse
console.warn(`Failed to parse blog post: ${file}`, error);
continue;
const filePath = path.join(BLOG_POSTS_DIR, file);
const content = fs.readFileSync(filePath, 'utf-8');
const post = parseMarkdownPost(file, content);
if (post) {
posts.push(post);
}
}
// Sort by date descending (newest first)
return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const sortedPosts = posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
// Cache the results
cache.set(cacheKey, sortedPosts);
return sortedPosts;
}
export function getBlogPostBySlug(slug: string): BlogPost | null {
-282
View File
@@ -1,282 +0,0 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { cache } from './cache';
describe('Cache', () => {
beforeEach(() => {
vi.useFakeTimers();
cache.clear(); // Clear cache before each test
});
afterEach(() => {
vi.useRealTimers();
cache.clear(); // Clean up after each test
});
describe('set and get', () => {
it('should set and retrieve a value', () => {
cache.set('key', 'value');
expect(cache.get('key')).toBe('value');
});
it('should set multiple values with different keys', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
expect(cache.get('key1')).toBe('value1');
expect(cache.get('key2')).toBe('value2');
expect(cache.get('key3')).toBe('value3');
});
it('should overwrite existing value for same key', () => {
cache.set('key', 'value1');
expect(cache.get('key')).toBe('value1');
cache.set('key', 'value2');
expect(cache.get('key')).toBe('value2');
});
it('should return null for non-existing key', () => {
expect(cache.get('nonexistent')).toBeNull();
});
it('should store different types of values', () => {
cache.set('string', 'text');
cache.set('number', 42);
cache.set('boolean', true);
cache.set('object', { name: 'test' });
cache.set('array', [1, 2, 3]);
cache.set('null', null);
expect(cache.get('string')).toBe('text');
expect(cache.get('number')).toBe(42);
expect(cache.get('boolean')).toBe(true);
expect(cache.get('object')).toEqual({ name: 'test' });
expect(cache.get('array')).toEqual([1, 2, 3]);
expect(cache.get('null')).toBeNull();
});
});
describe('TTL expiration', () => {
it('should use default TTL of 5 minutes', () => {
cache.set('test', 'value');
// Should be available before TTL expires
expect(cache.get('test')).toBe('value');
// Advance time by 4 minutes 59 seconds
vi.advanceTimersByTime(4 * 60 * 1000 + 59 * 1000);
expect(cache.get('test')).toBe('value');
// Advance time by 2 more seconds (total 5 minutes 1 second)
vi.advanceTimersByTime(2 * 1000);
expect(cache.get('test')).toBeNull();
});
it('should accept custom TTL per item', () => {
cache.set('short', 'value', 1000); // 1 second TTL
cache.set('long', 'value', 10000); // 10 seconds TTL
// Both should be available initially
expect(cache.get('short')).toBe('value');
expect(cache.get('long')).toBe('value');
// After 1.5 seconds, short should expire but long should remain
vi.advanceTimersByTime(1500);
expect(cache.get('short')).toBeNull();
expect(cache.get('long')).toBe('value');
// After 9 more seconds (total 10.5), long should expire
vi.advanceTimersByTime(9000);
expect(cache.get('long')).toBeNull();
});
it('should return null for expired item', () => {
cache.set('key', 'value', 1000);
// Should be available initially
expect(cache.get('key')).toBe('value');
// Should be null after expiration
vi.advanceTimersByTime(1001);
expect(cache.get('key')).toBeNull();
});
it('should delete expired item from storage', () => {
cache.set('key', 'value', 1000);
// Item should exist
expect(cache.get('key')).toBe('value');
// After expiration, get should delete it
vi.advanceTimersByTime(1001);
expect(cache.get('key')).toBeNull();
// Subsequent get should also return null (item was deleted)
expect(cache.get('key')).toBeNull();
});
it('should return value at exact TTL boundary', () => {
cache.set('key', 'value', 1000);
// At exactly TTL time, should still be valid
vi.advanceTimersByTime(1000);
expect(cache.get('key')).toBe('value');
// One millisecond later, should expire
vi.advanceTimersByTime(1);
expect(cache.get('key')).toBeNull();
});
it('should respect different TTLs for different items', () => {
cache.set('item1', 'value1', 1000);
cache.set('item2', 'value2', 2000);
cache.set('item3', 'value3', 3000);
// All items should be available initially
expect(cache.get('item1')).toBe('value1');
expect(cache.get('item2')).toBe('value2');
expect(cache.get('item3')).toBe('value3');
// After 1.5 seconds, item1 should expire
vi.advanceTimersByTime(1500);
expect(cache.get('item1')).toBeNull();
expect(cache.get('item2')).toBe('value2');
expect(cache.get('item3')).toBe('value3');
// After 1 more second (total 2.5), item2 should expire
vi.advanceTimersByTime(1000);
expect(cache.get('item1')).toBeNull();
expect(cache.get('item2')).toBeNull();
expect(cache.get('item3')).toBe('value3');
// After 1 more second (total 3.5), item3 should expire
vi.advanceTimersByTime(1000);
expect(cache.get('item1')).toBeNull();
expect(cache.get('item2')).toBeNull();
expect(cache.get('item3')).toBeNull();
});
it('should reset TTL when item is overwritten', () => {
cache.set('key', 'value1', 1000);
// Advance time by 500ms
vi.advanceTimersByTime(500);
// Overwrite with new value and new TTL
cache.set('key', 'value2', 2000);
// After 1500ms more (total 2000ms from first set), should still be available
// because TTL was reset
vi.advanceTimersByTime(1500);
expect(cache.get('key')).toBe('value2');
// After 600ms more (2100ms from second set), should expire
vi.advanceTimersByTime(600);
expect(cache.get('key')).toBeNull();
});
it('should handle zero TTL', () => {
cache.set('key', 'value', 0);
// Should expire immediately
vi.advanceTimersByTime(1);
expect(cache.get('key')).toBeNull();
});
it('should handle very long TTL', () => {
const oneDayInMs = 24 * 60 * 60 * 1000;
cache.set('key', 'value', oneDayInMs);
// Should be available after 23 hours
vi.advanceTimersByTime(23 * 60 * 60 * 1000);
expect(cache.get('key')).toBe('value');
// Should still be available at exactly 24 hours
vi.advanceTimersByTime(60 * 60 * 1000);
expect(cache.get('key')).toBe('value');
// Should expire after 24 hours + 1ms
vi.advanceTimersByTime(1);
expect(cache.get('key')).toBeNull();
});
});
describe('has', () => {
it('should return true for existing key', () => {
cache.set('key', 'value');
expect(cache.has('key')).toBe(true);
});
it('should return false for non-existing key', () => {
expect(cache.has('nonexistent')).toBe(false);
});
it('should return false for expired key', () => {
cache.set('key', 'value', 1000);
expect(cache.has('key')).toBe(true);
vi.advanceTimersByTime(1001);
expect(cache.has('key')).toBe(false);
});
it('should return false for null values', () => {
cache.set('key', null);
expect(cache.has('key')).toBe(false);
});
});
describe('delete', () => {
it('should delete existing key', () => {
cache.set('key', 'value');
expect(cache.get('key')).toBe('value');
cache.delete('key');
expect(cache.get('key')).toBeNull();
});
it('should handle deleting non-existing key', () => {
expect(() => cache.delete('nonexistent')).not.toThrow();
expect(cache.get('nonexistent')).toBeNull();
});
it('should only delete specified key', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
cache.delete('key2');
expect(cache.get('key1')).toBe('value1');
expect(cache.get('key2')).toBeNull();
expect(cache.get('key3')).toBe('value3');
});
});
describe('clear', () => {
it('should clear all items from cache', () => {
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('key3', 'value3');
cache.clear();
expect(cache.get('key1')).toBeNull();
expect(cache.get('key2')).toBeNull();
expect(cache.get('key3')).toBeNull();
});
it('should handle clearing empty cache', () => {
expect(() => cache.clear()).not.toThrow();
});
it('should allow setting items after clear', () => {
cache.set('key', 'value1');
cache.clear();
cache.set('key', 'value2');
expect(cache.get('key')).toBe('value2');
});
});
});
-165
View File
@@ -1,165 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AppError, errorCodes, handleError, createAPIError } from './errorHandling';
import * as analytics from './analytics';
// Mock the analytics module
vi.mock('./analytics', () => ({
trackEvent: vi.fn()
}));
describe('errorHandling', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('AppError', () => {
it('should create an AppError with all properties', () => {
const error = new AppError('Test error', 'TEST_CODE', 'error', { key: 'value' });
expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(AppError);
expect(error.message).toBe('Test error');
expect(error.code).toBe('TEST_CODE');
expect(error.severity).toBe('error');
expect(error.metadata).toEqual({ key: 'value' });
expect(error.name).toBe('AppError');
});
it('should create an AppError with default severity', () => {
const error = new AppError('Test error', 'TEST_CODE');
expect(error.severity).toBe('error');
expect(error.metadata).toBeUndefined();
});
it('should create an AppError with warning severity', () => {
const error = new AppError('Test warning', 'TEST_CODE', 'warning');
expect(error.severity).toBe('warning');
});
it('should create an AppError with info severity', () => {
const error = new AppError('Test info', 'TEST_CODE', 'info');
expect(error.severity).toBe('info');
});
});
describe('errorCodes', () => {
it('should have all error codes defined', () => {
expect(errorCodes.NETWORK_ERROR).toBe('ERR_NETWORK');
expect(errorCodes.API_ERROR).toBe('ERR_API');
expect(errorCodes.AUTH_ERROR).toBe('ERR_AUTH');
expect(errorCodes.VALIDATION_ERROR).toBe('ERR_VALIDATION');
expect(errorCodes.UNKNOWN_ERROR).toBe('ERR_UNKNOWN');
});
});
describe('handleError', () => {
it('should handle AppError and return it unchanged', () => {
const originalError = new AppError('Test error', 'TEST_CODE', 'error', { foo: 'bar' });
const result = handleError(originalError, 'test-context');
expect(result).toBe(originalError);
expect(result.message).toBe('Test error');
expect(result.code).toBe('TEST_CODE');
expect(result.severity).toBe('error');
expect(result.metadata).toEqual({ foo: 'bar' });
expect(analytics.trackEvent).toHaveBeenCalledWith(
'Error',
'TEST_CODE',
'test-context: Test error'
);
});
it('should convert regular Error to AppError', () => {
const originalError = new Error('Regular error');
const result = handleError(originalError, 'test-context');
expect(result).toBeInstanceOf(AppError);
expect(result.message).toBe('Regular error');
expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
expect(result.severity).toBe('error');
expect(result.metadata).toEqual({ originalError });
expect(analytics.trackEvent).toHaveBeenCalledWith(
'Error',
errorCodes.UNKNOWN_ERROR,
'test-context: Regular error'
);
});
it('should handle unknown error types', () => {
const result = handleError('string error', 'test-context');
expect(result).toBeInstanceOf(AppError);
expect(result.message).toBe('An unexpected error occurred');
expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
expect(result.severity).toBe('error');
expect(result.metadata).toEqual({ originalError: 'string error' });
expect(analytics.trackEvent).toHaveBeenCalledWith(
'Error',
errorCodes.UNKNOWN_ERROR,
'test-context: An unexpected error occurred'
);
});
it('should handle null error', () => {
const result = handleError(null, 'test-context');
expect(result).toBeInstanceOf(AppError);
expect(result.message).toBe('An unexpected error occurred');
expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
expect(result.metadata).toEqual({ originalError: null });
});
it('should handle undefined error', () => {
const result = handleError(undefined, 'test-context');
expect(result).toBeInstanceOf(AppError);
expect(result.message).toBe('An unexpected error occurred');
expect(result.code).toBe(errorCodes.UNKNOWN_ERROR);
expect(result.metadata).toEqual({ originalError: undefined });
});
it('should track error in analytics', () => {
const error = new AppError('Analytics test', 'ANALYTICS_CODE');
handleError(error, 'analytics-context');
expect(analytics.trackEvent).toHaveBeenCalledTimes(1);
expect(analytics.trackEvent).toHaveBeenCalledWith(
'Error',
'ANALYTICS_CODE',
'analytics-context: Analytics test'
);
});
});
describe('createAPIError', () => {
it('should create an API error with status code', () => {
const error = createAPIError(404, 'Not found');
expect(error).toBeInstanceOf(AppError);
expect(error.message).toBe('Not found');
expect(error.code).toBe(errorCodes.API_ERROR);
expect(error.severity).toBe('error');
expect(error.metadata).toEqual({ status: 404 });
});
it('should create an API error for 500 status', () => {
const error = createAPIError(500, 'Internal server error');
expect(error).toBeInstanceOf(AppError);
expect(error.message).toBe('Internal server error');
expect(error.code).toBe(errorCodes.API_ERROR);
expect(error.metadata).toEqual({ status: 500 });
});
it('should create an API error for 401 status', () => {
const error = createAPIError(401, 'Unauthorized');
expect(error).toBeInstanceOf(AppError);
expect(error.message).toBe('Unauthorized');
expect(error.metadata).toEqual({ status: 401 });
});
});
});
+1
View File
@@ -34,6 +34,7 @@
"src/pages-vite",
"src/hooks",
"src/services",
"src/utils",
"**/*.bak"
]
}
-39
View File
@@ -1,39 +0,0 @@
import { defineConfig } from 'vitest/config';
import { resolve } from 'path';
export default defineConfig({
plugins: [],
test: {
globals: true,
environment: 'jsdom',
setupFiles: [],
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/.next/**',
'**/coverage/**',
'**/*.bak/**',
'**/src/components-vite/**',
'**/src/pages-vite/**'
],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'.next/',
'coverage/',
'**/*.config.{js,ts}',
'**/*.bak',
'src/components-vite/**',
'src/pages-vite/**'
]
}
},
resolve: {
alias: {
'@': resolve(__dirname, './src')
}
}
});