---
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);
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>
+ · <span>{wc} words</span>
+ · <span>{readTime} min</span>
+ </em></p>
+ </div>
+ </div>
+ );
+ })}
+ </Fragment>
+
+ <Fragment slot="menu">
+ <BlogNav currentUrl={currentUrl} />
+ </Fragment>
+</BaseLayout>
---
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>
+ · <span>{wc} words</span>
+ · <span>{readTime} min</span>
+ </em></p>
+ </div>
+ </div>
+ );
+ })}
+ </Fragment>
+
+ <Fragment slot="menu">
+ <BlogNav currentUrl={currentUrl} />
+ </Fragment>
+</BaseLayout>
---
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>
+ · <span>{wc} words</span>
+ · <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>