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:
@@ -0,0 +1,146 @@
|
||||
import type { GlobalConfig } from "payload";
|
||||
import { isAdmin } from "@/access";
|
||||
|
||||
const WEEKDAYS = [
|
||||
{ label: "Montag", value: "Monday" },
|
||||
{ label: "Dienstag", value: "Tuesday" },
|
||||
{ label: "Mittwoch", value: "Wednesday" },
|
||||
{ label: "Donnerstag", value: "Thursday" },
|
||||
{ label: "Freitag", value: "Friday" },
|
||||
{ label: "Samstag", value: "Saturday" },
|
||||
{ label: "Sonntag", value: "Sunday" },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Central SEO settings, edited once by Anna, applied automatically across
|
||||
* the whole website (see lib/seo/*). Everything here is optional — nothing
|
||||
* is invented if a field is left blank (see lib/seo/jsonld.ts, which only
|
||||
* emits LocalBusiness structured data once real address data exists).
|
||||
*/
|
||||
export const SEOSettings: GlobalConfig = {
|
||||
slug: "seo-settings",
|
||||
label: "SEO-Einstellungen",
|
||||
admin: {
|
||||
description: "Globale SEO-Vorgaben für die gesamte Website — greift automatisch überall dort, wo eine Seite keine eigenen SEO-Angaben hat.",
|
||||
group: "SEO",
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
update: isAdmin,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: "tabs",
|
||||
tabs: [
|
||||
{
|
||||
label: "Allgemein",
|
||||
fields: [
|
||||
{
|
||||
name: "siteUrl",
|
||||
type: "text",
|
||||
label: "Website-URL",
|
||||
admin: {
|
||||
description: "Nur bei Bedarf setzen (z. B. https://anouma.org) — überschreibt die Server-Konfiguration. Leer lassen, um die technische Standardeinstellung zu verwenden.",
|
||||
},
|
||||
validate: (value: string | null | undefined) => {
|
||||
if (!value) return true;
|
||||
return /^https?:\/\/.+/.test(value) || "Bitte eine vollständige URL angeben (z. B. https://anouma.org).";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "defaultDescription",
|
||||
type: "textarea",
|
||||
label: "Standard-Beschreibung",
|
||||
maxLength: 300,
|
||||
admin: { description: "Wird verwendet, wenn eine Seite keine eigene Meta-Beschreibung hat." },
|
||||
},
|
||||
{
|
||||
name: "defaultOgImage",
|
||||
type: "upload",
|
||||
relationTo: "media",
|
||||
label: "Standard Social-Media-Bild",
|
||||
admin: { description: "Wird verwendet, wenn eine Seite kein eigenes Open-Graph-Bild hat." },
|
||||
},
|
||||
{
|
||||
name: "robotsIndexable",
|
||||
type: "checkbox",
|
||||
label: "Website für Suchmaschinen sichtbar",
|
||||
defaultValue: true,
|
||||
admin: {
|
||||
description: "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.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Lokale Informationen",
|
||||
admin: { description: "Werden nur verwendet, wenn tatsächlich ausgefüllt — es werden keine Angaben erfunden." },
|
||||
fields: [
|
||||
{
|
||||
name: "businessName",
|
||||
type: "text",
|
||||
label: "Geschäfts-/Markenname",
|
||||
defaultValue: "Anouma",
|
||||
},
|
||||
{
|
||||
name: "businessDescription",
|
||||
type: "textarea",
|
||||
label: "Kurzbeschreibung",
|
||||
admin: { description: "Kurzer, sachlicher Beschreibungstext für strukturierte Daten (nicht der Website-Text)." },
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{ name: "latitude", type: "number", label: "Breitengrad (optional)", admin: { width: "50%" } },
|
||||
{ name: "longitude", type: "number", label: "Längengrad (optional)", admin: { width: "50%" } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "openingHours",
|
||||
type: "array",
|
||||
label: "Öffnungs-/Erreichbarkeitszeiten",
|
||||
admin: { description: "Optional — nur für tatsächlich feste Zeiten (z. B. Sprechzeiten)." },
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{
|
||||
name: "days",
|
||||
type: "select",
|
||||
label: "Tage",
|
||||
hasMany: true,
|
||||
required: true,
|
||||
options: [...WEEKDAYS],
|
||||
admin: { width: "50%" },
|
||||
},
|
||||
{ name: "opens", type: "text", label: "Von (z. B. 09:00)", required: true, admin: { width: "25%" } },
|
||||
{ name: "closes", type: "text", label: "Bis (z. B. 17:00)", required: true, admin: { width: "25%" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Social Media",
|
||||
fields: [
|
||||
{
|
||||
name: "socialLinks",
|
||||
type: "array",
|
||||
label: "Profile",
|
||||
fields: [
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{ name: "platform", type: "text", label: "Plattform (z. B. Instagram)", required: true, admin: { width: "35%" } },
|
||||
{ name: "url", type: "text", label: "Link", required: true, admin: { width: "65%" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user