import { appLocales } from "../i18n/routing"; import type { SeoSettings } from "./seo-settings"; import type { SiteSettings, SiteSettingsMediaBindings } from "./site-settings"; export type SeoCheckStatus = "ok" | "warn" | "error"; export type SeoCheck = { id: string; label: string; status: SeoCheckStatus; detail: string; }; /** * Pure readiness checklist shown on the admin SEO page. Every input comes from * the caller so the same function is testable without a database. */ export function buildSeoChecklist(input: { seo: SeoSettings; settings: SiteSettings; bindings: SiteSettingsMediaBindings; maintenanceEnabled: boolean; publishedProjectCount: number; sitemapEntryCount: number; siteUrl: string; }): SeoCheck[] { const { seo, settings, bindings, maintenanceEnabled, publishedProjectCount, sitemapEntryCount, siteUrl } = input; const checks: SeoCheck[] = []; checks.push({ id: "indexing", label: "Indexierung", status: seo.allowIndexing && !maintenanceEnabled ? "ok" : "error", detail: maintenanceEnabled ? "Wartungsmodus aktiv: robots.txt sperrt alles, Sitemap ist leer." : seo.allowIndexing ? "Suchmaschinen duerfen die Seite indexieren." : "Indexierung ist deaktiviert (noindex + robots disallow).", }); checks.push({ id: "site-url", label: "Oeffentliche URL", status: /^https:\/\//.test(siteUrl) && !/localhost|127\.0\.0\.1/.test(siteUrl) ? "ok" : "warn", detail: `Canonical Basis: ${siteUrl}`, }); for (const locale of appLocales) { const localeSettings = settings.locales[locale]; const descriptionLength = localeSettings.siteDescription.length; const status: SeoCheckStatus = descriptionLength === 0 ? "error" : descriptionLength < 50 || descriptionLength > 160 ? "warn" : "ok"; checks.push({ id: `description-${locale}`, label: `Meta Description (${locale.toUpperCase()})`, status, detail: descriptionLength === 0 ? "Fehlt. Wird unter Settings > Localization gepflegt." : `${descriptionLength} Zeichen (empfohlen 50-160).`, }); } checks.push({ id: "og-image", label: "Standard Share Bild (OG)", status: bindings.defaultOgImage ? "ok" : "warn", detail: bindings.defaultOgImage ? "Gesetzt. Projekte nutzen ihr Cover, alle anderen Seiten dieses Bild." : "Nicht gesetzt. Links ohne Vorschaubild. Unter Settings > Brand pflegen.", }); checks.push({ id: "favicon", label: "Favicon", status: bindings.favicon ? "ok" : "warn", detail: bindings.favicon ? "Gesetzt." : "Nicht gesetzt (Fallback-Icon wird generiert).", }); checks.push({ id: "verification", label: "Search Console / Bing", status: seo.googleSiteVerification || seo.bingSiteVerification ? "ok" : "warn", detail: seo.googleSiteVerification || seo.bingSiteVerification ? "Verification Meta Tags werden ausgegeben." : "Kein Verification Code hinterlegt.", }); checks.push({ id: "structured-data", label: "Strukturierte Daten (JSON-LD)", status: seo.structuredDataName || settings.locales[settings.defaultLocale].siteName ? "ok" : "warn", detail: `${seo.structuredDataType} + WebSite auf allen Seiten, CreativeWork pro Projekt.`, }); checks.push({ id: "projects", label: "Veroeffentlichte Projekte", status: publishedProjectCount > 0 ? "ok" : "warn", detail: publishedProjectCount > 0 ? `${publishedProjectCount} Projekt(e) in der Sitemap.` : "Noch kein Projekt veroeffentlicht. Portfolio-Seiten sind leer.", }); checks.push({ id: "sitemap", label: "Sitemap", status: sitemapEntryCount > 0 ? "ok" : maintenanceEnabled || !seo.allowIndexing ? "warn" : "error", detail: `${sitemapEntryCount} URL(s) in /sitemap.xml (alle Sprachen, mit hreflang).`, }); return checks; } export function summarizeSeoChecklist(checks: SeoCheck[]) { return { ok: checks.filter((check) => check.status === "ok").length, warn: checks.filter((check) => check.status === "warn").length, error: checks.filter((check) => check.status === "error").length, }; }