Files
anouma/app/(frontend)/ueber-mich/page.tsx
T
maroandClaude Sonnet 5 1cd15aff25 Add email verification, personal calendar feed, and full SEO implementation
- Customer accounts now require email verification (hashed, single-use,
  time-limited tokens) before they can request/confirm bookings, with
  resend flows on login/account/booking widget and rate limiting.
- Admins get a private, rotatable iCalendar (ICS) subscription feed of
  their confirmed bookings and public events, timezone-correct for
  Europe/Berlin including DST, never exposing meeting passwords.
- Adds a full SEO layer: per-page canonical/OG/Twitter metadata with
  CMS-editable overrides and content-derived fallbacks, a dynamic
  sitemap.xml and robots.txt driven by real published content, JSON-LD
  (Organization/LocalBusiness, WebSite, WebPage, BreadcrumbList, Service,
  Event, BlogPosting) that never fabricates data, and a CMS-managed
  redirect table for changed slugs.
- Global ANOUMA-naming audit: the brand name is never used to label
  personal account/calendar areas anywhere in the app, CMS, or emails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 22:57:31 +02:00

110 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Metadata } from "next";
import { PageHeader } from "@/components/PageHeader";
import { Section } from "@/components/Section";
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
import { RichText } from "@/components/RichText";
import { CTA } from "@/components/CTA";
import { Reveal } from "@/components/Reveal";
import { OrganicBlob } from "@/components/OrganicBlob";
import { getAboutGlobal, getSEOSettingsGlobal } from "@/lib/payload/globals";
import { mediaAlt, mediaUrl } from "@/lib/payload/media";
import { resolveSeo } from "@/lib/seo/resolve";
import { buildMetadata } from "@/lib/seo/metadata";
import { getSiteUrl } from "@/lib/seo/config";
import { excerptFromRichText } from "@/lib/seo/textExcerpt";
import { JsonLd, webPageJsonLd } from "@/lib/seo/jsonld";
import { siteConfig } from "@/lib/site";
export const dynamic = "force-dynamic";
export async function generateMetadata(): Promise<Metadata> {
const [about, seoSettings] = await Promise.all([getAboutGlobal(), getSEOSettingsGlobal()]);
const resolved = resolveSeo({
seo: about.seo,
fallbackTitle: `Über mich ${siteConfig.name}`,
fallbackDescription: () => excerptFromRichText(about.body) || seoSettings.defaultDescription || "Mein Weg, meine Werte und was mich bewegt.",
fallbackImage: about.portrait,
defaultOgImage: seoSettings.defaultOgImage,
});
return buildMetadata({
title: resolved.title,
description: resolved.description,
path: "/ueber-mich",
ogImageUrl: resolved.ogImageUrl,
keywords: resolved.keywords,
});
}
export default async function UeberMichPage() {
const about = await getAboutGlobal();
const hasClosing = about.closingHighlight || about.closingParagraph;
const siteUrl = await getSiteUrl();
return (
<>
<JsonLd
data={webPageJsonLd({
name: about.title || "Über mich",
description: excerptFromRichText(about.body) || "Mein Weg, meine Werte und was mich bewegt.",
url: `${siteUrl}/ueber-mich`,
})}
/>
<PageHeader
eyebrow={about.eyebrow || "Über mich"}
title={about.title || "Mein Weg, meine Werte und was mich bewegt"}
crumbs={[{ title: "Über mich" }]}
/>
<Section tone="plain">
<div className="grid gap-14 lg:grid-cols-[0.85fr_1.15fr] lg:items-start">
<Reveal className="lg:sticky lg:top-28">
<div className="relative mx-auto aspect-[4/5] w-full max-w-sm lg:max-w-none">
<ImagePlaceholder
mood="dustyrose"
label="Portrait"
src={mediaUrl(about.portrait, "hero")}
alt={mediaAlt(about.portrait)}
priority
className="h-full w-full"
/>
</div>
</Reveal>
<div className="max-w-2xl">
<Reveal>
<RichText data={about.body} />
</Reveal>
</div>
</div>
</Section>
{hasClosing && (
<section className="relative overflow-hidden bg-anouma-plum py-24 text-anouma-cream-light">
<OrganicBlob tone="mauve" className="-left-24 top-0 h-96 w-96 opacity-25" />
<div className="relative mx-auto max-w-3xl px-6 text-center sm:px-8">
<Reveal>
{about.closingLead && (
<p className="text-lg text-anouma-cream-light/90">{about.closingLead}</p>
)}
{about.closingHighlight && (
<p className="mt-6 text-balance font-serif text-3xl italic leading-snug sm:text-4xl">
{about.closingHighlight}
</p>
)}
{about.closingParagraph && (
<p className="mx-auto mt-8 max-w-2xl text-lg leading-relaxed text-anouma-cream-light/90">
{about.closingParagraph}
</p>
)}
</Reveal>
</div>
</section>
)}
<CTA
title="Lust, dich kennenzulernen"
lead="Wenn dich mein Weg anspricht, freue ich mich, dich in einem persönlichen Gespräch kennenzulernen."
/>
</>
);
}