]> juplo.de Git - website/commitdiff
Step 6 (Abschluss): Blog-Listing-Seiten mit vollständigem Layout
authorKai Moritz <kai.milan.moritz@googlemail.com>
Sat, 6 Jun 2026 10:15:45 +0000 (10:15 +0000)
committerKai Moritz <kai.milan.moritz@googlemail.com>
Sat, 6 Jun 2026 10:15:45 +0000 (10:15 +0000)
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 <noreply@anthropic.com>
src/pages/blog/archive/[year].astro
src/pages/blog/archive/index.astro
src/pages/blog/index.astro

index ecdde543987d6df8c748abade76f3c1de268275f..79c610d7fd0a7818f51c3e4b9bcedafe12a0f354 100644 (file)
@@ -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 },
+];
 ---
-<html lang="en">
-<head><meta charset="utf-8"><title>Archiv {year} | juplo</title></head>
-<body>
-  <h1>Archiv {year}</h1>
-  <ul>
-    {posts.map(post => (
-      <li>
-        <a href={post.data.url ?? `/${post.id}/`}>{post.data.title}</a>
-        <small> — {post.data.date.toLocaleDateString('de-DE')}</small>
-      </li>
-    ))}
-  </ul>
-  <p><a href="/blog/archive/">← Alle Jahre</a></p>
-</body>
-</html>
+
+<BaseLayout title={`Archiv - ${year}`}>
+  <Fragment slot="breadcrumb">
+    <Breadcrumb crumbs={crumbs} />
+  </Fragment>
+
+  <Fragment slot="article">
+    <header>
+      <h1>Archiv - {year}</h1>
+    </header>
+    {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 (
+        <div class="archive-entry">
+          <h3 class="archive-entry-title entry-hint-parent">
+            <a class="entry-link" aria-label={`post link to ${post.data.title}`} href={url}>{post.data.title}</a>
+          </h3>
+          <div class="archive-meta">
+            <p><em>
+              <span title={dateIso}>Posted on {dateStr}</span>
+              &nbsp;·&nbsp;<span>{wc} words</span>
+              &nbsp;·&nbsp;<span>{readTime} min</span>
+            </em></p>
+          </div>
+        </div>
+      );
+    })}
+  </Fragment>
+
+  <Fragment slot="menu">
+    <BlogNav currentUrl={currentUrl} />
+  </Fragment>
+</BaseLayout>
index 5d7880e3fedf8832d45777954ed1d5f10853d748..d4df6fc47c1b9d2bdb55976e6be33f9d1150bfe5 100644 (file)
@@ -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<string, number> = {};
-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' },
+];
 ---
-<html lang="en">
-<head><meta charset="utf-8"><title>Archiv | juplo</title></head>
-<body>
-  <h1>Archiv</h1>
-  <ul>
-    {years.map(year => (
-      <li>
-        <a href={`/blog/archive/${year}/`}>
-          <span>{year}</span> <span>({byYear[year]})</span>
+
+<BaseLayout title="Archive">
+  <Fragment slot="breadcrumb">
+    <Breadcrumb crumbs={crumbs} />
+  </Fragment>
+
+  <Fragment slot="article">
+    <header>
+      <h1>
+        <a href="/blog/archive/index.xml" title="RSS" aria-label="RSS">
+          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4"
+            stroke-linecap="round" stroke-linejoin="round" height="23">
+            <path d="M4 11a9 9 0 0 1 9 9" />
+            <path d="M4 4a16 16 0 0 1 16 16" />
+            <circle cx="5" cy="19" r="1" />
+          </svg>
+          Archive of all Blog-Articles
         </a>
-      </li>
-    ))}
-  </ul>
-  <p><a href="/blog/">← Blog</a></p>
-</body>
-</html>
+      </h1>
+    </header>
+    {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 (
+        <div class="archive-entry">
+          <h3 class="archive-entry-title entry-hint-parent">
+            <a class="entry-link" aria-label={`post link to ${post.data.title}`} href={url}>{post.data.title}</a>
+          </h3>
+          <div class="archive-meta">
+            <p><em>
+              <span title={dateIso}>Posted on {dateStr}</span>
+              &nbsp;·&nbsp;<span>{wc} words</span>
+              &nbsp;·&nbsp;<span>{readTime} min</span>
+            </em></p>
+          </div>
+        </div>
+      );
+    })}
+  </Fragment>
+
+  <Fragment slot="menu">
+    <BlogNav currentUrl={currentUrl} />
+  </Fragment>
+</BaseLayout>
index 5b727ba7f5d7c544e42314620ec6d058c94b7d4c..aefbc5750e25f9db0298e045b5539ba30d65e3da 100644 (file)
 ---
 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<string, typeof posts> = {};
