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>
This commit is contained in:
@@ -78,6 +78,7 @@ export interface Config {
|
||||
availability: Availability;
|
||||
'availability-overrides': AvailabilityOverride;
|
||||
'booking-requests': BookingRequest;
|
||||
redirects: Redirect;
|
||||
'payload-kv': PayloadKv;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
@@ -95,6 +96,7 @@ export interface Config {
|
||||
availability: AvailabilitySelect<false> | AvailabilitySelect<true>;
|
||||
'availability-overrides': AvailabilityOverridesSelect<false> | AvailabilityOverridesSelect<true>;
|
||||
'booking-requests': BookingRequestsSelect<false> | BookingRequestsSelect<true>;
|
||||
redirects: RedirectsSelect<false> | RedirectsSelect<true>;
|
||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
@@ -113,6 +115,7 @@ export interface Config {
|
||||
booking: Booking;
|
||||
'meeting-settings': MeetingSetting;
|
||||
'booking-settings': BookingSetting;
|
||||
'seo-settings': SeoSetting;
|
||||
};
|
||||
globalsSelect: {
|
||||
home: HomeSelect<false> | HomeSelect<true>;
|
||||
@@ -123,6 +126,7 @@ export interface Config {
|
||||
booking: BookingSelect<false> | BookingSelect<true>;
|
||||
'meeting-settings': MeetingSettingsSelect<false> | MeetingSettingsSelect<true>;
|
||||
'booking-settings': BookingSettingsSelect<false> | BookingSettingsSelect<true>;
|
||||
'seo-settings': SeoSettingsSelect<false> | SeoSettingsSelect<true>;
|
||||
};
|
||||
locale: null;
|
||||
widgets: {
|
||||
@@ -180,6 +184,7 @@ export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
role: 'admin';
|
||||
calendarFeedTokenHash?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -212,6 +217,12 @@ export interface Customer {
|
||||
* Optional — für eine spätere Nutzung vorbereitet, aktuell nicht verpflichtend.
|
||||
*/
|
||||
phone?: string | null;
|
||||
/**
|
||||
* Wird automatisch gesetzt, sobald der Bestätigungslink aus der E-Mail angeklickt wird. Kann hier bei Bedarf manuell gesetzt werden.
|
||||
*/
|
||||
emailVerified?: boolean | null;
|
||||
emailVerificationTokenHash?: string | null;
|
||||
emailVerificationExpires?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -352,6 +363,21 @@ export interface Event {
|
||||
*/
|
||||
isPrivateBooking?: boolean | null;
|
||||
bookingRequest?: (number | null) | BookingRequest;
|
||||
seo?: {
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Seitentitel erzeugt.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Inhalt erzeugt (erste ~160 Zeichen).
|
||||
*/
|
||||
description?: string | null;
|
||||
keywords?: string | null;
|
||||
/**
|
||||
* Falls leer: Hauptbild dieser Seite bzw. das Standard-Social-Bild aus den SEO-Einstellungen.
|
||||
*/
|
||||
ogImage?: (number | null) | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
@@ -439,6 +465,21 @@ export interface Offer {
|
||||
*/
|
||||
price?: string | null;
|
||||
durationMinutes?: number | null;
|
||||
seo?: {
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Seitentitel erzeugt.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Inhalt erzeugt (erste ~160 Zeichen).
|
||||
*/
|
||||
description?: string | null;
|
||||
keywords?: string | null;
|
||||
/**
|
||||
* Falls leer: Hauptbild dieser Seite bzw. das Standard-Social-Bild aus den SEO-Einstellungen.
|
||||
*/
|
||||
ogImage?: (number | null) | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
@@ -477,6 +518,22 @@ export interface Post {
|
||||
};
|
||||
coverImage?: (number | null) | Media;
|
||||
publishDate?: string | null;
|
||||
author?: string | null;
|
||||
seo?: {
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Seitentitel erzeugt.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Inhalt erzeugt (erste ~160 Zeichen).
|
||||
*/
|
||||
description?: string | null;
|
||||
keywords?: string | null;
|
||||
/**
|
||||
* Falls leer: Hauptbild dieser Seite bzw. das Standard-Social-Bild aus den SEO-Einstellungen.
|
||||
*/
|
||||
ogImage?: (number | null) | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
@@ -533,6 +590,27 @@ export interface AvailabilityOverride {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* Leitet eine alte URL (z. B. nach einer Slug-Änderung bei Angeboten, Terminen oder Aktuelles) dauerhaft auf eine neue weiter.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "redirects".
|
||||
*/
|
||||
export interface Redirect {
|
||||
id: number;
|
||||
/**
|
||||
* Nur der Pfad, z. B. /angebote/doula (ohne Domain).
|
||||
*/
|
||||
fromPath: string;
|
||||
/**
|
||||
* Ziel-Pfad, z. B. /angebote/doula-begleitung.
|
||||
*/
|
||||
toPath: string;
|
||||
type: 'permanent' | 'temporary';
|
||||
enabled?: boolean | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv".
|
||||
@@ -596,6 +674,10 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'booking-requests';
|
||||
value: number | BookingRequest;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'redirects';
|
||||
value: number | Redirect;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user:
|
||||
@@ -656,6 +738,7 @@ export interface PayloadMigration {
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
role?: T;
|
||||
calendarFeedTokenHash?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
@@ -680,6 +763,9 @@ export interface UsersSelect<T extends boolean = true> {
|
||||
export interface CustomersSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
phone?: T;
|
||||
emailVerified?: T;
|
||||
emailVerificationTokenHash?: T;
|
||||
emailVerificationExpires?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
@@ -777,6 +863,14 @@ export interface EventsSelect<T extends boolean = true> {
|
||||
hostReminder30Sent?: T;
|
||||
isPrivateBooking?: T;
|
||||
bookingRequest?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
keywords?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
@@ -792,6 +886,15 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
content?: T;
|
||||
coverImage?: T;
|
||||
publishDate?: T;
|
||||
author?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
keywords?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
@@ -812,6 +915,14 @@ export interface OffersSelect<T extends boolean = true> {
|
||||
bookable?: T;
|
||||
price?: T;
|
||||
durationMinutes?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
keywords?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
@@ -881,6 +992,18 @@ export interface BookingRequestsSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "redirects_select".
|
||||
*/
|
||||
export interface RedirectsSelect<T extends boolean = true> {
|
||||
fromPath?: T;
|
||||
toPath?: T;
|
||||
type?: T;
|
||||
enabled?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
@@ -963,6 +1086,21 @@ export interface Home {
|
||||
| null;
|
||||
ctaTitle?: string | null;
|
||||
ctaLead?: string | null;
|
||||
seo?: {
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Seitentitel erzeugt.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Inhalt erzeugt (erste ~160 Zeichen).
|
||||
*/
|
||||
description?: string | null;
|
||||
keywords?: string | null;
|
||||
/**
|
||||
* Falls leer: Hauptbild dieser Seite bzw. das Standard-Social-Bild aus den SEO-Einstellungen.
|
||||
*/
|
||||
ogImage?: (number | null) | Media;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
@@ -995,6 +1133,21 @@ export interface About {
|
||||
closingLead?: string | null;
|
||||
closingHighlight?: string | null;
|
||||
closingParagraph?: string | null;
|
||||
seo?: {
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Seitentitel erzeugt.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Inhalt erzeugt (erste ~160 Zeichen).
|
||||
*/
|
||||
description?: string | null;
|
||||
keywords?: string | null;
|
||||
/**
|
||||
* Falls leer: Hauptbild dieser Seite bzw. das Standard-Social-Bild aus den SEO-Einstellungen.
|
||||
*/
|
||||
ogImage?: (number | null) | Media;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
@@ -1009,6 +1162,21 @@ export interface AngeboteIntro {
|
||||
eyebrow?: string | null;
|
||||
title: string;
|
||||
lead?: string | null;
|
||||
seo?: {
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Seitentitel erzeugt.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Inhalt erzeugt (erste ~160 Zeichen).
|
||||
*/
|
||||
description?: string | null;
|
||||
keywords?: string | null;
|
||||
/**
|
||||
* Falls leer: Hauptbild dieser Seite bzw. das Standard-Social-Bild aus den SEO-Einstellungen.
|
||||
*/
|
||||
ogImage?: (number | null) | Media;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
@@ -1023,6 +1191,21 @@ export interface AktuellesIntro {
|
||||
eyebrow?: string | null;
|
||||
title: string;
|
||||
lead?: string | null;
|
||||
seo?: {
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Seitentitel erzeugt.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Inhalt erzeugt (erste ~160 Zeichen).
|
||||
*/
|
||||
description?: string | null;
|
||||
keywords?: string | null;
|
||||
/**
|
||||
* Falls leer: Hauptbild dieser Seite bzw. das Standard-Social-Bild aus den SEO-Einstellungen.
|
||||
*/
|
||||
ogImage?: (number | null) | Media;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
@@ -1040,6 +1223,21 @@ export interface Contact {
|
||||
email: string;
|
||||
phone?: string | null;
|
||||
region?: string | null;
|
||||
seo?: {
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Seitentitel erzeugt.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Inhalt erzeugt (erste ~160 Zeichen).
|
||||
*/
|
||||
description?: string | null;
|
||||
keywords?: string | null;
|
||||
/**
|
||||
* Falls leer: Hauptbild dieser Seite bzw. das Standard-Social-Bild aus den SEO-Einstellungen.
|
||||
*/
|
||||
ogImage?: (number | null) | Media;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
@@ -1062,6 +1260,21 @@ export interface Booking {
|
||||
}[]
|
||||
| null;
|
||||
formNote?: string | null;
|
||||
seo?: {
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Seitentitel erzeugt.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Falls leer: automatisch aus dem Inhalt erzeugt (erste ~160 Zeichen).
|
||||
*/
|
||||
description?: string | null;
|
||||
keywords?: string | null;
|
||||
/**
|
||||
* Falls leer: Hauptbild dieser Seite bzw. das Standard-Social-Bild aus den SEO-Einstellungen.
|
||||
*/
|
||||
ogImage?: (number | null) | Media;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
@@ -1091,6 +1304,63 @@ export interface BookingSetting {
|
||||
street?: string | null;
|
||||
postalCode?: string | null;
|
||||
city?: string | null;
|
||||
region?: string | null;
|
||||
/**
|
||||
* Für strukturierte Daten, z. B. „Deutschland“.
|
||||
*/
|
||||
country?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* Globale SEO-Vorgaben für die gesamte Website — greift automatisch überall dort, wo eine Seite keine eigenen SEO-Angaben hat.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "seo-settings".
|
||||
*/
|
||||
export interface SeoSetting {
|
||||
id: number;
|
||||
/**
|
||||
* Nur bei Bedarf setzen (z. B. https://anouma.org) — überschreibt die Server-Konfiguration. Leer lassen, um die technische Standardeinstellung zu verwenden.
|
||||
*/
|
||||
siteUrl?: string | null;
|
||||
/**
|
||||
* Wird verwendet, wenn eine Seite keine eigene Meta-Beschreibung hat.
|
||||
*/
|
||||
defaultDescription?: string | null;
|
||||
/**
|
||||
* Wird verwendet, wenn eine Seite kein eigenes Open-Graph-Bild hat.
|
||||
*/
|
||||
defaultOgImage?: (number | null) | Media;
|
||||
/**
|
||||
* Deaktivieren, um die gesamte öffentliche Website vorübergehend für Suchmaschinen zu sperren (z. B. während des Aufbaus). Betrifft nicht Admin/Konto — die sind ohnehin immer gesperrt.
|
||||
*/
|
||||
robotsIndexable?: boolean | null;
|
||||
businessName?: string | null;
|
||||
/**
|
||||
* Kurzer, sachlicher Beschreibungstext für strukturierte Daten (nicht der Website-Text).
|
||||
*/
|
||||
businessDescription?: string | null;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
/**
|
||||
* Optional — nur für tatsächlich feste Zeiten (z. B. Sprechzeiten).
|
||||
*/
|
||||
openingHours?:
|
||||
| {
|
||||
days: ('Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday')[];
|
||||
opens: string;
|
||||
closes: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
socialLinks?:
|
||||
| {
|
||||
platform: string;
|
||||
url: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
@@ -1119,6 +1389,14 @@ export interface HomeSelect<T extends boolean = true> {
|
||||
};
|
||||
ctaTitle?: T;
|
||||
ctaLead?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
keywords?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
@@ -1135,6 +1413,14 @@ export interface AboutSelect<T extends boolean = true> {
|
||||
closingLead?: T;
|
||||
closingHighlight?: T;
|
||||
closingParagraph?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
keywords?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
@@ -1147,6 +1433,14 @@ export interface AngeboteIntroSelect<T extends boolean = true> {
|
||||
eyebrow?: T;
|
||||
title?: T;
|
||||
lead?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
keywords?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
@@ -1159,6 +1453,14 @@ export interface AktuellesIntroSelect<T extends boolean = true> {
|
||||
eyebrow?: T;
|
||||
title?: T;
|
||||
lead?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
keywords?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
@@ -1174,6 +1476,14 @@ export interface ContactSelect<T extends boolean = true> {
|
||||
email?: T;
|
||||
phone?: T;
|
||||
region?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
keywords?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
@@ -1194,6 +1504,14 @@ export interface BookingSelect<T extends boolean = true> {
|
||||
id?: T;
|
||||
};
|
||||
formNote?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
keywords?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
@@ -1219,6 +1537,40 @@ export interface BookingSettingsSelect<T extends boolean = true> {
|
||||
street?: T;
|
||||
postalCode?: T;
|
||||
city?: T;
|
||||
region?: T;
|
||||
country?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "seo-settings_select".
|
||||
*/
|
||||
export interface SeoSettingsSelect<T extends boolean = true> {
|
||||
siteUrl?: T;
|
||||
defaultDescription?: T;
|
||||
defaultOgImage?: T;
|
||||
robotsIndexable?: T;
|
||||
businessName?: T;
|
||||
businessDescription?: T;
|
||||
latitude?: T;
|
||||
longitude?: T;
|
||||
openingHours?:
|
||||
| T
|
||||
| {
|
||||
days?: T;
|
||||
opens?: T;
|
||||
closes?: T;
|
||||
id?: T;
|
||||
};
|
||||
socialLinks?:
|
||||
| T
|
||||
| {
|
||||
platform?: T;
|
||||
url?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
|
||||
Reference in New Issue
Block a user