Skip to content

Commit 2cf7772

Browse files
committed
feat: add blog categories, minor content tweaks
1 parent d8dd734 commit 2cf7772

8 files changed

Lines changed: 227 additions & 19 deletions

File tree

public/.htaccess

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ RewriteEngine On
77
# and posts were at matthias-andrasch.eu/YYYY/<slug>/
88
# ------------------------------------
99
RewriteRule ^([0-9]{4})/(.*)$ /blog/$1/$2 [R=301,L]
10+
11+
# ------------------------------------
12+
# 2) Old category URLs: /blog/cat/slug → /blog/kategorie/slug
13+
# ------------------------------------
14+
RewriteRule ^blog/cat/(.*)$ /blog/kategorie/$1 [R=301,L]

src/content/pages/absurditaet-des-lebens.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ Warum sind wir eigentlich auf diesem Planeten? Christian Uhle versucht, dieser F
1212
>
1313
> Albert Camus
1414
15-
Mehr dazu: https://workwhileclimate.at/sinn-des-lebens/
15+
Mehr dazu: https://matthias-andrasch.eu/blog/2024/sinn-des-lebens/

src/content/pages/green-coding.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Via "Where Is The Green In The Mindset? Improving Awareness For Green Coding In
2020

2121
**Meine Blogartikel**
2222

23-
- https://blog.matthias-andrasch.eu/cat/green-coding-green-web/
23+
- <a href="/blog/kategorie/green-coding-green-web">/blog/kategorie/green-coding-green-web</a>
2424

2525
**Online-Communities**
2626

