Files
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

100 lines
3.5 KiB
TypeScript
Raw Permalink 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 { ContactForm } from "@/components/ContactForm";
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
import { Reveal } from "@/components/Reveal";
import { getContactGlobal, getSEOSettingsGlobal } from "@/lib/payload/globals";
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";
import { siteConfig } from "@/lib/site";
export const dynamic = "force-dynamic";
const FALLBACK_DESCRIPTION = "Ich freue mich, von dir zu hören.";
export async function generateMetadata(): Promise<Metadata> {
const [contact, seoSettings] = await Promise.all([getContactGlobal(), getSEOSettingsGlobal()]);
const resolved = resolveSeo({
seo: contact.seo,
fallbackTitle: `Kontakt ${siteConfig.name}`,
fallbackDescription: () => contact.lead || seoSettings.defaultDescription || FALLBACK_DESCRIPTION,
defaultOgImage: seoSettings.defaultOgImage,
});
return buildMetadata({
title: resolved.title,
description: resolved.description,
path: "/kontakt",
ogImageUrl: resolved.ogImageUrl,
keywords: resolved.keywords,
});
}
export default async function KontaktPage() {
const contact = await getContactGlobal();
const siteUrl = await getSiteUrl();
return (
<>
<JsonLd
data={webPageJsonLd({
name: contact.title || "Kontakt",
description: contact.lead || FALLBACK_DESCRIPTION,
url: `${siteUrl}/kontakt`,
})}
/>
<PageHeader
eyebrow={contact.eyebrow || "Kontakt"}
title={contact.title || "Ich freue mich, von dir zu hören"}
lead={contact.lead}
crumbs={[{ title: "Kontakt" }]}
/>
<Section tone="plain">
<div className="grid gap-14 lg:grid-cols-[1fr_0.8fr]">
<Reveal>
<ContactForm toEmail={contact.email} />
</Reveal>
<Reveal delay={0.1} className="space-y-8">
<div className="relative aspect-[4/3] w-full">
<ImagePlaceholder mood="mauve" label="Kontakt" className="h-full w-full" />
</div>
<div className="space-y-3 text-anouma-plum">
<p>
<span className="block text-xs font-medium uppercase tracking-[0.18em] text-anouma-plum/70">
E-Mail
</span>
<a
href={`mailto:${contact.email}`}
className="text-lg text-anouma-plum hover:text-anouma-mauve-dark"
>
{contact.email}
</a>
</p>
{contact.phone && (
<p>
<span className="block text-xs font-medium uppercase tracking-[0.18em] text-anouma-plum/70">
Telefon
</span>
<span className="text-lg text-anouma-plum">{contact.phone}</span>
</p>
)}
{contact.region && (
<p>
<span className="block text-xs font-medium uppercase tracking-[0.18em] text-anouma-plum/70">
Region
</span>
<span className="text-lg text-anouma-plum">{contact.region}</span>
</p>
)}
</div>
</Reveal>
</div>
</Section>
</>
);
}