// ============================================================================ // @mintel/page-audit — AI Report Generator // Uses Gemini Pro (via OpenRouter) to synthesize DataForSEO data into // a structured IST-analysis report in German. // ============================================================================ import type { DomainAuditResult, AuditReport, PageAuditData, AuditIssue } from "./types.js"; const OPENROUTER_BASE = "https://openrouter.ai/api/v1"; const REPORT_MODEL = "google/gemini-3.1-pro-preview"; /** * Generate an AI-powered IST-analysis report from audit data. */ export async function generateAuditReport( audit: DomainAuditResult, openrouterKey: string, ): Promise { const summary = buildAuditSummary(audit); const systemPrompt = ` Du bist ein Senior SEO- und UX-Stratege. Analysiere die technischen Audit-Daten einer Website und erstelle einen präzisen IST-Analyse-Bericht auf DEUTSCH. Stil: - Faktisch, direkt, kein Bullshit - Konkrete Handlungsempfehlungen, keine vagen Floskeln - Technik-verständlich für Entscheider (nicht für Entwickler) Output: JSON (kein Markdown drumherum) `; const userPrompt = ` Website: ${audit.domain} Seiten gecrawlt: ${audit.totalPages} Audit-Datum: ${audit.auditedAt} === TECHNISCHE AUSWERTUNG === ${summary} === TOP-ISSUES === ${audit.topIssues.map((i) => `[${i.severity.toUpperCase()}] ${i.message}${i.count ? ` (${i.count}x)` : ""}`).join("\n")} Erstelle jetzt den IST-Analyse-Report als JSON: { "executiveSummary": string (2-3 Sätze über den aktuellen Zustand der Website), "strengths": string[] (max 4, was gut läuft), "criticalIssues": string[] (max 5, sofort zu beheben), "quickWins": string[] (max 4, einfach umzusetzen mit großer Wirkung), "strategicRecommendations": string[] (max 4, mittel-/langfristig), "seoScore": number (0-100, realistisch), "uxScore": number (0-100, realistisch), "performanceScore": number (0-100, realistisch), "overallHealth": "critical" | "needs-work" | "good" | "excellent" } `; const response = await fetch(`${OPENROUTER_BASE}/chat/completions`, { method: "POST", headers: { Authorization: `Bearer ${openrouterKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: REPORT_MODEL, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt }, ], response_format: { type: "json_object" }, }), }); if (!response.ok) { throw new Error(`LLM request failed: ${response.status} ${await response.text()}`); } const json = await response.json(); const content = json.choices?.[0]?.message?.content || "{}"; // Clean up markdown JSON wrappers if present const cleaned = content.replace(/^```(?:json)?\n?/m, "").replace(/```$/m, "").trim(); try { return JSON.parse(cleaned) as AuditReport; } catch { throw new Error(`Could not parse AI report: ${cleaned.slice(0, 200)}`); } } /** * Build a human-readable text summary of the audit data for the LLM prompt. */ function buildAuditSummary(audit: DomainAuditResult): string { const pages = audit.pages; const brokenPages = pages.filter((p) => p.statusCode >= 400); const noTitle = pages.filter((p) => !p.pageTitle); const noDesc = pages.filter((p) => !p.metaDescription); const noH1 = pages.filter((p) => !p.h1); const notIndexable = pages.filter((p) => !p.seo.isIndexable); const noViewport = pages.filter((p) => !p.seo.hasViewport); const slowPages = pages.filter((p) => p.loadTime !== null && p.loadTime > 3000); const imagesWithoutAlt = pages.reduce((sum, p) => sum + p.images.missingAlt, 0); const totalImages = pages.reduce((sum, p) => sum + p.images.total, 0); const avgLoad = pages .filter((p) => p.loadTime !== null) .reduce((sum, p, _, arr) => sum + (p.loadTime || 0) / arr.length, 0); const lines = [ `Seiten gesamt: ${pages.length}`, `Seiten mit Fehler (4xx/5xx): ${brokenPages.length}`, `Seiten ohne : ${noTitle.length}`, `Seiten ohne Meta-Description: ${noDesc.length}`, `Seiten ohne H1: ${noH1.length}`, `Nicht-indexierbare Seiten: ${notIndexable.length}`, `Seiten ohne Viewport-Meta: ${noViewport.length}`, `Bilder gesamt: ${totalImages}, davon ohne alt-Attribut: ${imagesWithoutAlt}`, `Langsame Seiten (>3s): ${slowPages.length}`, `Ø Ladezeit: ${avgLoad > 0 ? `${(avgLoad / 1000).toFixed(1)}s` : "unbekannt"}`, ]; // Core Web Vitals (from first valid page) const lcpPages = pages.filter((p) => p.performance.lcp !== null); if (lcpPages.length > 0) { const avgLcp = lcpPages.reduce((s, p) => s + (p.performance.lcp || 0), 0) / lcpPages.length; lines.push(`Ø LCP: ${(avgLcp / 1000).toFixed(1)}s (Ziel: <2.5s)`); } const clsPages = pages.filter((p) => p.performance.cls !== null); if (clsPages.length > 0) { const avgCls = clsPages.reduce((s, p) => s + (p.performance.cls || 0), 0) / clsPages.length; lines.push(`Ø CLS: ${avgCls.toFixed(3)} (Ziel: <0.1)`); } // Top pages by issues const worstPages = [...pages] .sort((a, b) => b.issues.length - a.issues.length) .slice(0, 5); if (worstPages.length > 0) { lines.push("\nSeiten mit den meisten Problemen:"); for (const page of worstPages) { lines.push(` ${page.url}: ${page.issues.length} Issues (${page.issues.map((i) => i.code).join(", ")})`); } } return lines.join("\n"); }