Files
at-mintel/packages/seo-engine/src/steps/content-gap.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

85 lines
2.6 KiB
TypeScript

import { llmJsonRequest } from "../llm-client.js";
import type { TopicCluster } from "../types.js";
export interface ExistingPage {
url: string;
title: string;
}
export interface ContentGap {
recommendedTitle: string;
targetKeyword: string;
relatedCluster: string;
priority: "high" | "medium" | "low";
rationale: string;
}
const CONTENT_GAP_SYSTEM_PROMPT = `
You are a senior SEO Content Strategist. Your job is to compare a set of TOPIC CLUSTERS
(keywords the company should rank for) against the EXISTING PAGES on their website.
### OBJECTIVE:
Identify content gaps — topics/keywords that have NO corresponding page yet.
For each gap, recommend a page title, the primary target keyword, which cluster it belongs to,
and a priority (high/medium/low) based on commercial intent and relevance.
### RULES:
- Only recommend gaps for topics that are genuinely MISSING from the existing pages.
- Do NOT recommend pages that already exist (even if the title is slightly different — use semantic matching).
- Priority "high" = commercial/transactional intent, directly drives revenue.
- Priority "medium" = informational with strong industry relevance.
- Priority "low" = broad, top-of-funnel topics.
- LANGUAGE: Match the language of the project context (if German context, recommend German titles).
### OUTPUT FORMAT:
{
"contentGaps": [
{
"recommendedTitle": "string",
"targetKeyword": "string",
"relatedCluster": "string",
"priority": "high" | "medium" | "low",
"rationale": "string"
}
]
}
`;
export async function analyzeContentGaps(
topicClusters: TopicCluster[],
existingPages: ExistingPage[],
config: { openRouterApiKey: string; model?: string },
): Promise<ContentGap[]> {
if (topicClusters.length === 0) return [];
if (existingPages.length === 0) {
console.log(
"[Content Gap] No existing pages provided, skipping gap analysis.",
);
return [];
}
const userPrompt = `
TOPIC CLUSTERS (what the company SHOULD rank for):
${JSON.stringify(topicClusters, null, 2)}
EXISTING PAGES ON THE WEBSITE:
${existingPages.map((p, i) => `${i + 1}. "${p.title}" — ${p.url}`).join("\n")}
Identify ALL content gaps. Be thorough but precise.
`;
try {
const { data } = await llmJsonRequest<{ contentGaps: ContentGap[] }>({
model: config.model || "google/gemini-2.5-pro",
apiKey: config.openRouterApiKey,
systemPrompt: CONTENT_GAP_SYSTEM_PROMPT,
userPrompt,
});
return data.contentGaps || [];
} catch (err) {
console.error("[Content Gap] Analysis failed:", (err as Error).message);
return [];
}
}