Files
anouma/app/(frontend)/page.tsx
T

272 lines
11 KiB
TypeScript

import type { Metadata } from "next";
import Link from "next/link";
import { Hero } from "@/components/Hero";
import { Button } from "@/components/Button";
import { ArrowLink } from "@/components/ArrowLink";
import { Section, SectionHeading } from "@/components/Section";
import { AngebotCard } from "@/components/AngebotCard";
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
import { OrganicBlob } from "@/components/OrganicBlob";
import { RichText } from "@/components/RichText";
import { CTA } from "@/components/CTA";
import { Reveal } from "@/components/Reveal";
import { EventTeaserCard } from "@/components/EventTeaserCard";
import { getOffers, getPosts, getUpcomingEvents } from "@/lib/payload/content";
import { getHomeGlobal, getSEOSettingsGlobal } from "@/lib/payload/globals";
import { groupOffersByCategory, moodForOffer } from "@/lib/angebote";
import { mediaAlt, mediaUrl } from "@/lib/payload/media";
import { siteConfig } from "@/lib/site";
import { resolveSeo } from "@/lib/seo/resolve";
import { buildMetadata } from "@/lib/seo/metadata";
import { getSiteUrl } from "@/lib/seo/config";
import { JsonLd, webPageJsonLd } from "@/lib/seo/jsonld";
export const dynamic = "force-dynamic";
export async function generateMetadata(): Promise<Metadata> {
const [home, seoSettings] = await Promise.all([getHomeGlobal(), getSEOSettingsGlobal()]);
const resolved = resolveSeo({
seo: home.seo,
fallbackTitle: `${siteConfig.name} - Begleitung für dich und deine Familie`,
fallbackDescription: () =>
home.heroSupporting ||
seoSettings.defaultDescription ||
"Prozessbegleitung, Doula-Begleitung, Kindergruppen und Singkreise - Räume für Verbindung mit dir selbst, miteinander und mit der Natur.",
fallbackImage: home.heroImage,
defaultOgImage: seoSettings.defaultOgImage,
});
const meta = await buildMetadata({
title: resolved.title,
description: resolved.description,
path: "",
ogImageUrl: resolved.ogImageUrl,
keywords: resolved.keywords,
});
// Homepage bypasses the "%s - Anouma" title template - the brand is
// already part of the fallback/CMS title itself, avoiding "Anouma - Anouma".
return { ...meta, title: { absolute: resolved.title } };
}
export default async function Home() {
const [home, offers, posts, events] = await Promise.all([
getHomeGlobal(),
getOffers(),
getPosts(3),
getUpcomingEvents(3),
]);
const groups = groupOffersByCategory(offers);
const featuredGroup = groups.find((g) => g.category === "prozessbegleitung");
const remainingGroups = featuredGroup ? groups.filter((g) => g !== featuredGroup) : groups;
const siteUrl = await getSiteUrl();
return (
<>
<JsonLd
data={webPageJsonLd({
name: home.heroTitle || "Willkommen bei Anouma",
description: home.heroSupporting || siteConfig.description,
url: siteUrl,
})}
/>
<Hero
eyebrow={home.heroEyebrow ?? undefined}
title={home.heroTitle || "Willkommen bei Anouma"}
mood="rose"
imageSrc={mediaUrl(home.heroImage, "hero")}
imageAlt={mediaAlt(home.heroImage)}
actions={
<>
<Button href="/termin-buchen">Termin buchen</Button>
<ArrowLink href="/angebote" size="base" className="self-center">
Angebote entdecken
</ArrowLink>
</>
}
>
{home.heroHighlight && <p>{home.heroHighlight}</p>}
{home.heroSupporting && (
<p className="mt-3 text-base text-anouma-plum/90">{home.heroSupporting}</p>
)}
</Hero>
<Section tone="cream">
<SectionHeading
eyebrow="Angebote"
title="Räume, die zu deinem Weg passen"
lead="Von persönlicher Prozessbegleitung über Doula-Begleitung bis zu Kinder- und Singkreisen - jedes Angebot ist ein eigener, geschützter Raum."
/>
<div className="mt-14 space-y-6">
{/* Prozessbegleitung ist das Kernangebot - bekommt deshalb den größeren, ersten Platz statt eines gleichwertigen 4er-Rasters. */}
{featuredGroup && (
<Reveal>
<AngebotCard
href={featuredGroup.offers.length === 1 ? `/angebote/${featuredGroup.offers[0].slug}` : featuredGroup.href}
title={featuredGroup.label}
tagline={featuredGroup.offers.length === 1 ? featuredGroup.offers[0].shortDescription : `${featuredGroup.offers.length} Angebote`}
mood={moodForOffer(featuredGroup.offers[0])}
imageSrc={mediaUrl(featuredGroup.offers[0].image, "card")}
size="large"
/>
</Reveal>
)}
<div className="grid gap-6 sm:grid-cols-3">
{remainingGroups.map((group, i) => {
const isSingle = group.offers.length === 1;
const first = group.offers[0];
return (
<Reveal key={group.category} delay={i * 0.08}>
<AngebotCard
href={isSingle ? `/angebote/${first.slug}` : group.href}
title={group.label}
tagline={isSingle ? first.shortDescription : `${group.offers.length} Angebote`}
mood={moodForOffer(first)}
imageSrc={mediaUrl(first.image, "card")}
/>
</Reveal>
);
})}
</div>
</div>
</Section>
{(home.aboutTitle || home.aboutText) && (
<Section tone="plain">
<div className="grid gap-14 lg:grid-cols-2 lg:items-center">
<Reveal>
<div className="relative mx-auto aspect-[4/5] w-full max-w-md lg:max-w-none">
<ImagePlaceholder
mood="mauve"
label="Anouma"
src={mediaUrl(home.aboutImage, "hero")}
alt={mediaAlt(home.aboutImage)}
className="h-full w-full"
/>
</div>
</Reveal>
<Reveal delay={0.1}>
{home.aboutEyebrow && (
<p className="mb-3 text-xs font-medium uppercase tracking-[0.22em] text-anouma-plum">
{home.aboutEyebrow}
</p>
)}
{home.aboutTitle && (
<h2 className="text-balance font-serif text-4xl font-medium leading-tight text-anouma-plum sm:text-5xl">
{home.aboutTitle}
</h2>
)}
{home.aboutText && (
<div className="mt-6">
<RichText data={home.aboutText} />
</div>
)}
<div className="mt-8">
<ArrowLink href="/ueber-mich">Mehr über mich lesen</ArrowLink>
</div>
</Reveal>
</div>
</Section>
)}
{home.values && home.values.length > 0 && (
<Section tone="mauve" className="relative overflow-hidden">
<OrganicBlob tone="sage" className="-left-24 top-1/2 h-96 w-96 -translate-y-1/2 opacity-20" />
<div className="relative">
<SectionHeading
eyebrow={home.valuesEyebrow ?? undefined}
title={home.valuesTitle || "Philosophie"}
align="center"
className="mx-auto text-anouma-cream-light [&_h2]:text-anouma-cream-light [&_p]:text-anouma-cream-light/90"
/>
<div className="mt-16 grid gap-10 sm:grid-cols-3">
{home.values.map((wert, i) => (
<Reveal key={wert.id ?? wert.title} delay={i * 0.1} className="text-center">
<p className="font-serif text-sm font-medium uppercase tracking-[0.2em] text-white/85">
{wert.title}
</p>
<p className="mt-4 text-balance font-serif text-2xl leading-snug italic">
„{wert.quote}“
</p>
</Reveal>
))}
</div>
</div>
</Section>
)}
{events.length > 0 && (
<Section tone="warm">
<div className="flex flex-wrap items-end justify-between gap-6">
<SectionHeading eyebrow="Termine" title="Aktuelle Termine" className="mb-0" />
<ArrowLink href="/termine">Alle Termine</ArrowLink>
</div>
<div className="mt-12 grid gap-6 sm:grid-cols-3">
{events.map((event, i) => (
<Reveal key={event.slug} delay={i * 0.08}>
<EventTeaserCard event={event} />
</Reveal>
))}
</div>
</Section>
)}
{posts.length > 0 && (
<Section tone="cream">
<div className="flex flex-wrap items-end justify-between gap-6">
<SectionHeading eyebrow="Aktuelles" title="Neuigkeiten und Inspiration" className="mb-0" />
<ArrowLink href="/aktuelles">Alle Neuigkeiten</ArrowLink>
</div>
<div className="mt-12 grid gap-6 sm:grid-cols-3">
{posts.map((post, i) => (
<Reveal key={post.slug} delay={i * 0.08}>
<Link href={`/aktuelles/${post.slug}`} className="group block">
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-3xl">
<ImagePlaceholder
mood="sand"
label={post.title}
src={mediaUrl(post.coverImage, "card")}
alt={mediaAlt(post.coverImage) ?? post.title}
shape="soft"
className="h-full w-full transition-transform duration-700 group-hover:scale-[1.03]"
/>
</div>
<p className="mt-4 text-xs font-medium uppercase tracking-[0.18em] text-anouma-plum/70">
{new Date(post.publishDate ?? post.createdAt).toLocaleDateString("de-DE", {
day: "2-digit",
month: "long",
year: "numeric",
})}
</p>
<h3 className="mt-2 font-serif text-xl font-medium text-anouma-plum">{post.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-anouma-plum/90">{post.teaser}</p>
</Link>
</Reveal>
))}
</div>
</Section>
)}
<Section tone="plain">
<SectionHeading eyebrow="Impressionen" title="Einblicke in gemeinsame Räume" align="center" className="mx-auto" />
<div className="mt-14 grid grid-cols-2 gap-4 sm:grid-cols-4">
{(["moss", "rose", "peach", "sand"] as const).map((mood, i) => (
<Reveal key={mood} delay={i * 0.06}>
<ImagePlaceholder mood={mood} label="Impression" shape="soft" className="aspect-square" />
</Reveal>
))}
</div>
<div className="mt-10 flex justify-center">
<ArrowLink href="/impressionen">Alle Impressionen ansehen</ArrowLink>
</div>
</Section>
<CTA
title={home.ctaTitle || "Kennenlernen & Termine vereinbaren"}
lead={
home.ctaLead ||
"Ich freue mich, von dir zu hören - schreib mir oder buche direkt ein unverbindliches Kennenlerngespräch."
}
/>
</>
);
}