diff --git a/src/app/[locale]/blog/[slug]/page.tsx b/src/app/[locale]/blog/[slug]/page.tsx index 3b3dd93..ae1256d 100644 --- a/src/app/[locale]/blog/[slug]/page.tsx +++ b/src/app/[locale]/blog/[slug]/page.tsx @@ -5,8 +5,9 @@ import Link from 'next/link'; import Image from 'next/image'; import type { Metadata } from 'next'; import { BreadcrumbJsonLd, ArticleJsonLd } from '@/components/seo'; -import { getBlogPostBySlug, getBlogPostSlugs } from '@/lib/blog'; +import { getBlogPostBySlug, getBlogPostSlugs, calculateReadingTime } from '@/lib/blog'; import { renderMarkdown } from '@/lib/markdown'; +import { Clock } from 'lucide-react'; type Props = { params: Promise<{ locale: string; slug: string }>; @@ -1315,6 +1316,9 @@ export default async function BlogPostPage({ params }: Props) { { name: post.title, url: `/${locale}/blog/${slug}` }, ]; + // Calculate reading time + const readingTime = post.content ? calculateReadingTime(post.content) : 1; + return (
@@ -1343,6 +1347,10 @@ export default async function BlogPostPage({ params }: Props) {
+
+ + {readingTime} min read +
{post.category} diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx index 5d475a4..6a44936 100644 --- a/src/app/[locale]/blog/page.tsx +++ b/src/app/[locale]/blog/page.tsx @@ -1,19 +1,17 @@ import { setRequestLocale } from 'next-intl/server'; import { getTranslations } from 'next-intl/server'; -import { ArrowRight } from 'lucide-react'; +import { ArrowRight, Calendar, Tag, ExternalLink, ChevronLeft, ChevronRight, Clock } from 'lucide-react'; +import Link from 'next/link'; +import Image from 'next/image'; import type { Metadata } from 'next'; import { getAllBlogPosts, BlogPost } from '@/lib/blog'; -<<<<<<< HEAD -import { BlogList } from '@/components/blog/BlogList'; -======= import { legacyBlogPosts } from '@/data/legacyBlogPosts'; const POSTS_PER_PAGE = 12; ->>>>>>> origin/master type Props = { params: Promise<{ locale: string }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; + searchParams: Promise<{ page?: string }>; }; const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://damjan-savic.com'; @@ -84,8 +82,6 @@ export async function generateMetadata({ params }: Props): Promise { }; } -<<<<<<< HEAD -======= function getFormattedDate(date: string, locale: string) { const languageMap: Record = { de: 'de-DE', @@ -156,6 +152,12 @@ function BlogPostCard({ post, locale }: { post: BlogPost; locale: string }) { {getFormattedDate(post.date, locale)}
+ {post.readingTime && ( +
+ + {post.readingTime} min read +
+ )} {post.tags && post.tags.length > 0 && (
@@ -338,11 +340,10 @@ function Pagination({ ); } ->>>>>>> origin/master export default async function BlogPage({ params, searchParams }: Props) { const { locale } = await params; - const resolvedSearchParams = await searchParams; + const { page } = await searchParams; setRequestLocale(locale); const t = await getTranslations('blog'); @@ -360,9 +361,14 @@ export default async function BlogPage({ params, searchParams }: Props) { ...legacyPosts, ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - // Extract search and category from URL params - const initialSearch = typeof resolvedSearchParams.search === 'string' ? resolvedSearchParams.search : ''; - const initialCategory = typeof resolvedSearchParams.category === 'string' ? resolvedSearchParams.category : null; + // 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 (
@@ -379,23 +385,37 @@ export default async function BlogPage({ params, searchParams }: Props) {

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

+ {/* Post count */} +

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

- {/* Blog List with Search and Filters */} - + {posts.map((post) => ( + + ))} + + + {/* Pagination */} + - t('ui.pagination.showing', { start, end, total }), - noPostsText: t('ui.errors.posts'), - paginationPrevious: t('ui.pagination.previous'), - paginationNext: t('ui.pagination.next'), - }} + t={(key) => t(key)} /> + + {/* Empty State */} + {posts.length === 0 && ( +
+

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

+
+ )}
); diff --git a/src/lib/blog.ts b/src/lib/blog.ts index ec45c80..5b43a8e 100644 --- a/src/lib/blog.ts +++ b/src/lib/blog.ts @@ -28,6 +28,7 @@ export interface BlogPost { tags?: string[]; /** Vollständiger Markdown-Inhalt */ content?: string; + readingTime?: number; } // Verzeichnis der Blog-Beiträge @@ -99,6 +100,31 @@ function detectCategory(filename: string, content: string): string { return 'Technologie'; } +/** + * Berechnet die geschätzte Lesezeit für einen Blog-Post + * @param content - Der Markdown-Inhalt des Posts + * @returns Lesezeit in Minuten (mindestens 1) + */ +export function calculateReadingTime(content: string): number { + // Remove markdown syntax for more accurate word count + const text = content + .replace(/```[\s\S]*?```/g, '') // Remove code blocks + .replace(/`[^`]*`/g, '') // Remove inline code + .replace(/!\[.*?\]\(.*?\)/g, '') // Remove images + .replace(/\[.*?\]\(.*?\)/g, '') // Remove links + .replace(/[#*_~]/g, '') // Remove markdown formatting + .replace(/\s+/g, ' ') // Normalize whitespace + .trim(); + + // Count words + const words = text.split(/\s+/).filter(word => word.length > 0).length; + + // Calculate reading time (average reading speed: 200 words per minute) + const minutes = Math.ceil(words / 200); + + return Math.max(1, minutes); // Minimum 1 minute +} + /** * Parst einen Markdown-Blog-Post und extrahiert alle Metadaten * Extrahiert Titel, Meta-Description, Keywords, Kategorie und generiert Slug @@ -149,6 +175,9 @@ function parseMarkdownPost(filename: string, content: string): BlogPost | null { // Cover image path const coverImage = `/images/posts/${slug}/cover.jpg`; + // Calculate reading time + const readingTime = calculateReadingTime(content); + return { slug, title, @@ -158,6 +187,7 @@ function parseMarkdownPost(filename: string, content: string): BlogPost | null { category, tags, content, + readingTime, }; } catch (error) { console.error(`Error parsing ${filename}:`, error);