Add blog detail page (/blog/[slug])
- app/blog/[slug]/page.tsx: article header (category/readTime, Playfair display title, excerpt, byline), full-bleed hero image, RichText body, "Passend dazu: ToDo-Karten" cross-promo, author bio card, "Weiterlesen" related-post card. Widths, fonts, and the Passend-dazu card's border/padding/spacing were corrected against the actual built Figma frame (page-blog-detail, node 4667:344, jCCZyh1DGwdjpv1wGge9To) via get_design_context after a few visually-wrong guesses. - RichText.tsx: new "quote" case renders Lexical's blockquote feature as the "Merke dir:" pull-quote — label+underline+divider+Caveat lines, using the real exported sparkle/underline assets, matching Figma's actual (background-less) structure instead of a guessed bordered card. - lib/payload.ts: getPostBySlug() for single-post fetches. - lib/format.ts: formatDate() — "03. Juli 2025"-style German dates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,47 @@ export async function getBlogPosts(limit = 3): Promise<BlogPost[]> {
|
||||
}));
|
||||
}
|
||||
|
||||
export type PostDetail = BlogPost & {
|
||||
content: unknown;
|
||||
publishedAt: string;
|
||||
};
|
||||
|
||||
type PayloadPostDetail = PayloadPost & { content: unknown; publishedAt: string };
|
||||
|
||||
export async function getPostBySlug(slug: string): Promise<PostDetail | null> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
"where[slug][equals]": slug,
|
||||
depth: "2",
|
||||
limit: "1",
|
||||
});
|
||||
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/posts?${params}`, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`getPostBySlug: Payload returned ${res.status} ${res.statusText}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data: { docs?: PayloadPostDetail[] } = await res.json();
|
||||
const doc = data.docs?.[0];
|
||||
if (!doc) return null;
|
||||
return {
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
slug: doc.slug,
|
||||
category:
|
||||
typeof doc.category === "object" && doc.category ? doc.category.name : "",
|
||||
readTime: doc.readTime,
|
||||
excerpt: doc.excerpt,
|
||||
thumbnail:
|
||||
typeof doc.thumbnail === "object" && doc.thumbnail ? doc.thumbnail.url : null,
|
||||
content: doc.content,
|
||||
publishedAt: doc.publishedAt,
|
||||
};
|
||||
}
|
||||
|
||||
// Frontend-facing shape — `id` is Payload's `slug` field, not its numeric
|
||||
// row id. Cart items are stored in localStorage keyed by this string (see
|
||||
// lib/cart.ts), so slugs were chosen in the Products collection to match
|
||||
|
||||
Reference in New Issue
Block a user