feat: redesign CMS and unify German admin UI
This commit is contained in:
@@ -38,7 +38,7 @@ export async function generateMetadata({ params }: Args): Promise<Metadata> {
|
||||
|
||||
const resolved = resolveSeo({
|
||||
seo: post.seo,
|
||||
fallbackTitle: `${post.title} – ${siteConfig.name}`,
|
||||
fallbackTitle: `${post.title} - ${siteConfig.name}`,
|
||||
fallbackDescription: () => post.teaser,
|
||||
fallbackImage: post.coverImage,
|
||||
defaultOgImage: seoSettings.defaultOgImage,
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function generateMetadata({ params }: Args): Promise<Metadata> {
|
||||
|
||||
const resolved = resolveSeo({
|
||||
seo: offer.seo,
|
||||
fallbackTitle: `${offer.title} – ${siteConfig.name}`,
|
||||
fallbackTitle: `${offer.title} - ${siteConfig.name}`,
|
||||
fallbackDescription: () => offer.shortDescription,
|
||||
fallbackImage: offer.image,
|
||||
defaultOgImage: seoSettings.defaultOgImage,
|
||||
@@ -133,7 +133,7 @@ export default async function OfferDetailPage({ params }: Args) {
|
||||
{offer.groupSchedule && (
|
||||
<div className="mb-8 rounded-2xl bg-white p-5 text-sm text-anouma-plum">
|
||||
<p className="font-medium">Fester Termin</p>
|
||||
<p className="mt-1">{offer.groupSchedule.recurrence === "monthly" ? "Monatlich" : "Wöchentlich"}{offer.groupSchedule.weekday ? ` · ${WEEKDAY_LABELS[offer.groupSchedule.weekday] ?? offer.groupSchedule.weekday}` : ""}{offer.groupSchedule.startTime ? ` · ${offer.groupSchedule.startTime}` : ""}{offer.groupSchedule.endTime ? `–${offer.groupSchedule.endTime}` : ""}</p>
|
||||
<p className="mt-1">{offer.groupSchedule.recurrence === "monthly" ? "Monatlich" : "Wöchentlich"}{offer.groupSchedule.weekday ? ` · ${WEEKDAY_LABELS[offer.groupSchedule.weekday] ?? offer.groupSchedule.weekday}` : ""}{offer.groupSchedule.startTime ? ` · ${offer.groupSchedule.startTime}` : ""}{offer.groupSchedule.endTime ? `-${offer.groupSchedule.endTime}` : ""}</p>
|
||||
</div>
|
||||
)}
|
||||
{offer.groupCapacity && <p className="mb-2 text-sm text-anouma-plum/75">Plätze: {offer.groupCapacity}</p>}
|
||||
|
||||
@@ -19,13 +19,13 @@ import { siteConfig } from "@/lib/site";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const FALLBACK_DESCRIPTION =
|
||||
"Prozessbegleitung, Doula-Begleitung, Kindergruppen und Singkreise – die Angebote von Anouma im Überblick.";
|
||||
"Prozessbegleitung, Doula-Begleitung, Kindergruppen und Singkreise - die Angebote von Anouma im Überblick.";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const [intro, seoSettings] = await Promise.all([getAngeboteIntroGlobal(), getSEOSettingsGlobal()]);
|
||||
const resolved = resolveSeo({
|
||||
seo: intro.seo,
|
||||
fallbackTitle: `Begleitung für dich und deine Familie – ${siteConfig.name}`,
|
||||
fallbackTitle: `Begleitung für dich und deine Familie - ${siteConfig.name}`,
|
||||
fallbackDescription: () => intro.lead || seoSettings.defaultDescription || FALLBACK_DESCRIPTION,
|
||||
defaultOgImage: seoSettings.defaultOgImage,
|
||||
});
|
||||
@@ -170,7 +170,7 @@ export default async function AngebotePage() {
|
||||
|
||||
<CTA
|
||||
title="Unsicher, welcher Raum passt?"
|
||||
lead="Melde dich gerne für ein unverbindliches Kennenlerngespräch – gemeinsam schauen wir, was dein Anliegen gerade braucht."
|
||||
lead="Melde dich gerne für ein unverbindliches Kennenlerngespräch - gemeinsam schauen wir, was dein Anliegen gerade braucht."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return buildMetadata({
|
||||
title: `Singkreise – ${siteConfig.name}`,
|
||||
title: `Singkreise - ${siteConfig.name}`,
|
||||
description: "Gemeinsam singen. Verbinden. Heilen. Für Herz und Seele.",
|
||||
path: "/angebote/singkreise",
|
||||
});
|
||||
@@ -30,7 +30,7 @@ export default async function SingkreisePage() {
|
||||
<>
|
||||
<PageHeader
|
||||
eyebrow="Singkreise"
|
||||
title="Singkreise – miteinander singen, klingen und sein"
|
||||
title="Singkreise - miteinander singen, klingen und sein"
|
||||
lead="Gemeinsam singen. Verbinden. Heilen. Für Herz und Seele."
|
||||
crumbs={[{ title: "Angebote", href: "/angebote" }, { title: "Singkreise" }]}
|
||||
/>
|
||||
@@ -53,7 +53,7 @@ export default async function SingkreisePage() {
|
||||
|
||||
<CTA
|
||||
title="Deine Stimme erklingen lassen"
|
||||
lead="Ob offen für alle, für Schwangere oder für Mamas mit Baby – melde dich, wenn du einen Kreis besuchen möchtest."
|
||||
lead="Ob offen für alle, für Schwangere oder für Mamas mit Baby - melde dich, wenn du einen Kreis besuchen möchtest."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ const TOKEN_SHAPE = /^[0-9a-f]{64}$/i;
|
||||
type Args = { params: Promise<{ tokenFile: string }> };
|
||||
|
||||
/**
|
||||
* GET /calendar/<token>.ics — a private iCalendar feed, subscribable from
|
||||
* GET /calendar/<token>.ics - a private iCalendar feed, subscribable from
|
||||
* Apple Calendar, Google Calendar, Outlook, Thunderbird etc. The token
|
||||
* itself is the only credential (there's no login here), so it's treated
|
||||
* like a secret: looked up by hash only, never logged, and the response is
|
||||
|
||||
@@ -33,9 +33,9 @@ export default async function DatenschutzPage() {
|
||||
<div>
|
||||
<h2 className="font-serif text-2xl font-medium text-anouma-plum">Verantwortliche Stelle</h2>
|
||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||
[Platzhalter – Vor- und Nachname]
|
||||
[Platzhalter - Vor- und Nachname]
|
||||
<br />
|
||||
[Platzhalter – Anschrift]
|
||||
[Platzhalter - Anschrift]
|
||||
<br />
|
||||
E-Mail: {contact.email}
|
||||
</p>
|
||||
@@ -48,7 +48,7 @@ export default async function DatenschutzPage() {
|
||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||
Beim Besuch dieser Website werden aus technischen Gründen automatisch Informationen
|
||||
erfasst, die dein Browser übermittelt (z. B. IP-Adresse, Datum und Uhrzeit des
|
||||
Zugriffs). [Platzhalter – Details zum verwendeten Hosting-Anbieter ergänzen.]
|
||||
Zugriffs). [Platzhalter - Details zum verwendeten Hosting-Anbieter ergänzen.]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: "Antropos";
|
||||
src: url("/fonts/antropos.ttf") format("truetype");
|
||||
src: url("/fonts/baar-antropos.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
@@ -80,6 +80,10 @@ body {
|
||||
font-family: "Antropos", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.font-antropos {
|
||||
font-family: "Antropos", ui-serif, Georgia, serif;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--color-anouma-rose-soft);
|
||||
color: var(--color-anouma-plum);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { JsonLd, webPageJsonLd } from "@/lib/seo/jsonld";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const DESCRIPTION = "Bildeindrücke aus der Arbeit von Anouma – Natur, Gemeinschaft und gemeinsame Räume.";
|
||||
const DESCRIPTION = "Bildeindrücke aus der Arbeit von Anouma - Natur, Gemeinschaft und gemeinsame Räume.";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return buildMetadata({ title: "Impressionen", description: DESCRIPTION, path: "/impressionen" });
|
||||
@@ -36,7 +36,7 @@ export default async function ImpressionenPage() {
|
||||
<PageHeader
|
||||
eyebrow="Impressionen"
|
||||
title="Einblicke in gemeinsame Räume"
|
||||
lead="Diese Galerie ist als Platzhalter-System angelegt – echte Fotografien lassen sich hier später direkt einsetzen, ohne das Layout zu verändern."
|
||||
lead="Diese Galerie ist als Platzhalter-System angelegt - echte Fotografien lassen sich hier später direkt einsetzen, ohne das Layout zu verändern."
|
||||
crumbs={[{ title: "Impressionen" }]}
|
||||
/>
|
||||
<Section tone="plain">
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: "Impressum",
|
||||
description: "Impressum von Anouma gemäß § 5 TMG.",
|
||||
// noindex, but still crawlable — legal boilerplate deliberately kept
|
||||
// noindex, but still crawlable - legal boilerplate deliberately kept
|
||||
// out of search results without cutting off the internal links on it.
|
||||
robots: { index: false, follow: true },
|
||||
alternates: { canonical: `${siteUrl}/impressum` },
|
||||
@@ -35,11 +35,11 @@ export default async function ImpressumPage() {
|
||||
<div>
|
||||
<h2 className="font-serif text-2xl font-medium text-anouma-plum">Angaben gemäß § 5 TMG</h2>
|
||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||
[Platzhalter – Vor- und Nachname]
|
||||
[Platzhalter - Vor- und Nachname]
|
||||
<br />
|
||||
[Platzhalter – Straße und Hausnummer]
|
||||
[Platzhalter - Straße und Hausnummer]
|
||||
<br />
|
||||
[Platzhalter – PLZ und Ort]
|
||||
[Platzhalter - PLZ und Ort]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export default async function ImpressumPage() {
|
||||
Umsatzsteuer-Identifikationsnummer
|
||||
</h2>
|
||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||
[Platzhalter – sofern vorhanden, gemäß § 27 a Umsatzsteuergesetz]
|
||||
[Platzhalter - sofern vorhanden, gemäß § 27 a Umsatzsteuergesetz]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@ export default async function ImpressumPage() {
|
||||
Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV
|
||||
</h2>
|
||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||
[Platzhalter – Vor- und Nachname, Anschrift wie oben]
|
||||
[Platzhalter - Vor- und Nachname, Anschrift wie oben]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
const [contact, seoSettings] = await Promise.all([getContactGlobal(), getSEOSettingsGlobal()]);
|
||||
const resolved = resolveSeo({
|
||||
seo: contact.seo,
|
||||
fallbackTitle: `Kontakt – ${siteConfig.name}`,
|
||||
fallbackTitle: `Kontakt - ${siteConfig.name}`,
|
||||
fallbackDescription: () => contact.lead || seoSettings.defaultDescription || FALLBACK_DESCRIPTION,
|
||||
defaultOgImage: seoSettings.defaultOgImage,
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ResendVerificationButton } from "@/components/auth/ResendVerificationBu
|
||||
import { getCurrentCustomer } from "@/lib/auth/customer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
// Applies to every /konto/* page at once — private account area, never indexable.
|
||||
// Applies to every /konto/* page at once - private account area, never indexable.
|
||||
export const metadata: Metadata = { robots: { index: false, follow: false } };
|
||||
|
||||
const navItems = [
|
||||
|
||||
@@ -14,7 +14,7 @@ export default async function KontoProfilPage() {
|
||||
<div className="mt-6 rounded-3xl bg-white p-6">
|
||||
<ProfileForm id={customer.id} name={customer.name} phone={customer.phone ?? ""} />
|
||||
<p className="mt-6 text-xs text-anouma-plum/60">
|
||||
E-Mail: {customer.email} – für eine Änderung der E-Mail-Adresse melde dich bitte direkt bei Anna.
|
||||
E-Mail: {customer.email} - für eine Änderung der E-Mail-Adresse melde dich bitte direkt bei Anna.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteConfig.domain),
|
||||
title: {
|
||||
default: siteConfig.title,
|
||||
template: `%s – ${siteConfig.name}`,
|
||||
template: `%s - ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
openGraph: {
|
||||
@@ -47,7 +47,7 @@ export const metadata: Metadata = {
|
||||
title: siteConfig.title,
|
||||
description: siteConfig.description,
|
||||
},
|
||||
// Configurable via env only — never a hardcoded verification ID (see .env.example).
|
||||
// Configurable via env only - never a hardcoded verification ID (see .env.example).
|
||||
...(process.env.GOOGLE_SITE_VERIFICATION ? { verification: { google: process.env.GOOGLE_SITE_VERIFICATION } } : {}),
|
||||
};
|
||||
|
||||
|
||||
@@ -27,11 +27,11 @@ 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`,
|
||||
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.",
|
||||
"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,
|
||||
});
|
||||
@@ -42,8 +42,8 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
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".
|
||||
// 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 } };
|
||||
}
|
||||
|
||||
@@ -93,10 +93,10 @@ export default async function Home() {
|
||||
<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."
|
||||
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. */}
|
||||
{/* Prozessbegleitung ist das Kernangebot - bekommt deshalb den größeren, ersten Platz statt eines gleichwertigen 4er-Rasters. */}
|
||||
{featuredGroup && (
|
||||
<Reveal>
|
||||
<AngebotCard
|
||||
@@ -263,7 +263,7 @@ export default async function Home() {
|
||||
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."
|
||||
"Ich freue mich, von dir zu hören - schreib mir oder buche direkt ein unverbindliches Kennenlerngespräch."
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
const [booking, seoSettings] = await Promise.all([getBookingGlobal(), getSEOSettingsGlobal()]);
|
||||
const resolved = resolveSeo({
|
||||
seo: booking.seo,
|
||||
fallbackTitle: `Termin buchen – ${siteConfig.name}`,
|
||||
fallbackTitle: `Termin buchen - ${siteConfig.name}`,
|
||||
fallbackDescription: () => booking.lead || seoSettings.defaultDescription || FALLBACK_DESCRIPTION,
|
||||
defaultOgImage: seoSettings.defaultOgImage,
|
||||
});
|
||||
@@ -115,7 +115,7 @@ export default async function TerminBuchenPage() {
|
||||
title="Allgemeine Anfrage"
|
||||
lead={
|
||||
bookableOffers.length > 0
|
||||
? "Für einen konkreten Termin wähle oben ein Angebot – hier kannst du auch einfach eine allgemeine Nachricht schreiben."
|
||||
? "Für einen konkreten Termin wähle oben ein Angebot - hier kannst du auch einfach eine allgemeine Nachricht schreiben."
|
||||
: undefined
|
||||
}
|
||||
align="left"
|
||||
|
||||
@@ -30,7 +30,7 @@ export default async function BeitretenPage({ params }: Args) {
|
||||
<OrganicBlob tone="rose" className="-right-24 -top-24 h-96 w-96" />
|
||||
<OrganicBlob tone="cream" className="-left-32 bottom-0 h-96 w-96" />
|
||||
<div className="relative mx-auto w-full max-w-md px-6 text-center">
|
||||
<Link href="/" className="font-serif text-2xl font-semibold tracking-[0.12em] text-anouma-mauve-dark">
|
||||
<Link href="/" className="font-antropos text-2xl font-semibold tracking-[0.12em] text-anouma-mauve-dark">
|
||||
ANOUMA
|
||||
</Link>
|
||||
<p className="mt-8 text-sm font-medium uppercase tracking-[0.18em] text-anouma-plum/70">
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function generateMetadata({ params }: Args): Promise<Metadata> {
|
||||
|
||||
const resolved = resolveSeo({
|
||||
seo: event.seo,
|
||||
fallbackTitle: `${event.title} – ${siteConfig.name}`,
|
||||
fallbackTitle: `${event.title} - ${siteConfig.name}`,
|
||||
fallbackDescription: () => excerptFromRichText(event.description) || `${event.title} bei ${siteConfig.name}.`,
|
||||
fallbackImage: event.image,
|
||||
defaultOgImage: seoSettings.defaultOgImage,
|
||||
@@ -74,7 +74,7 @@ export default async function EventDetailPage({ params }: Args) {
|
||||
if (event.isOnline) {
|
||||
const payload = await getPayload({ config });
|
||||
const { user } = await payload.auth({ headers: await headers() });
|
||||
// Must check the collection, not just presence — a logged-in customer
|
||||
// Must check the collection, not just presence - a logged-in customer
|
||||
// (collection "customers") must never be treated as host.
|
||||
const isHost = user?.collection === "users" && user.role === "admin";
|
||||
const settings = await payload.findGlobal({ slug: "meeting-settings" });
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function TerminePage() {
|
||||
<div className="rounded-3xl bg-anouma-cream-light p-8 text-center sm:p-10">
|
||||
<p className="font-serif text-xl font-medium text-anouma-plum">Aktuell sind keine Termine geplant</p>
|
||||
<p className="mt-2 text-base leading-relaxed text-anouma-plum/90">
|
||||
Schau gerne bald wieder vorbei, oder melde dich direkt – ich freue mich von dir zu hören.
|
||||
Schau gerne bald wieder vorbei, oder melde dich direkt - ich freue mich von dir zu hören.
|
||||
</p>
|
||||
<div className="mt-5 flex justify-center">
|
||||
<ArrowLink href="/kontakt">Zur Kontaktseite</ArrowLink>
|
||||
|
||||
@@ -21,7 +21,7 @@ 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}`,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user