-for (const post of posts) {
+const recentPosts = allPosts.slice(0, 3);
+
+const byYear = new Map<string, number>();
+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<string, number>();
+const tagMap = new Map<string, number>();
+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' },
+];
 ---
-<html lang="en">
-<head><meta charset="utf-8"><title>Blog | juplo</title></head>
-<body>
-  <h1>Blog</h1>
-  <h2>Recent Posts</h2>
-  <ul>
-    {posts.slice(0, 3).map(post => (
-      <li><a href={post.data.url ?? `/${post.id}/`}>{post.data.title}</a></li>
-    ))}
-  </ul>
-  <h2>Browse By Year</h2>
-  <ul>
-    {years.map(year => (
-      <li><a href={`/blog/archive/${year}/`}>{year} ({byYear[year].length})</a></li>
-    ))}
-  </ul>
-</body>
-</html>
+
+<BaseLayout title="Blog">
+  <Fragment slot="breadcrumb">
+    <Breadcrumb crumbs={crumbs} />
+  </Fragment>
+
+  <Fragment slot="article">
+    <header><h1>Recently Published Articles</h1></header>
+    {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 (
+        <div class="archive-entry">
+          <h2 class="archive-entry-title entry-hint-parent">
+            <a class="entry-link" aria-label={`post link to ${post.data.title}`} href={url}>{post.data.title}</a>
+          </h2>
+          <div class="archive-meta">
+            <p><em>
+              <span title={dateIso}>Posted on {dateStr}</span>
+              &nbsp;·&nbsp;<span>{wc} words</span>
+              &nbsp;·&nbsp;<span>{readTime} min</span>
+            </em></p>
+          </div>
+          {summary && (
+            <div class="archive-summary">
+              {summary}
+              {' '}<a href={url}>Read on...</a>
+            </div>
+          )}
+        </div>
+      );
+    })}
+
+    <header><h1>Browse Articles By Year</h1></header>
+    <ul class="t">
+      {years.map(([year, count]) => (
+        <li>
+          <a href={`/blog/archive/${year}/`}>
+            <span class="title">{year}</span><span class="sep"> | </span><span class="count">{count}</span>
+          </a>
+        </li>
+      ))}
+    </ul>
+
+    <header><h1>Browse Articles By Category</h1></header>
+    <ul class="t">
+      {categories.map(cat => (
+        <li>
+          <a href={`/categories/${cat.name}/`}>
+            <span class="title">{cat.displayName}</span><span class="sep"> | </span><span class="count">{cat.count}</span>
+          </a>
+        </li>
+      ))}
+    </ul>
+
+    <header><h1>Browse Articles By Tag</h1></header>
+    <ul class="t">
+      {tags.map(tag => (
+        <li>
+          <a href={`/tags/${tag.name}/`}>
+            <span class="title">{tag.displayName}</span><span class="sep"> | </span><span class="count">{tag.count}</span>
+          </a>
+        </li>
+      ))}
+    </ul>
+  </Fragment>
+
+  <Fragment slot="menu">
+    <BlogNav currentUrl={currentUrl} />
+  </Fragment>
+</BaseLayout>