Files
at-mintel/packages/seo-engine/src/editor.ts
Marc Mintel ded9da7d32
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Failing after 51s
Monorepo Pipeline / 🧹 Lint (push) Failing after 2m25s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m28s
Monorepo Pipeline / 🚀 Release (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
feat(seo-engine): implement competitor scraper, MDX draft editor, and strategy report generator
2026-03-02 10:16:11 +01:00

149 lines
6.1 KiB
TypeScript

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<string, ReverseEngineeredBriefing>,
config: FileEditorConfig,
): Promise<string[]> {
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;
}