Files
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

71 lines
2.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Optional, dev-only SEO sanity check — NOT part of the build or deploy
* pipeline, never blocks production. Run manually:
*
* npm run seo:check
*
* Only ever warns; exits 0 regardless of findings (a content gap here isn't
* a broken deploy).
*/
import { getPayload } from "payload";
import config from "../payload.config";
let warnings = 0;
function warn(message: string) {
warnings++;
console.warn(` ⚠ ${message}`);
}
async function checkCollection(
payload: Awaited<ReturnType<typeof getPayload>>,
collection: "offers" | "events" | "posts",
label: string,
) {
console.log(`${label}:`);
const { docs } = await payload.find({ collection, limit: 500, depth: 0 });
const slugCounts = new Map<string, number>();
for (const doc of docs as { slug?: string | null; title?: string }[]) {
if (doc.slug) slugCounts.set(doc.slug, (slugCounts.get(doc.slug) ?? 0) + 1);
}
for (const [slug, count] of slugCounts) {
if (count > 1) warn(`Slug „${slug}“ kommt ${count}× vor.`);
}
for (const doc of docs as { slug?: string | null; title?: string; seo?: { description?: string | null } | null }[]) {
if (!doc.title?.trim()) warn(`Dokument ohne Titel (slug: ${doc.slug ?? "—"}).`);
if (!doc.slug?.trim()) warn(`Dokument ohne Slug: „${doc.title ?? "—"}“.`);
}
if (docs.length === 0) console.log(" (keine Einträge)");
}
async function checkMedia(payload: Awaited<ReturnType<typeof getPayload>>) {
console.log("Medien (Alt-Texte):");
const { docs } = await payload.find({ collection: "media", limit: 1000, depth: 0 });
const missingAlt = (docs as { alt?: string; filename?: string }[]).filter((doc) => !doc.alt?.trim());
if (missingAlt.length === 0) {
console.log(" Alle Bilder haben einen Alt-Text.");
} else {
for (const doc of missingAlt) warn(`Bild ohne Alt-Text: ${doc.filename ?? "unbekannt"}`);
}
}
async function run() {
const payload = await getPayload({ config });
await checkCollection(payload, "offers", "Angebote");
await checkCollection(payload, "events", "Termine");
await checkCollection(payload, "posts", "Aktuelles");
await checkMedia(payload);
console.log(`\n${warnings === 0 ? "Keine Auffälligkeiten gefunden." : `${warnings} Hinweis(e) gefunden — siehe oben.`}`);
process.exit(0);
}
run().catch((err) => {
console.error(err);
process.exit(0);
});