feat: redesign CMS and unify German admin UI
This commit is contained in:
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
|
||||
import { Inter } from "next/font/google";
|
||||
import "../(frontend)/globals.css";
|
||||
|
||||
// Its own root layout — the video-call UI is intentionally full-bleed and
|
||||
// Its own root layout - the video-call UI is intentionally full-bleed and
|
||||
// dark, without the marketing navbar/footer around it.
|
||||
|
||||
const inter = Inter({ variable: "--font-inter", subsets: ["latin"] });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0
|
||||
import { CalendarNavLink as CalendarNavLink_4d06fac03127b339306d5f5ecb073251 } from '../../../components/admin/CalendarNavLink'
|
||||
import { AnalyticsNavLink as AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962 } from '../../../components/admin/AnalyticsNavLink'
|
||||
import { ConversationsNavLink as ConversationsNavLink_67a2594cf28806bc76971e67212a6181 } from '../../../components/admin/ConversationsNavLink'
|
||||
import { AdminWelcome as AdminWelcome_b3c24bec21c663ad093f131aaae3d0a6 } from '../../../components/admin/AdminWelcome'
|
||||
import { AdminCalendarView as AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a } from '../../../components/admin/AdminCalendarView'
|
||||
import { AdminAnalyticsView as AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0 } from '../../../components/admin/AdminAnalyticsView'
|
||||
import { AdminConversationsView as AdminConversationsView_180f5c400a066a0a8f248da44d961055 } from '../../../components/admin/AdminConversationsView'
|
||||
@@ -57,6 +58,7 @@ export const importMap = {
|
||||
"/components/admin/CalendarNavLink#CalendarNavLink": CalendarNavLink_4d06fac03127b339306d5f5ecb073251,
|
||||
"/components/admin/AnalyticsNavLink#AnalyticsNavLink": AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962,
|
||||
"/components/admin/ConversationsNavLink#ConversationsNavLink": ConversationsNavLink_67a2594cf28806bc76971e67212a6181,
|
||||
"/components/admin/AdminWelcome#AdminWelcome": AdminWelcome_b3c24bec21c663ad093f131aaae3d0a6,
|
||||
"/components/admin/AdminCalendarView#AdminCalendarView": AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a,
|
||||
"/components/admin/AdminAnalyticsView#AdminAnalyticsView": AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0,
|
||||
"/components/admin/AdminConversationsView#AdminConversationsView": AdminConversationsView_180f5c400a066a0a8f248da44d961055,
|
||||
|
||||
@@ -6,10 +6,10 @@ import { calendarFeedUrl, generateCalendarFeedToken, hashCalendarFeedToken } fro
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* POST /api/admin/calendar-feed/regenerate — (re)generates Anna's personal
|
||||
* POST /api/admin/calendar-feed/regenerate - (re)generates Anna's personal
|
||||
* ICS feed link. Only the new token's hash is stored; the plaintext URL is
|
||||
* returned exactly once in this response and can never be retrieved again
|
||||
* — generating a new one immediately invalidates whichever link was issued
|
||||
* - generating a new one immediately invalidates whichever link was issued
|
||||
* before (see collections/Users.ts).
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
|
||||
@@ -23,7 +23,7 @@ function normalizeEventName(value: unknown): string | null {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/** Hostname only (never path/query) — see collections/AnalyticsEvents.ts. */
|
||||
/** Hostname only (never path/query) - see collections/AnalyticsEvents.ts. */
|
||||
function referrerHostFrom(referrer: unknown): string | undefined {
|
||||
if (typeof referrer !== "string" || !referrer) return undefined;
|
||||
try {
|
||||
@@ -35,7 +35,7 @@ function referrerHostFrom(referrer: unknown): string | undefined {
|
||||
|
||||
/**
|
||||
* Public tracking beacon, called by lib/analytics/client.ts only after the
|
||||
* visitor has granted statistics consent — but the client's own claim is
|
||||
* visitor has granted statistics consent - but the client's own claim is
|
||||
* never trusted alone: this route re-checks the CookieSettings global's
|
||||
* `statistics.enabled` flag server-side before writing anything, so a
|
||||
* global kill-switch (or a stale/tampered client) can never bypass it.
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Customer } from "@/payload-types";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// Deliberately identical whether or not an account exists / is already
|
||||
// verified — this endpoint must never let a caller find out either of
|
||||
// verified - this endpoint must never let a caller find out either of
|
||||
// those things about an email address that isn't their own session.
|
||||
const GENERIC_RESPONSE = {
|
||||
ok: true,
|
||||
@@ -60,7 +60,7 @@ export async function POST(request: Request) {
|
||||
body = await request.json();
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("Invalid body");
|
||||
} catch {
|
||||
// no body — handled below as a missing email
|
||||
// no body - handled below as a missing email
|
||||
}
|
||||
const email = typeof body?.email === "string" ? body.email.trim().toLowerCase() : "";
|
||||
if (!email || email.length > 254) {
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function POST(request: Request, { params }: Args) {
|
||||
}
|
||||
|
||||
// The beforeChange hook on booking-requests re-checks for conflicts
|
||||
// (with an advisory lock) before allowing this to become "confirmed" —
|
||||
// (with an advisory lock) before allowing this to become "confirmed" -
|
||||
// the alternative slot may have been taken by someone else since it was
|
||||
// proposed.
|
||||
await payload.update({
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function POST(request: Request, { params }: Args) {
|
||||
if (!user || user.collection !== "customers") {
|
||||
return NextResponse.json({ error: "Bitte melde dich an." }, { status: 401 });
|
||||
}
|
||||
// Cancelling is deliberately allowed regardless of verification status —
|
||||
// Cancelling is deliberately allowed regardless of verification status -
|
||||
// only creating/confirming a booking is a gated "protected" action.
|
||||
|
||||
return await withBookingTransaction(payload, `booking:${id}`, async (req) => {
|
||||
|
||||
@@ -67,7 +67,7 @@ export async function POST(request: Request) {
|
||||
const end = new Date(start.getTime() + offer.durationMinutes * 60_000);
|
||||
|
||||
// Re-validate the slot is actually free right now (defense against a
|
||||
// stale slot list) — the hard, race-safe check still runs again when
|
||||
// stale slot list) - the hard, race-safe check still runs again when
|
||||
// Anna confirms (see collections/BookingRequests.ts).
|
||||
const dayBounds = berlinDayBounds(start);
|
||||
return await withBookingTransaction(payload, dayBounds.key, async (req) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ function safeEqual(a: string, b: string): boolean {
|
||||
/**
|
||||
* Intended to be called by an external cron job once daily, e.g.:
|
||||
* curl -H "Authorization: Bearer $CRON_SECRET" https://anouma.org/api/cron/analytics-cleanup
|
||||
* (a second, separate cron entry from /api/cron/event-reminders — reuses
|
||||
* (a second, separate cron entry from /api/cron/event-reminders - reuses
|
||||
* the same CRON_SECRET, no new secret needed)
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const dynamic = "force-dynamic";
|
||||
/**
|
||||
* Used by the Docker healthcheck (see docker-compose.yml) and by
|
||||
* setup.sh/deploy.sh to confirm the app can actually reach Postgres before
|
||||
* being considered "up" — not just that the Node process is listening.
|
||||
* being considered "up" - not just that the Node process is listening.
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function POST(request: Request, { params }: Args) {
|
||||
}
|
||||
|
||||
const { user } = await payload.auth({ headers: request.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";
|
||||
|
||||
@@ -81,7 +81,7 @@ export async function POST(request: Request, { params }: Args) {
|
||||
role: isHost ? "host" : "participant",
|
||||
accessVersion: access.accessVersion,
|
||||
ttlMs: Math.min(6 * 3600000, access.expiresAt - Date.now()),
|
||||
// Only meaningful for participants — a host authenticates via a real
|
||||
// Only meaningful for participants - a host authenticates via a real
|
||||
// session already, so there's no need to anchor a kick to it (and
|
||||
// hosts can't be kicked in the first place.
|
||||
clientId: isHost ? undefined : clientId,
|
||||
|
||||
+383
-3
@@ -1,3 +1,383 @@
|
||||
/* Optional custom overrides for the Payload admin UI. Left empty intentionally —
|
||||
the admin panel uses Payload's own default styling, only the public site
|
||||
uses the ANOUMA design system (see app/(frontend)/globals.css). */
|
||||
@font-face {
|
||||
font-family: "Anouma Admin";
|
||||
src: url("/fonts/baar-antropos.ttf") format("truetype");
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
}
|
||||
|
||||
:root {
|
||||
--anouma-ink: #342c31;
|
||||
--anouma-plum: #66505f;
|
||||
--anouma-mauve: #8b616d;
|
||||
--anouma-rose: #d19ca1;
|
||||
--anouma-cream: #f7f2ed;
|
||||
--anouma-paper: #fffdfb;
|
||||
--anouma-line: #e4d9d2;
|
||||
--font-body: "Anouma Admin", Inter, ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: var(--anouma-cream);
|
||||
color: var(--anouma-ink);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
body,
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
/* The left navigation is intentionally dark and quiet. Content stays light. */
|
||||
.nav {
|
||||
background: var(--anouma-ink);
|
||||
border-right: 1px solid #4b3f46;
|
||||
color: #f8f2ed;
|
||||
}
|
||||
|
||||
.nav__header {
|
||||
border-bottom: 1px solid #4b3f46;
|
||||
padding: 24px 20px 20px;
|
||||
}
|
||||
|
||||
.nav__header a,
|
||||
.nav__header button {
|
||||
color: #fffaf6;
|
||||
}
|
||||
|
||||
.nav__links {
|
||||
padding: 18px 12px;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.nav-group__label,
|
||||
.nav-group__toggle {
|
||||
color: #bdaeb4;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-group__label {
|
||||
padding: 0 10px 8px;
|
||||
}
|
||||
|
||||
.nav__link {
|
||||
border-radius: 4px;
|
||||
color: #f8f2ed;
|
||||
font-size: 14px;
|
||||
margin: 2px 0;
|
||||
min-height: 38px;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.nav__link:hover,
|
||||
.nav__link:focus-visible,
|
||||
.nav__link.active {
|
||||
background: #4c3d47;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav__link.active {
|
||||
box-shadow: inset 3px 0 0 var(--anouma-rose);
|
||||
}
|
||||
|
||||
.nav__controls {
|
||||
border-top: 1px solid #4b3f46;
|
||||
padding: 14px 12px;
|
||||
}
|
||||
|
||||
.app-header,
|
||||
.step-nav {
|
||||
background: var(--anouma-paper);
|
||||
border-bottom: 1px solid var(--anouma-line);
|
||||
}
|
||||
|
||||
.template-default__wrap,
|
||||
.dashboard__wrap,
|
||||
.collection-list__wrap,
|
||||
.edit-view__wrap {
|
||||
background: var(--anouma-cream);
|
||||
}
|
||||
|
||||
.template-default__wrap {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard__header,
|
||||
.collection-list__header,
|
||||
.edit-view__header {
|
||||
border-bottom: 1px solid var(--anouma-line);
|
||||
margin-bottom: 26px;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.dashboard__header h1,
|
||||
.collection-list__header h1,
|
||||
.edit-view__header h1 {
|
||||
color: var(--anouma-ink);
|
||||
font-size: clamp(26px, 3vw, 38px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.card,
|
||||
.dashboard__card,
|
||||
.table-wrap,
|
||||
.collection-list__table-wrap,
|
||||
.field-type,
|
||||
.document-fields__main {
|
||||
border-color: var(--anouma-line);
|
||||
}
|
||||
|
||||
.card,
|
||||
.dashboard__card,
|
||||
.table-wrap {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 24px rgb(52 44 49 / 4%);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn--style-primary {
|
||||
--bg-color: var(--anouma-mauve);
|
||||
--hover-bg: var(--anouma-plum);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
.rs__control {
|
||||
border-color: #d7c9c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus,
|
||||
.rs__control--is-focused {
|
||||
border-color: var(--anouma-mauve);
|
||||
box-shadow: 0 0 0 2px rgb(139 97 109 / 12%);
|
||||
}
|
||||
|
||||
.field-label,
|
||||
.field-label label {
|
||||
color: var(--anouma-plum);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.description,
|
||||
.field-description {
|
||||
color: #7b6b72;
|
||||
}
|
||||
|
||||
.table thead th,
|
||||
.table th {
|
||||
background: #f1e9e4;
|
||||
color: var(--anouma-plum);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: #fbf7f4;
|
||||
}
|
||||
|
||||
.anouma-admin-welcome {
|
||||
background: var(--anouma-paper);
|
||||
border: 1px solid var(--anouma-line);
|
||||
border-left: 4px solid var(--anouma-mauve);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 12px 32px rgb(52 44 49 / 5%);
|
||||
margin: 0 0 28px;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.anouma-admin-welcome__intro {
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.anouma-admin-eyebrow {
|
||||
color: var(--anouma-mauve);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
margin: 0 0 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.anouma-admin-welcome h1 {
|
||||
color: var(--anouma-ink);
|
||||
font-size: clamp(25px, 3vw, 36px);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.anouma-admin-welcome__intro > p:last-child {
|
||||
color: #776870;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.anouma-admin-quicklinks {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.anouma-admin-quicklink {
|
||||
background: #faf5f1;
|
||||
border: 1px solid var(--anouma-line);
|
||||
border-radius: 4px;
|
||||
color: var(--anouma-ink);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 154px;
|
||||
padding: 16px;
|
||||
text-decoration: none;
|
||||
transition: border-color 120ms ease, background 120ms ease, transform 120ms ease;
|
||||
}
|
||||
|
||||
.anouma-admin-quicklink:hover,
|
||||
.anouma-admin-quicklink:focus-visible {
|
||||
background: #fffaf7;
|
||||
border-color: var(--anouma-mauve);
|
||||
color: var(--anouma-ink);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.anouma-admin-quicklink__number {
|
||||
color: var(--anouma-rose);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.anouma-admin-quicklink__kicker {
|
||||
color: var(--anouma-mauve);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.anouma-admin-quicklink strong {
|
||||
font-size: 17px;
|
||||
font-weight: 650;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.anouma-admin-quicklink > span:last-child {
|
||||
color: #7b6b72;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.anouma-custom-nav-item,
|
||||
.anouma-custom-nav-action {
|
||||
margin: 0 0 6px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.anouma-custom-nav-link,
|
||||
.anouma-custom-nav-button {
|
||||
align-items: center;
|
||||
border: 1px solid #5a4a55;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
gap: 10px;
|
||||
min-height: 38px;
|
||||
padding: 8px 10px;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.anouma-custom-nav-link {
|
||||
color: #fffaf6;
|
||||
}
|
||||
|
||||
.anouma-custom-nav-link:hover,
|
||||
.anouma-custom-nav-link:focus-visible {
|
||||
background: #4c3d47;
|
||||
border-color: #7d6371;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.anouma-custom-nav-link__icon {
|
||||
color: var(--anouma-rose);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.anouma-custom-nav-button {
|
||||
background: var(--anouma-mauve);
|
||||
border-color: var(--anouma-mauve);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.anouma-custom-nav-button:hover,
|
||||
.anouma-custom-nav-button:focus-visible {
|
||||
background: var(--anouma-plum);
|
||||
border-color: var(--anouma-plum);
|
||||
}
|
||||
|
||||
.anouma-custom-nav-button:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.anouma-custom-nav-input {
|
||||
background: #fffaf6;
|
||||
border: 1px solid #d7c9c1;
|
||||
border-radius: 3px;
|
||||
color: var(--anouma-ink);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
margin-top: 6px;
|
||||
padding: 6px 7px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.anouma-custom-nav-error {
|
||||
color: #f1b5b5;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.anouma-admin-quicklinks {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.anouma-admin-welcome {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.anouma-admin-quicklinks {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ImagePlaceholder } from "@/components/ImagePlaceholder";
|
||||
import { siteConfig } from "@/lib/site";
|
||||
import "./(frontend)/globals.css";
|
||||
|
||||
// Required because the app has two root layouts — (frontend) and (payload) —
|
||||
// Required because the app has two root layouts - (frontend) and (payload) -
|
||||
// so there is no single layout Next.js could compose a 404 page from.
|
||||
// See node_modules/next/dist/docs/.../not-found.md ("global-not-found.js").
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function GlobalNotFound() {
|
||||
>
|
||||
<body className="flex min-h-full flex-col bg-background text-foreground">
|
||||
{/* This page is always statically prerendered, so it can't read live
|
||||
CMS data — the mega menu is intentionally omitted here. */}
|
||||
CMS data - the mega menu is intentionally omitted here. */}
|
||||
<Navbar offers={[]} />
|
||||
<main className="flex-1">
|
||||
<section className="relative overflow-hidden py-24 sm:py-32">
|
||||
@@ -67,7 +67,7 @@ export default function GlobalNotFound() {
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{/* Minimal static footer — the full Footer reads live CMS data,
|
||||
{/* Minimal static footer - the full Footer reads live CMS data,
|
||||
which isn't available on this always-static 404 page. */}
|
||||
<footer className="bg-anouma-plum py-8 text-center text-sm text-anouma-cream-light">
|
||||
© {new Date().getFullYear()} {siteConfig.name}
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ import type { MetadataRoute } from "next";
|
||||
import { getSiteUrl, isSiteIndexable } from "@/lib/seo/config";
|
||||
|
||||
// Reads the CMS's SEO settings (site URL override, robots kill switch) on
|
||||
// every request — must not be statically prerendered at build time.
|
||||
// every request - must not be statically prerendered at build time.
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function robots(): Promise<MetadataRoute.Robots> {
|
||||
@@ -10,7 +10,7 @@ export default async function robots(): Promise<MetadataRoute.Robots> {
|
||||
|
||||
if (!indexable) {
|
||||
// Global kill switch (SEO-Einstellungen → "Website für Suchmaschinen
|
||||
// sichtbar") — block everything rather than list exceptions.
|
||||
// sichtbar") - block everything rather than list exceptions.
|
||||
return { rules: [{ userAgent: "*", disallow: "/" }] };
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export default async function robots(): Promise<MetadataRoute.Robots> {
|
||||
"/admin",
|
||||
"/admin/*",
|
||||
"/api/*",
|
||||
// Customer accounts — never public.
|
||||
// Customer accounts - never public.
|
||||
"/konto",
|
||||
"/konto/*",
|
||||
"/login",
|
||||
@@ -32,7 +32,7 @@ export default async function robots(): Promise<MetadataRoute.Robots> {
|
||||
"/auth/*",
|
||||
// Private calendar-subscription feed (secret-token URLs).
|
||||
"/calendar/*",
|
||||
// Meeting join/call flow — session-gated, never a page worth indexing.
|
||||
// Meeting join/call flow - session-gated, never a page worth indexing.
|
||||
"/termine/*/beitreten",
|
||||
"/termine/*/call",
|
||||
],
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ import type { MetadataRoute } from "next";
|
||||
import { getAllEvents, getOffers, getPosts } from "@/lib/payload/content";
|
||||
import { getSiteUrl, isSiteIndexable } from "@/lib/seo/config";
|
||||
|
||||
// Reads live CMS content on every request — must not be statically
|
||||
// Reads live CMS content on every request - must not be statically
|
||||
// prerendered at build time (no DB is available during `next build`; see
|
||||
// AGENTS.md / the same convention used by every other CMS-backed route).
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -10,7 +10,7 @@ export const dynamic = "force-dynamic";
|
||||
// Static, always-public routes that aren't backed by a dynamic [slug]
|
||||
// collection. Deliberately excludes: /login, /registrieren, /konto*,
|
||||
// /admin*, /calendar/*, /auth/*, /termine/*/beitreten, /termine/*/call,
|
||||
// /api/* — none of those are public content (see app/robots.ts, which
|
||||
// /api/* - none of those are public content (see app/robots.ts, which
|
||||
// mirrors this same exclusion list).
|
||||
const staticRoutes: { path: string; changeFrequency: MetadataRoute.Sitemap[number]["changeFrequency"]; priority: number }[] = [
|
||||
{ path: "", changeFrequency: "weekly", priority: 1 },
|
||||
@@ -40,7 +40,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
}));
|
||||
|
||||
// getOffers/getAllEvents/getPosts already only return published,
|
||||
// publicly-readable documents (overrideAccess: false — see
|
||||
// publicly-readable documents (overrideAccess: false - see
|
||||
// lib/payload/content.ts's header comment).
|
||||
const [offers, events, posts] = await Promise.all([getOffers(), getAllEvents(), getPosts()]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user