import * as fs from "node:fs/promises"; import * as path from "node:path"; import type { ContentGap } from "./types.js"; import type { ReverseEngineeredBriefing } from "./agents/scraper.js"; export interface FileEditorConfig { outputDir: string; authorName?: string; } /** * Generates an SEO-friendly URL slug from a title. */ function createSlug(title: string): string { return title .toLowerCase() .replace(/ä/g, "ae") .replace(/ö/g, "oe") .replace(/ü/g, "ue") .replace(/ß/g, "ss") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); } /** * Automatically creates local .mdx draft files for identified high-priority content gaps. * Each file is self-explanatory: it tells the writer exactly WHY this page needs to exist, * WHAT to write, and HOW to structure the content — all based on real competitor data. */ export async function createGapDrafts( gaps: ContentGap[], briefings: Map, config: FileEditorConfig, ): Promise { const createdFiles: string[] = []; try { await fs.mkdir(path.resolve(process.cwd(), config.outputDir), { recursive: true, }); } catch (e) { console.error( `[File Editor] Could not create directory ${config.outputDir}:`, e, ); return []; } const dateStr = new Date().toISOString().split("T")[0]; for (const gap of gaps) { if (gap.priority === "low") continue; const slug = createSlug(gap.recommendedTitle); const filePath = path.join( path.resolve(process.cwd(), config.outputDir), `${slug}.mdx`, ); const briefing = briefings.get(gap.targetKeyword); const priorityEmoji = gap.priority === "high" ? "🔴" : "🟡"; let body = ""; // ── Intro: Explain WHY this file exists ── body += `{/* ═══════════════════════════════════════════════════════════════════\n`; body += ` 📋 SEO CONTENT BRIEFING — Auto-generated by @mintel/seo-engine\n`; body += ` ═══════════════════════════════════════════════════════════════════\n\n`; body += ` Dieses Dokument wurde automatisch erstellt.\n`; body += ` Es basiert auf einer Analyse der aktuellen Google-Suchergebnisse\n`; body += ` und der Webseiten deiner Konkurrenz.\n\n`; body += ` ▸ Du kannst dieses File direkt als MDX-Seite verwenden.\n`; body += ` ▸ Ersetze den Briefing-Block unten durch deinen eigenen Text.\n`; body += ` ▸ Setze isDraft auf false, wenn der Text fertig ist.\n`; body += ` ═══════════════════════════════════════════════════════════════════ */}\n\n`; // ── Section 1: Warum diese Seite? ── body += `## ${priorityEmoji} Warum diese Seite erstellt werden sollte\n\n`; body += `**Priorität:** ${gap.priority === "high" ? "Hoch — Direkt umsatzrelevant" : "Mittel — Stärkt die thematische Autorität"}\n\n`; body += `${gap.rationale}\n\n`; body += `| Feld | Wert |\n`; body += `|------|------|\n`; body += `| **Focus Keyword** | \`${gap.targetKeyword}\` |\n`; body += `| **Topic Cluster** | ${gap.relatedCluster} |\n`; body += `| **Priorität** | ${gap.priority} |\n\n`; // ── Section 2: Competitor Briefing ── if (briefing) { body += `## 🔍 Konkurrenz-Analyse (Reverse Engineered)\n\n`; body += `> Die folgenden Daten stammen aus einer automatischen Analyse der Webseite,\n`; body += `> die aktuell auf **Platz 1 bei Google** für das Keyword \`${gap.targetKeyword}\` rankt.\n`; body += `> Nutze diese Informationen, um **besseren Content** zu schreiben.\n\n`; body += `### Content-Format des Konkurrenten\n\n`; body += `**${briefing.contentFormat}** — Empfohlene Mindestlänge: **~${briefing.recommendedWordCount} Wörter**\n\n`; body += `### Diese Themen MUSS dein Artikel abdecken\n\n`; body += `Die folgenden Punkte werden vom aktuellen Platz-1-Ranker behandelt. Wenn dein Artikel diese Themen nicht abdeckt, wird es schwer, ihn zu überholen:\n\n`; briefing.coreTopicsToCover.forEach( (t, i) => (body += `${i + 1}. ${t}\n`), ); body += `\n### Fachbegriffe & Entitäten die im Text vorkommen müssen\n\n`; body += `Diese Begriffe signalisieren Google, dass dein Text fachlich tiefgreifend ist. Versuche, möglichst viele davon natürlich in deinen Text einzubauen:\n\n`; briefing.entitiesToInclude.forEach((e) => (body += `- \`${e}\`\n`)); body += `\n### Empfohlene Gliederung\n\n`; body += `Orientiere dich an dieser Struktur (du kannst sie anpassen):\n\n`; briefing.suggestedHeadings.forEach( (h, i) => (body += `${i + 1}. **${h}**\n`), ); } else { body += `## 🔍 Konkurrenz-Analyse\n\n`; body += `> Für dieses Keyword konnte kein Konkurrent gescraped werden.\n`; body += `> Schreibe den Artikel trotzdem — du hast weniger Wettbewerb!\n`; } body += `\n---\n\n`; body += `## ✍️ Dein Content (hier schreiben)\n\n`; body += `{/* Lösche alles oberhalb dieser Zeile, wenn dein Text fertig ist. */}\n\n`; body += `Hier beginnt dein eigentlicher Artikel...\n`; const file = `--- title: "${gap.recommendedTitle}" description: "TODO: Meta-Description mit dem Keyword '${gap.targetKeyword}' schreiben." date: "${dateStr}" author: "${config.authorName || "Mintel SEO Engine"}" tags: ["${gap.relatedCluster}"] isDraft: true focus_keyword: "${gap.targetKeyword}" --- ${body}`; try { await fs.writeFile(filePath, file, "utf8"); console.log(`[File Editor] Created draft: ${filePath}`); createdFiles.push(filePath); } catch (err) { console.error( `[File Editor] Failed to write ${filePath}:`, (err as Error).message, ); } } return createdFiles; }