From 2d8af6a1344522352704f3520232eff6e2357f1e Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Sat, 6 Jun 2026 10:15:45 +0000 Subject: [PATCH] =?utf8?q?Step=206=20(Abschluss):=20Blog-Listing-Seiten=20?= =?utf8?q?mit=20vollst=C3=A4ndigem=20Layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit blog/index, blog/archive/index und blog/archive/[year] ersetzen das minimale Gerüst aus Schritt 3 durch das vollständige BaseLayout mit Header, Breadcrumb, BlogNav-Sidebar und Footer. Blog-Index zeigt 3 aktuelle Posts mit automatisch extrahierten Zusammen- fassungen (erste Textpassage, ~350 Zeichen) und Browse-Abschnitte für Jahr, Kategorie und Tag. Archive-Index und Jahres-Archive listen Beiträge mit Datum, Wortanzahl und Lesezeit auf – analog zur Hugo-Vorlage. Co-Authored-By: Claude Sonnet 4.6 --- src/pages/blog/archive/[year].astro | 73 ++++++++++--- src/pages/blog/archive/index.astro | 88 ++++++++++++---- src/pages/blog/index.astro | 153 +++++++++++++++++++++++----- 3 files changed, 254 insertions(+), 60 deletions(-) diff --git a/src/pages/blog/archive/[year].astro b/src/pages/blog/archive/[year].astro index ecdde543..79c610d7 100644 --- a/src/pages/blog/archive/[year].astro +++ b/src/pages/blog/archive/[year].astro @@ -1,5 +1,8 @@ --- import { getCollection } from 'astro:content'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import Breadcrumb from '../../../components/Breadcrumb.astro'; +import BlogNav from '../../../components/BlogNav.astro'; export async function getStaticPaths() { const posts = await getCollection('blog', ({ data }) => !data.draft); @@ -16,19 +19,59 @@ export async function getStaticPaths() { const { year } = Astro.params; const { posts } = Astro.props; + +const currentUrl = `/blog/archive/${year}/`; + +function postUrl(post: any): string { + return post.data.url ?? `/${post.id}/`; +} + +function wordCount(body: string): number { + return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length; +} + +const crumbs = [ + { title: 'Home', url: '/' }, + { title: 'Blog', url: '/blog/' }, + { title: 'Archive', url: '/blog/archive/' }, + { title: year }, +]; --- - -Archiv {year} | juplo - -

Archiv {year}

- -

← Alle Jahre

- - + + + + + + + +
+

Archiv - {year}

+
+ {posts.map(post => { + const url = postUrl(post); + const date = post.data.date; + const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); + const dateIso = date.toISOString(); + const wc = wordCount(post.body ?? ''); + const readTime = Math.ceil(wc / 200); + return ( +
+

+ {post.data.title} +

+
+

+ Posted on {dateStr} +  Â· {wc} words +  Â· {readTime} min +

+
+
+ ); + })} +
+ + + + +
diff --git a/src/pages/blog/archive/index.astro b/src/pages/blog/archive/index.astro index 5d7880e3..d4df6fc4 100644 --- a/src/pages/blog/archive/index.astro +++ b/src/pages/blog/archive/index.astro @@ -1,27 +1,73 @@ --- import { getCollection } from 'astro:content'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import Breadcrumb from '../../../components/Breadcrumb.astro'; +import BlogNav from '../../../components/BlogNav.astro'; -const posts = await getCollection('blog', ({ data }) => !data.draft); -const byYear: Record = {}; -for (const post of posts) { - const year = post.data.date.getFullYear().toString(); - byYear[year] = (byYear[year] ?? 0) + 1; +const currentUrl = '/blog/archive/'; + +const posts = (await getCollection('blog', ({ data }) => !data.draft)) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); + +function postUrl(post: any): string { + return post.data.url ?? `/${post.id}/`; +} + +function wordCount(body: string): number { + return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length; } -const years = Object.keys(byYear).sort((a, b) => Number(b) - Number(a)); + +const crumbs = [ + { title: 'Home', url: '/' }, + { title: 'Blog', url: '/blog/' }, + { title: 'Archive' }, +]; --- - -Archiv | juplo - -

Archiv

- -

← Blog

- - + + + {posts.map(post => { + const url = postUrl(post); + const date = post.data.date; + const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); + const dateIso = date.toISOString(); + const wc = wordCount(post.body ?? ''); + const readTime = Math.ceil(wc / 200); + return ( +
+

+ {post.data.title} +

+
+

+ Posted on {dateStr} +  Â· {wc} words +  Â· {readTime} min +

+
+
+ ); + })} + + + + + + diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 5b727ba7..aefbc575 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -1,32 +1,137 @@ --- import { getCollection } from 'astro:content'; +import BaseLayout from '../../layouts/BaseLayout.astro'; +import Breadcrumb from '../../components/Breadcrumb.astro'; +import BlogNav from '../../components/BlogNav.astro'; -const posts = (await getCollection('blog', ({ data }) => !data.draft)) +const currentUrl = '/blog/'; + +const allPosts = (await getCollection('blog', ({ data }) => !data.draft)) .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()); -const byYear: Record = {}; -for (const post of posts) { +const recentPosts = allPosts.slice(0, 3); + +const byYear = new Map(); +for (const post of allPosts) { const year = post.data.date.getFullYear().toString(); - if (!byYear[year]) byYear[year] = []; - byYear[year].push(post); + byYear.set(year, (byYear.get(year) ?? 0) + 1); } -const years = Object.keys(byYear).sort((a, b) => Number(b) - Number(a)); +const years = [...byYear.entries()].sort((a, b) => Number(b[0]) - Number(a[0])); + +const categoryMap = new Map(); +const tagMap = new Map(); +for (const post of allPosts) { + for (const c of post.data.categories ?? []) categoryMap.set(c, (categoryMap.get(c) ?? 0) + 1); + for (const t of post.data.tags ?? []) tagMap.set(t, (tagMap.get(t) ?? 0) + 1); +} +const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); +const categories = [...categoryMap.entries()] + .map(([n, c]) => ({ name: n, displayName: capitalize(n), count: c })) + .sort((a, b) => a.displayName.localeCompare(b.displayName)); +const tags = [...tagMap.entries()] + .map(([n, c]) => ({ name: n, displayName: capitalize(n), count: c })) + .sort((a, b) => a.displayName.localeCompare(b.displayName)); + +function postUrl(post: any): string { + return post.data.url ?? `/${post.id}/`; +} + +function getSummary(body: string): string { + let text = body + .replace(/```[\s\S]*?```/g, '') + .replace(/`[^`]*`/g, '') + .replace(/<[^>]+>/g, '') + .replace(/\{\{[^}]*\}\}/g, '') + .replace(/!\[[^\]]*\]\([^)]*\)/g, '') + .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') + .replace(/^#{1,6}\s+/gm, '') + .replace(/[*_]{1,3}([^*_\n]+)[*_]{1,3}/g, '$1'); + const paras = text.split(/\n{2,}/).map(p => p.trim().replace(/\n/g, ' ')).filter(p => p.length > 20); + return paras[0]?.substring(0, 350) ?? ''; +} + +function wordCount(body: string): number { + return body.replace(/```[\s\S]*?```/g, '').split(/\s+/).filter(w => w.length > 0).length; +} + +const crumbs = [ + { title: 'Home', url: '/' }, + { title: 'Blog' }, +]; --- - -Blog | juplo - -

Blog

-

Recent Posts

- -

Browse By Year

- - - + + + + + + + +

Recently Published Articles

+ {recentPosts.map(post => { + const url = postUrl(post); + const date = post.data.date; + const dateStr = date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); + const dateIso = date.toISOString(); + const wc = wordCount(post.body ?? ''); + const readTime = Math.ceil(wc / 200); + const summary = getSummary(post.body ?? ''); + return ( +
+

+ {post.data.title} +

+
+

+ Posted on {dateStr} +  Â· {wc} words +  Â· {readTime} min +

+
+ {summary && ( +
+ {summary} + {' '}Read on... +
+ )} +
+ ); + })} + +

Browse Articles By Year

+ + +

Browse Articles By Category

+ + +

Browse Articles By Tag

+ +
+ + + + +
-- 2.39.5