Files
anouma/app/(frontend)/aktuelles/[slug]/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

106 lines
3.4 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 { notFound, permanentRedirect, redirect } from "next/navigation";
import { PageHeader } from "@/components/PageHeader";
import { Section } from "@/components/Section";
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
import { RichText } from "@/components/RichText";
import { getPostBySlug } from "@/lib/payload/content";
import { mediaAlt, mediaUrl } from "@/lib/payload/media";
import { getSEOSettingsGlobal } from "@/lib/payload/globals";
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, articleJsonLd } from "@/lib/seo/jsonld";
import { resolveRedirect } from "@/lib/seo/redirects";
import { siteConfig } from "@/lib/site";
export const dynamic = "force-dynamic";
type Args = { params: Promise<{ slug: string }> };
async function resolvePostOrRedirect(slug: string) {
const post = await getPostBySlug(slug);
if (post) return post;
const match = await resolveRedirect(`/aktuelles/${slug}`);
if (match) {
if (match.permanent) permanentRedirect(match.to);
redirect(match.to);
}
return null;
}
export async function generateMetadata({ params }: Args): Promise<Metadata> {
const { slug } = await params;
const [post, seoSettings] = await Promise.all([getPostBySlug(slug), getSEOSettingsGlobal()]);
if (!post) return {};
const resolved = resolveSeo({
seo: post.seo,
fallbackTitle: `${post.title} ${siteConfig.name}`,
fallbackDescription: () => post.teaser,
fallbackImage: post.coverImage,
defaultOgImage: seoSettings.defaultOgImage,
});
return buildMetadata({
title: resolved.title,
description: resolved.description,
path: `/aktuelles/${post.slug}`,
ogImageUrl: resolved.ogImageUrl,
keywords: resolved.keywords,
type: "article",
});
}
export default async function PostDetailPage({ params }: Args) {
const { slug } = await params;
const post = await resolvePostOrRedirect(slug);
if (!post) notFound();
const publishDate = new Date(post.publishDate ?? post.createdAt).toLocaleDateString("de-DE", {
day: "2-digit",
month: "long",
year: "numeric",
});
const siteUrl = await getSiteUrl();
return (
<>
<JsonLd
data={articleJsonLd({
post,
description: post.seo?.description || post.teaser || excerptFromRichText(post.content),
imageUrl: mediaUrl(post.coverImage, "hero"),
siteUrl,
providerName: siteConfig.name,
})}
/>
<PageHeader
eyebrow={publishDate}
title={post.title}
lead={post.teaser}
crumbs={[{ title: "Aktuelles", href: "/aktuelles" }, { title: post.title }]}
/>
<div className="mx-auto -mt-10 max-w-5xl px-6 sm:px-8 lg:px-12">
<div className="relative aspect-[21/9] w-full">
<ImagePlaceholder
mood="sand"
label={post.title}
src={mediaUrl(post.coverImage, "hero")}
alt={mediaAlt(post.coverImage) ?? post.title}
shape="soft"
priority
className="h-full w-full"
/>
</div>
</div>
<Section tone="plain">
<div className="mx-auto max-w-2xl">
<RichText data={post.content} />
</div>
</Section>
</>
);
}