Files
anouma/globals/SEOSettings.ts
T
maroandClaude Sonnet 5 1cd15aff25 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>
2026-08-25 22:57:31 +02:00

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%" } },
],
},
],
},
],
},
],
},
],
};