feat(seo-engine): implement competitor scraper, MDX draft editor, and strategy report generator
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
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
This commit is contained in:
84
packages/seo-engine/src/steps/content-gap.ts
Normal file
84
packages/seo-engine/src/steps/content-gap.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user