import { setRequestLocale } from 'next-intl/server'; import { getTranslations } from 'next-intl/server'; import { ArrowRight, Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'; 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; type Props = { params: Promise<{ locale: string }>; searchParams: Promise<{ page?: string }>; }; const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com'; export async function generateMetadata({ params }: Props): Promise { const { locale } = await params; const t = await getTranslations({ locale, namespace: 'blog.meta' }); const keywords: Record = { de: [ 'KI Blog', 'Automatisierung Blog', 'Tech Blog', 'KI-Agenten', 'n8n Tutorials', 'Voice AI', 'SaaS Entwicklung', 'Python Tutorials', ], en: [ 'AI Blog', 'Automation Blog', 'Tech Blog', 'AI Agents Guide', 'n8n Tutorials', 'Voice AI Guide', 'SaaS Development', 'Python Tutorials', 'GPT-4 Integration Guide', 'AI Developer Blog', 'Process Automation Tips', ], sr: [ 'AI Blog', 'Automatizacija Blog', 'Tech Blog', 'AI Agenti', 'n8n Tutoriali', 'Voice AI', 'SaaS Razvoj', 'Python Tutoriali', ], }; const localePath = locale === 'de' ? '/blog' : `/${locale}/blog`; return { title: t('title'), description: t('description'), keywords: keywords[locale] || keywords.de, alternates: { canonical: `${BASE_URL}${localePath}`, languages: { 'x-default': `${BASE_URL}/blog`, de: `${BASE_URL}/blog`, en: `${BASE_URL}/en/blog`, sr: `${BASE_URL}/sr/blog`, }, }, openGraph: { title: t('title'), description: t('description'), url: `${BASE_URL}${localePath}`, type: 'website', locale: locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US', alternateLocale: ['de_DE', 'en_US', 'sr_RS'].filter(l => l !== (locale === 'de' ? 'de_DE' : locale === 'sr' ? 'sr_RS' : 'en_US')), }, }; } function getFormattedDate(date: string, locale: string) { const languageMap: Record = { de: 'de-DE', en: 'en-US', sr: 'sr-RS', }; return new Date(date).toLocaleDateString(languageMap[locale] || 'de-DE', { year: 'numeric', month: 'long', day: 'numeric', }); } function getImagePath(coverImage: string) { if (coverImage.startsWith('http')) { return coverImage; } return coverImage.replace('/blog/', '/images/posts/'); } // Known existing images (from public/images/posts/) const EXISTING_IMAGES = new Set([ '/images/posts/erp-integration-breuninger/cover.jpg', '/images/posts/fullstack-development-timetracking/cover.jpg', '/images/posts/rfid-automation/cover.jpg', '/images/posts/automated-ad-creatives/cover.jpg', ]); // Blog image component function BlogImage({ src, alt }: { src: string; alt: string }) { return ( {alt} ); } function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) { return (
{/* Image Container */}
{/* Content */}
{getFormattedDate(post.date, locale)}
{post.tags && post.tags.length > 0 && (
{post.tags.length} Tags
)}

{post.title}

{post.excerpt}

{/* Tags */}
{post.tags?.slice(0, 3).map((tag, index) => ( {tag} ))} {post.tags && post.tags.length > 3 && ( +{post.tags.length - 3} more )}
{/* Link Icon */}
); } // Pagination component function Pagination({ currentPage, totalPages, locale, t, }: { currentPage: number; totalPages: number; locale: string; t: (key: string) => string; }) { const getPageNumbers = () => { const pages: (number | 'ellipsis')[] = []; if (totalPages <= 7) { // Show all pages if 7 or fewer for (let i = 1; i <= totalPages; i++) { pages.push(i); } } else { // Always show first page pages.push(1); if (currentPage > 3) { pages.push('ellipsis'); } // Show pages around current const start = Math.max(2, currentPage - 1); const end = Math.min(totalPages - 1, currentPage + 1); for (let i = start; i <= end; i++) { pages.push(i); } if (currentPage < totalPages - 2) { pages.push('ellipsis'); } // Always show last page pages.push(totalPages); } return pages; }; if (totalPages <= 1) return null; return ( ); } export default async function BlogPage({ params, searchParams }: Props) { const { locale } = await params; const { page } = await searchParams; setRequestLocale(locale); const t = await getTranslations('blog'); // Get dynamic posts from markdown files const dynamicPosts = getAllBlogPosts(); // Get legacy posts (with translations) const legacyPosts = legacyBlogPosts[locale] || legacyBlogPosts.de; // Merge: dynamic posts first, then legacy posts (avoiding duplicates) const legacySlugs = new Set(legacyPosts.map(p => p.slug)); const allPosts = [ ...dynamicPosts.filter(p => !legacySlugs.has(p.slug)), ...legacyPosts, ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); // Pagination const currentPage = Math.max(1, parseInt(page || '1', 10) || 1); const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE); const validPage = Math.min(currentPage, totalPages || 1); const startIndex = (validPage - 1) * POSTS_PER_PAGE; const endIndex = startIndex + POSTS_PER_PAGE; const posts = allPosts.slice(startIndex, endIndex); return (
{/* Header Section */}

BLOG

{t('meta.header.title')}

{t('meta.header.subtitle')}

{/* Post count */}

{t('ui.pagination.showing', { start: startIndex + 1, end: Math.min(endIndex, allPosts.length), total: allPosts.length, })}

{/* Blog Posts Grid */}
{posts.map((post) => ( ))}
{/* Pagination */} t(key)} /> {/* Empty State */} {posts.length === 0 && (

{t('ui.errors.posts')}

)}
); }