import type { Metadata } from "next"; import Link from "next/link"; import Image from "next/image"; import { Reveal, RevealGroup, RevealItem } from "../components/Reveal"; import { Newsletter } from "../components/Newsletter"; import { Footer } from "../components/Footer"; import { BlogCategoryFilter } from "../components/BlogCategoryFilter"; import { getBlogPosts, getBlogFilterEnabled } from "../lib/payload"; import { formatDate } from "../lib/format"; export const metadata: Metadata = { title: "Blog", description: "Gedanken, Methoden und Impulse für einen leichteren und klareren Alltag.", alternates: { canonical: "/blog" }, openGraph: { title: "Blog | einfach produktiv.", description: "Gedanken, Methoden und Impulse für einen leichteren und klareren Alltag.", url: "/blog", type: "website", images: ["/blog-featured.jpg"], }, }; // Every distinct category name in DOM order — Payload's `categories` field // stores related docs, no separate slug on this side, but since this page // already fetches every published post (limit 100, no pagination), a // plain client-agnostic array filter is enough; no separate Payload query // per category needed. function distinctCategories(posts: { categories: string[] }[]): string[] { const seen = new Set(); for (const post of posts) for (const c of post.categories) seen.add(c); return Array.from(seen); } export default async function BlogOverviewPage({ searchParams, }: { searchParams: Promise<{ categories?: string }>; }) { const [allPosts, blogFilterEnabled] = await Promise.all([getBlogPosts(100), getBlogFilterEnabled()]); const { categories: categoriesParam } = await searchParams; const activeCategories = blogFilterEnabled && categoriesParam ? categoriesParam.split(",").filter(Boolean) : []; const allCategories = blogFilterEnabled ? distinctCategories(allPosts) : []; const posts = activeCategories.length === 0 ? allPosts : allPosts.filter((post) => post.categories.some((c) => activeCategories.includes(c))); // getBlogPosts sorts "-featured,-publishedAt", so index 0 IS the actual // featured post when the unfiltered list is shown — but a category // filter can (and often will) exclude it entirely, in which case index 0 // is just the newest matching post, not an editorially featured one. It // still doesn't deserve the big hero-card treatment (implies "the // featured post", not "whatever sorted first"), so that layout is gated // on the post's own `featured` flag, not on array position. const [first, ...restAll] = posts; const featured = first?.featured ? first : undefined; const rest = featured ? restAll : posts; return ( <>
{/* Header — plain text, same treatment as /shop's ShopHeader.tsx and every legal page (the bespoke photo-bleed hero this used to be predated that convention being established elsewhere). */}

Startseite Blog

Blog

Gedanken, Methoden und Impulse für einen leichteren und klareren Alltag.

{allCategories.length > 1 && ( )}
{posts.length === 0 && (

Keine Beiträge in dieser Kategorie gefunden.

)} {featured && ( // key'd for the same reason as RevealGroup below — guards against // the same stuck-at-opacity-0 failure mode if a future filter // combination ever swaps in a *different* featured post without // an intervening moment where `featured` was falsy.
{featured.thumbnail && ( )}
{featured.categories.join(", ")} {featured.readTime} Min {formatDate(featured.publishedAt)}

{featured.title}

{featured.excerpt}

Zum Beitrag
)} {rest.length > 0 && ( // key'd on the rendered post set — RevealGroup's `whileInView` // only fires once per component instance (viewport.once=true), // and a category-filter change re-renders this same page // component in place (only `searchParams` differs, no full // remount). Since `rest.length > 0` stays true across most // filter transitions, RevealGroup itself never naturally // unmounts, so once it has already fired "show" for one // filter's list, freshly swapped-in RevealItems (new post ids) // mount into an already-settled parent that has no reason to // re-fire the reveal trigger — they were stuck at opacity 0 // forever, reported as the list appearing blank after refiltering. // Forcing a remount on every distinct post set restarts // RevealGroup's viewport tracking from scratch each time. post.id).join(",")} className="flex flex-col w-full max-w-[80rem] mx-auto px-[var(--layout-padding-x)] py-6 divide-y divide-border" > {rest.map((post) => (
{post.thumbnail && ( )}
{post.categories.join(", ")} {post.readTime} Min {formatDate(post.publishedAt)}

{post.title}

{post.excerpt}

Zum Beitrag
))}
)}