Replaces em dashes with en dashes across all hardcoded copy, emails, and seed content, then reworks the site's visual language so it reads as deliberately designed rather than assembled from generic components: - New ArrowLink component replaces ad hoc "Mehr erfahren ->" links and ghost buttons; one shared FormField (TextField/TextAreaField) replaces 7 near-duplicate field styles across every form - Navbar loses its glassmorphism blur for a solid warm background, gets a subtle active-link underline and a softer, warmer mega-menu - PageHeader supports per-page blob tone/position and a compact mode instead of an identical band on every interior page - Homepage: the core offer is featured large instead of four equal cards, the philosophy section gets a single calmer accent blob, blog teasers now show their cover image like the real /aktuelles page does - Booking calendar and widget: 44px touch targets throughout (month nav, day cells, time slots, cancel action), a visible "today" ring, and responsive padding so the calendar isn't cramped on small screens - Quieter, warmer empty states on /termine and /aktuelles No functional changes: all interaction logic (nav menus, forms, booking, CMS data fetching) is untouched, only markup and styling.
272 lines
11 KiB
TypeScript
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."
|
||
}
|
||
/>
|
||
</>
|
||
);
|
||
}
|