Add blog overview page (/blog), link Navbar "Blog" to it
- app/blog/page.tsx: header (title/subheading + bleeding photo with a left-edge fade, per Figma node 4577:330), featured-post card (most recent), remaining posts as a divided list with category/readTime/date, and the reusable Newsletter signup panel. Uses all real posts from Payload — the Figma mockup padded its list out to 4 items with two posts that were never actually seeded, so this only renders what exists (currently 1 featured + 2 listed). - lib/payload.ts: BlogPost now carries publishedAt (needed for the list's per-post date), removed the now-redundant duplicate field on PostDetail. - Navbar: "Blog" now links to /blog instead of scrolling to the homepage's #blog anchor — same real-route pattern as "Shop". - blog/[slug]/page.tsx: earlier title-size tuning (fluid clamp between text-h-feature and text-display) from this session's follow-up feedback, not yet pushed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,15 +55,17 @@ export default async function BlogDetailPage({
|
||||
<span>•</span>
|
||||
<span>{post.readTime} Min</span>
|
||||
</div>
|
||||
{/* text-display + Playfair, not text-h-feature + Lora — matches
|
||||
the actual Figma title exactly (Playfair Display SemiBold,
|
||||
44px), same "hero headline" treatment as Hero.tsx/
|
||||
TodoKartenHero.tsx/WeeklyImpulsesHero.tsx use, not the Lora
|
||||
section-heading style the legal pages use. text-display's
|
||||
clamp floors at exactly 2.75rem (44px) at the 768px Tablet
|
||||
breakpoint, matching the Figma value precisely. */}
|
||||
{/* Playfair (not Lora), matching the actual Figma title's font —
|
||||
same "hero headline" family as Hero.tsx/TodoKartenHero.tsx/
|
||||
WeeklyImpulsesHero.tsx use, not the Lora section-heading style
|
||||
the legal pages use. Sized down from text-display (Figma's
|
||||
literal 44px), but text-h-feature (max 40px) read too small —
|
||||
no named token sits between the two, so this is a one-off
|
||||
fluid(36, 48) clamp using the project's own fluid.ts formula
|
||||
(768px Tablet floor → 1440px Desktop cap), landing between
|
||||
them instead of jumping all the way back to text-display. */}
|
||||
<p
|
||||
className="font-semibold text-display text-text-primary leading-[1.12]"
|
||||
className="font-semibold text-[clamp(2.25rem,1.393rem+1.786vw,3rem)] text-text-primary leading-[1.15]"
|
||||
style={{ fontFamily: "var(--font-playfair)" }}
|
||||
>
|
||||
{post.title}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
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 { getBlogPosts } 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" },
|
||||
};
|
||||
|
||||
export default async function BlogOverviewPage() {
|
||||
const posts = await getBlogPosts(100);
|
||||
const [featured, ...rest] = posts;
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col flex-1 bg-bg-base">
|
||||
{/* Header — copy left, photo bleeds to the viewport edge on the
|
||||
right with a left-edge fade into bg-base, matching Figma's
|
||||
hero-fade-left/hero-fade-bottom overlays (node 4577:330). */}
|
||||
<Reveal className="relative flex flex-col md:flex-row items-center w-full min-h-[20rem] md:min-h-[27.5rem] border-b border-border overflow-hidden">
|
||||
<div className="relative z-10 flex flex-col gap-4 items-start px-[var(--layout-padding-x)] py-10 md:py-0 w-full md:w-auto md:max-w-[26rem]">
|
||||
<p
|
||||
className="font-semibold text-display text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Blog
|
||||
</p>
|
||||
<p className="text-body text-text-muted">
|
||||
Gedanken, Methoden und Impulse für einen leichteren und klareren Alltag.
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-full md:absolute md:inset-y-0 md:right-0 md:w-[68%] h-56 md:h-full">
|
||||
<Image alt="" src="/hero.jpg" fill sizes="(min-width: 768px) 68vw, 100vw" className="object-cover" />
|
||||
<div className="hidden md:block absolute inset-y-0 left-0 w-72 bg-gradient-to-r from-bg-base to-transparent" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-bg-base to-transparent" />
|
||||
</div>
|
||||
</Reveal>
|
||||
|
||||
{featured && (
|
||||
<Reveal delay={0.1} className="w-full px-[var(--layout-padding-x)] pt-10 pb-4">
|
||||
<Link
|
||||
href={`/blog/${featured.slug}`}
|
||||
className="group grid grid-cols-1 md:grid-cols-2 w-full max-w-[80rem] mx-auto rounded-md overflow-hidden bg-bg-muted transition-transform duration-300 hover:-translate-y-1"
|
||||
>
|
||||
<div className="relative w-full aspect-video md:aspect-auto md:h-full bg-bg-muted">
|
||||
{featured.thumbnail && (
|
||||
<Image alt="" src={featured.thumbnail} fill sizes="(min-width: 768px) 40rem, 100vw" className="object-cover" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-center gap-3 p-8 md:p-12 min-w-0">
|
||||
<div className="flex items-center gap-2 font-semibold text-text-muted text-body-sm uppercase tracking-wide">
|
||||
<span>{featured.category}</span>
|
||||
<span>•</span>
|
||||
<span>{featured.readTime} Min</span>
|
||||
</div>
|
||||
<p
|
||||
className="font-semibold text-h-section text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
{featured.title}
|
||||
</p>
|
||||
<p className="text-body text-text-muted line-clamp-3">{featured.excerpt}</p>
|
||||
<span className="flex items-center gap-1.5 font-bold text-body text-text-primary mt-1">
|
||||
Zum Beitrag
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
className="size-4 transition-transform duration-200 group-hover:translate-x-1"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4 10h12m0 0-5-5m5 5-5 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</Reveal>
|
||||
)}
|
||||
|
||||
{rest.length > 0 && (
|
||||
<RevealGroup 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) => (
|
||||
<RevealItem key={post.id} className="py-8 first:pt-0 last:pb-0">
|
||||
<Link href={`/blog/${post.slug}`} className="group flex flex-col sm:flex-row gap-6 items-start">
|
||||
<div className="relative w-full sm:w-[18.75rem] aspect-video sm:aspect-[300/192] shrink-0 rounded-md overflow-hidden bg-bg-muted">
|
||||
{post.thumbnail && (
|
||||
<Image alt="" src={post.thumbnail} fill sizes="(min-width: 640px) 18.75rem, 100vw" className="object-cover" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 font-semibold text-text-muted text-body-sm uppercase tracking-wide">
|
||||
<span>{post.category}</span>
|
||||
<span>•</span>
|
||||
<span>{post.readTime} Min</span>
|
||||
<span>•</span>
|
||||
<span className="uppercase">{formatDate(post.publishedAt)}</span>
|
||||
</div>
|
||||
<p
|
||||
className="font-semibold text-h-small text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
{post.title}
|
||||
</p>
|
||||
<p className="text-body text-text-muted">{post.excerpt}</p>
|
||||
<span className="flex items-center gap-1.5 font-bold text-body-sm text-text-primary mt-1">
|
||||
Zum Beitrag
|
||||
<svg
|
||||
viewBox="0 0 20 20"
|
||||
className="size-3.5 transition-transform duration-200 group-hover:translate-x-1"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4 10h12m0 0-5-5m5 5-5 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</RevealItem>
|
||||
))}
|
||||
</RevealGroup>
|
||||
)}
|
||||
|
||||
<Newsletter
|
||||
title="Neue Impulse im richtigen Moment."
|
||||
description="Melde dich zum Newsletter an und erhalte regelmäßig Gedanken, Methoden und praktische Tipps."
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ function smoothScrollTo(targetY: number) {
|
||||
|
||||
const navLinks = [
|
||||
{ label: "Werkzeuge", href: "#werkzeuge" },
|
||||
{ label: "Blog", href: "#blog" },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Über Björn", href: "#ueber-bjoern" },
|
||||
{ label: "Shop", href: "/shop" },
|
||||
];
|
||||
|
||||
+4
-2
@@ -9,6 +9,7 @@ export type BlogPost = {
|
||||
readTime: number;
|
||||
excerpt: string;
|
||||
thumbnail: string | null;
|
||||
publishedAt: string;
|
||||
};
|
||||
|
||||
type PayloadPost = {
|
||||
@@ -19,6 +20,7 @@ type PayloadPost = {
|
||||
readTime: number;
|
||||
excerpt: string;
|
||||
thumbnail: { url: string } | number | null;
|
||||
publishedAt: string;
|
||||
};
|
||||
|
||||
export async function getBlogPosts(limit = 3): Promise<BlogPost[]> {
|
||||
@@ -53,15 +55,15 @@ export async function getBlogPosts(limit = 3): Promise<BlogPost[]> {
|
||||
typeof post.thumbnail === "object" && post.thumbnail
|
||||
? post.thumbnail.url
|
||||
: null,
|
||||
publishedAt: post.publishedAt,
|
||||
}));
|
||||
}
|
||||
|
||||
export type PostDetail = BlogPost & {
|
||||
content: unknown;
|
||||
publishedAt: string;
|
||||
};
|
||||
|
||||
type PayloadPostDetail = PayloadPost & { content: unknown; publishedAt: string };
|
||||
type PayloadPostDetail = PayloadPost & { content: unknown };
|
||||
|
||||
export async function getPostBySlug(slug: string): Promise<PostDetail | null> {
|
||||
const params = new URLSearchParams({
|
||||
|
||||
Reference in New Issue
Block a user