feat: migrate npm registry from Verdaccio to Gitea Packages
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 35s
Monorepo Pipeline / 🧪 Test (push) Failing after 35s
Monorepo Pipeline / 🏗️ Build (push) Failing after 12s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 1s
Monorepo Pipeline / 🧹 Lint (push) Failing after 35s
Monorepo Pipeline / 🧪 Test (push) Failing after 35s
Monorepo Pipeline / 🏗️ Build (push) Failing after 12s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Image Processor (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
149
packages/page-audit/src/report.ts
Normal file
149
packages/page-audit/src/report.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
// ============================================================================
|
||||
// @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<AuditReport> {
|
||||
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 <title>: ${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");
|
||||
}
|
||||
Reference in New Issue
Block a user