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
85 lines
2.6 KiB
TypeScript
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 [];
|
|
}
|
|
}
|