- 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>
147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
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%" } },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|