src/pages/blog/[...page].astro

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
---
22
import Layout from '~/layouts/PageLayout.astro';
3-
import { getAllPosts, getFeaturedImage, formatDate, stripHtml } from '~/utils/wordpress';
3+
import { getAllPosts, getAllCategories, getFeaturedImage, formatDate, stripHtml } from '~/utils/wordpress';
4+
import type { WordPressCategory } from '~/utils/wordpress';
45
56
export async function getStaticPaths() {
6-
const allPosts = await getAllPosts();
7+
const [allPosts, allCategories] = await Promise.all([getAllPosts(), getAllCategories()]);
78
const postsPerPage = 12;
89
const totalPages = Math.ceil(allPosts.length / postsPerPage);
9-
10+
11+
// Build category lookup map
12+
const categoryMap: Record<number, WordPressCategory> = {};
13+
for (const cat of allCategories) {
14+
categoryMap[cat.id] = cat;
15+
}
16+
1017
return Array.from({ length: totalPages }, (_, i) => ({
1118
params: { page: i === 0 ? undefined : String(i + 1) },
1219
props: {
1320
posts: allPosts.slice(i * postsPerPage, (i + 1) * postsPerPage),
1421
currentPage: i + 1,
1522
totalPages,
23+
categoryMap,
24+
allCategories,
1625
},
1726
}));
1827
}
1928
20-
const { posts, currentPage, totalPages } = Astro.props;
29+
const { posts, currentPage, totalPages, categoryMap, allCategories } = Astro.props;
2130
2231
const metadata = {
2332
title: currentPage === 1 ? 'Blog' : `Blog - Seite ${currentPage}`,
@@ -27,10 +36,31 @@ const metadata = {
2736

2837
<Layout metadata={metadata}>
2938
<section class="px-4 py-16 sm:px-6 mx-auto lg:px-8 lg:py-20 max-w-6xl">
30-
<h1 class="text-4xl md:text-5xl font-bold leading-tighter tracking-normal mb-8 font-heading">
31-
Blog
32-
</h1>
33-
39+
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
40+
<h1 class="text-4xl md:text-5xl font-bold leading-tighter tracking-normal font-heading">
41+
Blog
42+
</h1>
43+
44+
{allCategories.length > 0 && (
45+
<div>
46+
<select
47+
id="category-filter"
48+
class="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 cursor-pointer"
49+
>
50+
<option value="">Kategorie wählen…</option>
51+
{allCategories.map((cat) => (
52+
<option value={`/blog/kategorie/${cat.slug}`}>{cat.name}</option>
53+
))}
54+
</select>
55+
<script is:inline>
56+
document.getElementById('category-filter').addEventListener('change', function() {
57+
if (this.value) window.location.href = this.value;
58+
});
59+
</script>
60+
</div>
61+
)}
62+
</div>
63+
3464
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
3565
{posts.map((post) => {
3666
const featuredImage = getFeaturedImage(post);
@@ -52,8 +82,19 @@ const metadata = {
5282
)}
5383

5484
<div class="p-5 flex flex-col flex-1">
55-
<div class="text-sm text-muted mb-2">
56-
{formatDate(post.date)}
85+
<div class="text-sm text-muted mb-2 flex items-center gap-2">
86+
<span>{formatDate(post.date)}</span>
87+
{post.categories?.[0] && categoryMap[post.categories[0]] && (
88+
<>
89+
<span>·</span>
90+
<a
91+
href={`/blog/kategorie/${categoryMap[post.categories[0]].slug}`}
92+
class="hover:text-accent transition-colors"
93+
>
94+
{categoryMap[post.categories[0]].name}
95+
</a>
96+
</>
97+
)}
5798
</div>
5899

59100
<h2 class="text-lg font-bold mb-3 tracking-normal">

src/pages/blog/[year]/[slug].astro

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
---
22
import Layout from '~/layouts/PageLayout.astro';
3-
import { getAllPosts, getPostBySlug, getFeaturedImage, formatDate, processContentWithPrivacy } from '~/utils/wordpress';
3+
import { getAllPosts, getAllCategories, getPostBySlug, getFeaturedImage, formatDate, processContentWithPrivacy } from '~/utils/wordpress';
4+
import type { WordPressCategory } from '~/utils/wordpress';
45
import PrivacyEmbed from '~/components/blog/PrivacyEmbed.astro';
56
67
export async function getStaticPaths() {
7-
const posts = await getAllPosts();
8+
const [posts, allCategories] = await Promise.all([getAllPosts(), getAllCategories()]);
9+
10+
const categoryMap: Record<number, WordPressCategory> = {};
11+
for (const cat of allCategories) {
12+
categoryMap[cat.id] = cat;
13+
}
814
915
return posts.map((post) => {
1016
const year = new Date(post.date).getFullYear().toString();
1117
return {
1218
params: { year, slug: post.slug },
13-
props: { post },
19+
props: { post, categoryMap },
1420
};
1521
});
1622
}
1723
18-
const { post } = Astro.props;
24+
const { post, categoryMap } = Astro.props;
1925
2026
if (!post) {
2127
return Astro.redirect('/404');
@@ -47,8 +53,17 @@ const metadata = {
4753

4854
{/* Post header */}
4955
<header class="mb-8">
50-
<div class="text-sm text-muted mb-4">
51-
{formatDate(post.date)}
56+
<div class="text-sm text-muted mb-4 flex items-center gap-2">
57+
<span>{formatDate(post.date)}</span>
58+
{post.categories?.[0] && categoryMap[post.categories[0]] && (
59+
<>
60+
<span>·</span>
61+
<span>Kategorie: <a
62+
href={`/blog/kategorie/${categoryMap[post.categories[0]].slug}`}
63+
class="hover:text-accent transition-colors"
64+
>{categoryMap[post.categories[0]].name}</a></span>
65+
</>
66+
)}
5267
</div>
5368

5469
<h1
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
import Layout from '~/layouts/PageLayout.astro';
3+
import { getAllPosts, getAllCategories, getFeaturedImage, formatDate, stripHtml } from '~/utils/wordpress';
4+
5+
export async function getStaticPaths() {
6+
const [allPosts, allCategories] = await Promise.all([getAllPosts(), getAllCategories()]);
7+
8+
return allCategories.map((category) => ({
9+
params: { slug: category.slug },
10+
props: {
11+
category,
12+
posts: allPosts.filter((post) => post.categories.includes(category.id)),
13+
},
14+
}));
15+
}
16+
17+
const { category, posts } = Astro.props;
18+
19+
const metadata = {
20+
title: `Kategorie: ${category.name}`,
21+
description: `Alle Blogbeiträge in der Kategorie "${category.name}".`,
22+
};
23+
---
24+
25+
<Layout metadata={metadata}>
26+
<section class="px-4 py-16 sm:px-6 mx-auto lg:px-8 lg:py-20 max-w-6xl">
27+
<div class="mb-8">
28+
<a
29+
href="/blog"
30+
class="text-accent hover:underline inline-flex items-center gap-2 text-sm"
31+
>
32+
← Alle Beiträge
33+
</a>
34+
</div>
35+
36+
<h1 class="text-4xl md:text-5xl font-bold leading-tighter tracking-normal mb-8 font-heading">
37+
Kategorie: {category.name}
38+
</h1>
39+
40+
{posts.length === 0 ? (
41+
<p class="text-muted">Keine Beiträge in dieser Kategorie.</p>
42+
) : (
43+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
44+
{posts.map((post) => {
45+
const featuredImage = getFeaturedImage(post);
46+
const excerpt = stripHtml(post.excerpt.rendered);
47+
const year = new Date(post.date).getFullYear();
48+
const postHref = `/blog/${year}/${post.slug}`;
49+
50+
return (
51+
<article class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden hover:shadow-md transition-shadow flex flex-col">
52+
{featuredImage && (
53+
<a href={postHref}>
54+
<img
55+
src={featuredImage.url}
56+
alt={featuredImage.alt}
57+
class="w-full h-48 object-cover hover:opacity-90 transition-opacity"
58+
loading="lazy"
59+
/>
60+
</a>
61+
)}
62+
63+
<div class="p-5 flex flex-col flex-1">
64+
<div class="text-sm text-muted mb-2">
65+
{formatDate(post.date)}
66+
</div>
67+
68+
<h2 class="text-lg font-bold mb-3 tracking-normal">
69+
<a
70+
href={postHref}
71+
class="hover:text-accent transition-colors"
72+
set:html={post.title.rendered}
73+
/>
74+
</h2>
75+
76+
<div class="text-sm text-secondary mb-4 leading-relaxed line-clamp-3 flex-1">
77+
{excerpt}
78+
</div>
79+
80+
<a
81+
href={postHref}
82+
class="text-accent hover:underline font-medium text-sm"
83+
>
84+
Weiterlesen →
85+
</a>
86+
</div>
87+
</article>
88+
);
89+
})}
90+
</div>
91+
)}
92+
</section>
93+
</Layout>

src/pages/index.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const metadata = {
2121
</h1>
2222
<p class="text-xl text-secondary font-light mb-8">Full-Stack · Wien · Open Source</p>
2323
<p class="text-lg text-secondary leading-relaxed max-w-[540px]">
24-
Hi, mein Name ist Matthias. 👋 Seit fünf Jahren lebe ich im schönen Wien, zugezogen aus Deutschland. Meine Brötchen bzw. Kaisersemmeln verdiene ich als Web-Entwickler – u.a. mit Nuxt, <a href="/projekte#craftcms" class="text-accent underline decoration-[var(--accent-green-glow)] decoration-2 underline-offset-4 hover:decoration-[var(--accent-green)] transition-all">Craft CMS</a> und <a href="/projekte#sveltekit" class="text-accent underline decoration-[var(--accent-green-glow)] decoration-2 underline-offset-4 hover:decoration-[var(--accent-green)] transition-all">Svelte(Kit)</a>. Mein Schwerpunkt liegt auf Full-Stack-Entwicklung und Barrierefreiheit.
24+
Hi! 👋 Seit fünf Jahren lebe ich im schönen Wien, zugezogen aus Deutschland. Meine Brötchen bzw. Semmeln verdiene ich aktuell als Web-Entwickler – u.a. mit Nuxt, Craft CMS und Svelte(Kit). Mein Schwerpunkt liegt auf Full-Stack-Entwicklung und Barrierefreiheit, mehr Erfahrungen würde ich u.a. gern im Bereich Testing, UX und Green Coding sammeln. Privat beschäftige ich mich mit Sinn- und Gerechtigkeitsfragen. Die Welt ein Stück besser hinterlassen wäre zauberhaft - hierfür fehlt mir noch der passende Hebel.
2525
</p>
2626
</div>
2727
<div class="hero-photo relative order-first md:order-last justify-self-center md:justify-self-end">

src/utils/wordpress.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ export interface WordPressPost {
3838
};
3939
}
4040

41+
export interface WordPressCategory {
42+
id: number;
43+
count: number;
44+
name: string;
45+
slug: string;
46+
description: string;
47+
}
48+
4149
export interface WordPressFeaturedImage {
4250
url: string;
4351
alt: string;
@@ -83,6 +91,36 @@ export async function getPostBySlug(slug: string): Promise<WordPressPost | null>
8391
}
8492
}
8593

94+
/**
95+
* Fetch all categories from WordPress REST API
96+
*/
97+
export async function getAllCategories(): Promise<WordPressCategory[]> {
98+
try {
99+
const response = await fetch(`${WP_API_BASE}/categories?per_page=100`);
100+
101+
if (!response.ok) {
102+
throw new Error(`WordPress API error: ${response.status}`);
103+
}
104+
105+
const categories: WordPressCategory[] = await response.json();
106+
// Filter out "Uncategorized" (id=1) and categories with no posts
107+
// Decode HTML entities in category names
108+
return categories
109+
.filter((cat) => cat.slug !== 'uncategorized' && cat.count > 0)
110+
.map((cat) => ({ ...cat, name: decodeHtmlEntities(cat.name) }));
111+
} catch (error) {
112+
console.error('Error fetching WordPress categories:', error);
113+
return [];
114+
}
115+
}
116+
117+
/**
118+
* Get the category name for a post (first category)
119+
*/
120+
export function getPostCategoryId(post: WordPressPost): number | null {
121+
return post.categories?.[0] ?? null;
122+
}
123+
86124
/**
87125
* Extract featured image from post data
88126
*/
@@ -118,6 +156,22 @@ export function formatDate(dateString: string): string {
118156
}).format(date);
119157
}
120158

159+
/**
160+
* Decode HTML entities in a string
161+
*/
162+
export function decodeHtmlEntities(text: string): string {
163+
return text
164+
.replace(/&amp;/g, '&')
165+
.replace(/&lt;/g, '<')
166+
.replace(/&gt;/g, '>')
167+
.replace(/&quot;/g, '"')
168+
.replace(/&#8217;/g, "'")
169+
.replace(/&#8220;/g, '\u201C')
170+
.replace(/&#8221;/g, '\u201D')
171+
.replace(/&hellip;/g, '\u2026')
172+
.replace(/&nbsp;/g, ' ');
173+
}
174+
121175
/**
122176
* Strip HTML tags from string (for excerpts)
123177
*/

0 commit comments

Comments
 (0)