main
All checks were successful
Backend Deploy 🚀 / build-and-deploy (push) Successful in 2m1s

This commit is contained in:
Harun CAN
2026-02-10 12:27:14 +03:00
parent 80f53511d8
commit fc88faddb9
141 changed files with 35961 additions and 101 deletions

9
src/modules/seo/index.ts Normal file
View File

@@ -0,0 +1,9 @@
// SEO Module - Index exports
// Path: src/modules/seo/index.ts
export * from './seo.module';
export * from './seo.service';
export * from './seo.controller';
export * from './services/keyword-research.service';
export * from './services/content-optimization.service';
export * from './services/competitor-analysis.service';

View File

@@ -0,0 +1,198 @@
// SEO Controller - API endpoints
// Path: src/modules/seo/seo.controller.ts
import { Controller, Get, Post, Body, Query, Param } from '@nestjs/common';
import { SeoService } from './seo.service';
import { KeywordResearchService } from './services/keyword-research.service';
import { ContentOptimizationService } from './services/content-optimization.service';
import { CompetitorAnalysisService } from './services/competitor-analysis.service';
@Controller('seo')
export class SeoController {
constructor(
private readonly seoService: SeoService,
private readonly keywordService: KeywordResearchService,
private readonly optimizationService: ContentOptimizationService,
private readonly competitorService: CompetitorAnalysisService,
) { }
// ========== FULL ANALYSIS ==========
@Post('analyze')
analyzeFull(
@Body() body: {
content: string;
targetKeyword: string;
title?: string;
metaDescription?: string;
url?: string;
competitorDomains?: string[];
},
) {
return this.seoService.analyzeFull(body.content, body.targetKeyword, {
title: body.title,
metaDescription: body.metaDescription,
url: body.url,
competitorDomains: body.competitorDomains,
});
}
@Post('quick-score')
quickScore(
@Body() body: { content: string; targetKeyword?: string },
) {
return { score: this.seoService.quickScore(body.content, body.targetKeyword) };
}
// ========== KEYWORDS ==========
@Get('keywords/suggest/:topic')
suggestKeywords(
@Param('topic') topic: string,
@Query('count') count?: string,
) {
return this.keywordService.suggestKeywords(topic, {
count: count ? parseInt(count, 10) : 20,
includeQuestions: true,
includeLongTail: true,
});
}
@Get('keywords/long-tail/:keyword')
getLongTailKeywords(
@Param('keyword') keyword: string,
@Query('count') count?: string,
) {
return this.keywordService.generateLongTail(keyword, count ? parseInt(count, 10) : 20);
}
@Get('keywords/lsi/:keyword')
getLSIKeywords(
@Param('keyword') keyword: string,
@Query('count') count?: string,
) {
return this.seoService.getLSIKeywords(keyword, count ? parseInt(count, 10) : 10);
}
@Post('keywords/cluster')
clusterKeywords(@Body() body: { keywords: string[] }) {
return this.keywordService.clusterKeywords(body.keywords);
}
@Get('keywords/difficulty/:keyword')
analyzeKeywordDifficulty(@Param('keyword') keyword: string) {
return this.seoService.analyzeKeywordDifficulty(keyword);
}
@Post('keywords/cannibalization')
checkCannibalization(
@Body() body: { newKeyword: string; existingKeywords: string[] },
) {
return this.seoService.checkCannibalization(body.newKeyword, body.existingKeywords);
}
// ========== CONTENT OPTIMIZATION ==========
@Post('optimize')
optimizeContent(
@Body() body: {
content: string;
targetKeyword: string;
title?: string;
metaDescription?: string;
url?: string;
},
) {
const score = this.optimizationService.analyze(body.content, {
targetKeyword: body.targetKeyword,
title: body.title,
metaDescription: body.metaDescription,
url: body.url,
});
const optimized = this.optimizationService.optimize(body.content, body.targetKeyword);
return { score, optimized };
}
@Post('meta/generate')
generateMeta(
@Body() body: { content: string; targetKeyword: string; brandName?: string },
) {
return this.optimizationService.generateMeta(body.content, body.targetKeyword, {
brandName: body.brandName,
});
}
@Get('titles/:keyword')
generateTitles(
@Param('keyword') keyword: string,
@Query('count') count?: string,
) {
return this.optimizationService.generateTitleVariations(
keyword,
count ? parseInt(count, 10) : 5,
);
}
@Post('descriptions')
generateDescriptions(
@Body() body: { keyword: string; content: string; count?: number },
) {
return this.optimizationService.generateDescriptionVariations(
body.keyword,
body.content,
body.count || 3,
);
}
// ========== COMPETITORS ==========
@Get('competitors/:domain')
analyzeCompetitor(@Param('domain') domain: string) {
return this.competitorService.analyzeCompetitor(domain);
}
@Post('competitors/gaps')
findContentGaps(
@Body() body: { yourKeywords: string[]; competitorDomains: string[] },
) {
return this.competitorService.findContentGaps(body.yourKeywords, body.competitorDomains);
}
@Get('competitors/content/:keyword')
analyzeTopContent(
@Param('keyword') keyword: string,
@Query('count') count?: string,
) {
return this.competitorService.analyzeTopContent(keyword, count ? parseInt(count, 10) : 10);
}
@Post('competitors/blueprint')
generateBlueprint(
@Body() body: { keyword: string; competitorDomains: string[] },
) {
return this.competitorService.analyzeTopContent(body.keyword, 5).then((insights) =>
this.competitorService.generateContentBlueprint(body.keyword, insights),
);
}
@Post('competitors/patterns')
getPublishingPatterns(@Body() body: { domains: string[] }) {
return this.competitorService.getPublishingPatterns(body.domains);
}
// ========== OUTLINE GENERATION ==========
@Post('outline')
generateOutline(
@Body() body: {
keyword: string;
competitorDomains?: string[];
contentType?: string;
},
) {
return this.seoService.generateOutline(body.keyword, {
competitorDomains: body.competitorDomains,
contentType: body.contentType,
});
}
}

View File

@@ -0,0 +1,21 @@
// SEO Intelligence Module - Keyword research and content optimization
// Path: src/modules/seo/seo.module.ts
import { Module } from '@nestjs/common';
import { SeoService } from './seo.service';
import { SeoController } from './seo.controller';
import { KeywordResearchService } from './services/keyword-research.service';
import { ContentOptimizationService } from './services/content-optimization.service';
import { CompetitorAnalysisService } from './services/competitor-analysis.service';
@Module({
providers: [
SeoService,
KeywordResearchService,
ContentOptimizationService,
CompetitorAnalysisService,
],
controllers: [SeoController],
exports: [SeoService],
})
export class SeoModule { }

View File

@@ -0,0 +1,190 @@
// SEO Service - Main orchestration service
// Path: src/modules/seo/seo.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { KeywordResearchService, Keyword, KeywordCluster } from './services/keyword-research.service';
import { ContentOptimizationService, SeoScore, OptimizedMeta } from './services/content-optimization.service';
import { CompetitorAnalysisService, ContentGap, CompetitorProfile } from './services/competitor-analysis.service';
export interface FullSeoAnalysis {
content: {
score: SeoScore;
meta: OptimizedMeta;
};
keywords: {
main: Keyword;
related: Keyword[];
clusters: KeywordCluster[];
longTail: ReturnType<KeywordResearchService['generateLongTail']>;
};
competitors: {
gaps: ContentGap[];
};
}
@Injectable()
export class SeoService {
private readonly logger = new Logger(SeoService.name);
constructor(
private readonly keywordService: KeywordResearchService,
private readonly optimizationService: ContentOptimizationService,
private readonly competitorService: CompetitorAnalysisService,
) { }
/**
* Full SEO analysis for content
*/
async analyzeFull(
content: string,
targetKeyword: string,
options?: {
title?: string;
metaDescription?: string;
url?: string;
competitorDomains?: string[];
},
): Promise<FullSeoAnalysis> {
// Analyze content
const score = this.optimizationService.analyze(content, {
targetKeyword,
title: options?.title,
metaDescription: options?.metaDescription,
url: options?.url,
});
// Generate optimized meta
const meta = this.optimizationService.generateMeta(content, targetKeyword);
// Research keywords
const keywordData = await this.keywordService.suggestKeywords(targetKeyword, {
count: 20,
includeQuestions: true,
includeLongTail: true,
});
// Cluster keywords
const allKeywords = [targetKeyword, ...keywordData.related.map((k) => k.term)];
const clusters = this.keywordService.clusterKeywords(allKeywords);
// Long-tail variations
const longTail = this.keywordService.generateLongTail(targetKeyword, 15);
// Content gaps (if competitors provided)
let gaps: ContentGap[] = [];
if (options?.competitorDomains?.length) {
gaps = await this.competitorService.findContentGaps(
allKeywords,
options.competitorDomains,
);
}
return {
content: { score, meta },
keywords: {
main: keywordData.main,
related: keywordData.related,
clusters,
longTail,
},
competitors: { gaps },
};
}
/**
* Quick SEO score check
*/
quickScore(content: string, targetKeyword?: string): number {
const analysis = this.optimizationService.analyze(content, { targetKeyword });
return analysis.overall;
}
/**
* Generate SEO-optimized content outline
*/
async generateOutline(
keyword: string,
options?: {
competitorDomains?: string[];
contentType?: string;
},
): Promise<{
title: string;
description: string;
headings: string[];
keywords: string[];
estimatedWordCount: number;
differentiators: string[];
}> {
// Get keyword data
const keywordData = await this.keywordService.suggestKeywords(keyword);
// Get title variations
const titles = this.optimizationService.generateTitleVariations(keyword, 1);
const descriptions = this.optimizationService.generateDescriptionVariations(keyword, '', 1);
// Generate outline if competitors provided
let headings: string[] = [];
let differentiators: string[] = [];
if (options?.competitorDomains?.length) {
const competitorContent = await this.competitorService.analyzeTopContent(keyword, 5);
const blueprint = this.competitorService.generateContentBlueprint(keyword, competitorContent);
headings = blueprint.suggestedHeadings;
differentiators = blueprint.differentiators;
} else {
headings = [
`What is ${keyword}?`,
`Why ${keyword} is Important`,
`How to Use ${keyword}`,
`${keyword} Best Practices`,
`Common ${keyword} Mistakes`,
`${keyword} Tools & Resources`,
`FAQs about ${keyword}`,
];
differentiators = [
'Include original research or data',
'Add expert insights',
'Provide actionable steps',
'Include real examples',
];
}
return {
title: titles[0] || `Complete Guide to ${keyword}`,
description: descriptions[0] || `Learn everything about ${keyword} in this comprehensive guide.`,
headings,
keywords: [keyword, ...keywordData.related.slice(0, 5).map((k) => k.term)],
estimatedWordCount: 1500,
differentiators,
};
}
/**
* Get LSI keywords for semantic SEO
*/
getLSIKeywords(keyword: string, count: number = 10): string[] {
return this.keywordService.generateLSIKeywords(keyword, count);
}
/**
* Analyze keyword difficulty
*/
analyzeKeywordDifficulty(keyword: string) {
return this.keywordService.analyzeKeywordDifficulty(keyword);
}
/**
* Check for keyword cannibalization
*/
checkCannibalization(newKeyword: string, existingKeywords: string[]) {
return this.optimizationService.checkCannibalization(newKeyword, existingKeywords);
}
/**
* Get competitor insights
*/
async getCompetitorInsights(domain: string): Promise<CompetitorProfile> {
return this.competitorService.analyzeCompetitor(domain);
}
}

View File

@@ -0,0 +1,264 @@
// Competitor Analysis Service - Analyze competitor content for SEO insights
// Path: src/modules/seo/services/competitor-analysis.service.ts
import { Injectable, Logger } from '@nestjs/common';
export interface CompetitorProfile {
url: string;
domain: string;
estimatedAuthority: number; // 0-100
contentFrequency: 'daily' | 'weekly' | 'monthly' | 'rarely';
topKeywords: string[];
contentTypes: string[];
avgContentLength: number;
socialPresence: {
twitter?: string;
linkedin?: string;
instagram?: string;
youtube?: string;
};
}
export interface ContentGap {
keyword: string;
competitorsCovering: number;
difficulty: number;
opportunity: 'high' | 'medium' | 'low';
suggestedContentType: string;
}
export interface CompetitorContentInsight {
title: string;
url: string;
estimatedTraffic: number;
keywords: string[];
contentLength: number;
backlinks: number;
publishedDate?: string;
strengths: string[];
weaknesses: string[];
}
@Injectable()
export class CompetitorAnalysisService {
private readonly logger = new Logger(CompetitorAnalysisService.name);
/**
* Analyze competitor for SEO insights
*/
async analyzeCompetitor(domain: string): Promise<CompetitorProfile> {
// In production, this would scrape/analyze the competitor
// For now, return mock data structure
return {
url: `https://${domain}`,
domain,
estimatedAuthority: Math.floor(Math.random() * 50 + 30),
contentFrequency: 'weekly',
topKeywords: [
`${domain} tips`,
`best ${domain} practices`,
`${domain} guide`,
`how to use ${domain}`,
`${domain} alternatives`,
],
contentTypes: ['blog', 'guides', 'case-studies', 'videos'],
avgContentLength: Math.floor(Math.random() * 1000 + 1500),
socialPresence: {
twitter: `@${domain.split('.')[0]}`,
linkedin: domain.split('.')[0],
},
};
}
/**
* Identify content gaps between you and competitors
*/
async findContentGaps(
yourKeywords: string[],
competitorDomains: string[],
): Promise<ContentGap[]> {
const gaps: ContentGap[] = [];
// Mock gap analysis
const potentialGaps = [
'beginner guide',
'advanced strategies',
'case studies',
'tool comparison',
'industry trends',
'best practices 2024',
'common mistakes',
'step by step tutorial',
'video walkthrough',
'templates',
];
for (const gap of potentialGaps) {
const covering = Math.floor(Math.random() * competitorDomains.length);
if (!yourKeywords.some((k) => k.toLowerCase().includes(gap))) {
gaps.push({
keyword: gap,
competitorsCovering: covering,
difficulty: Math.floor(Math.random() * 60 + 20),
opportunity: covering >= 2 ? 'high' : covering === 1 ? 'medium' : 'low',
suggestedContentType: this.suggestContentType(gap),
});
}
}
return gaps.sort((a, b) => {
const priorityMap = { high: 3, medium: 2, low: 1 };
return priorityMap[b.opportunity] - priorityMap[a.opportunity];
});
}
/**
* Analyze top competitor content for a keyword
*/
async analyzeTopContent(
keyword: string,
count: number = 10,
): Promise<CompetitorContentInsight[]> {
// Mock competitor content analysis
const insights: CompetitorContentInsight[] = [];
for (let i = 0; i < count; i++) {
insights.push({
title: `${keyword.charAt(0).toUpperCase() + keyword.slice(1)} - Complete Guide ${i + 1}`,
url: `https://example${i + 1}.com/${keyword.replace(/\s+/g, '-')}`,
estimatedTraffic: Math.floor(Math.random() * 50000 + 5000),
keywords: [
keyword,
`${keyword} tips`,
`best ${keyword}`,
`${keyword} examples`,
],
contentLength: Math.floor(Math.random() * 2000 + 1000),
backlinks: Math.floor(Math.random() * 500 + 50),
strengths: this.identifyStrengths(i),
weaknesses: this.identifyWeaknesses(i),
});
}
return insights.sort((a, b) => b.estimatedTraffic - a.estimatedTraffic);
}
/**
* Generate content blueprint based on competitor analysis
*/
generateContentBlueprint(
keyword: string,
competitorInsights: CompetitorContentInsight[],
): {
recommendedLength: number;
suggestedHeadings: string[];
keyPoints: string[];
differentiators: string[];
callToActions: string[];
} {
const avgLength = competitorInsights.reduce((sum, c) => sum + c.contentLength, 0) / competitorInsights.length;
return {
recommendedLength: Math.floor(avgLength * 1.2), // 20% longer than competitors
suggestedHeadings: [
`What is ${keyword}?`,
`Why ${keyword} Matters`,
`How to Get Started with ${keyword}`,
`${keyword} Best Practices`,
`Common ${keyword} Mistakes to Avoid`,
`${keyword} Tools and Resources`,
`${keyword} Case Studies`,
`Frequently Asked Questions about ${keyword}`,
],
keyPoints: [
'Comprehensive coverage of all subtopics',
'Include original data or research',
'Add expert quotes or interviews',
'Provide actionable steps',
'Include visual elements (images, infographics)',
],
differentiators: [
'Include original case studies',
'Add interactive elements or tools',
'Provide downloadable resources',
'Include video content',
'Offer a unique angle or perspective',
],
callToActions: [
`Download our free ${keyword} checklist`,
`Try our ${keyword} tool free`,
`Subscribe for more ${keyword} tips`,
`Get a personalized ${keyword} strategy`,
],
};
}
/**
* Track competitor content publishing
*/
getPublishingPatterns(
competitorDomains: string[],
): {
domain: string;
avgPostsPerWeek: number;
bestPublishingDay: string;
topContentTypes: string[];
engagementLevel: 'low' | 'medium' | 'high';
}[] {
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
return competitorDomains.map((domain) => ({
domain,
avgPostsPerWeek: Math.floor(Math.random() * 5 + 1),
bestPublishingDay: days[Math.floor(Math.random() * days.length)],
topContentTypes: ['blog', 'guides', 'case-studies'].slice(0, Math.floor(Math.random() * 3 + 1)),
engagementLevel: ['low', 'medium', 'high'][Math.floor(Math.random() * 3)] as 'low' | 'medium' | 'high',
}));
}
// Private helper methods
private suggestContentType(gap: string): string {
if (gap.includes('guide') || gap.includes('tutorial')) return 'long-form guide';
if (gap.includes('video')) return 'video content';
if (gap.includes('case')) return 'case study';
if (gap.includes('comparison') || gap.includes('vs')) return 'comparison article';
if (gap.includes('template')) return 'template/resource';
return 'blog post';
}
private identifyStrengths(index: number): string[] {
const allStrengths = [
'Comprehensive coverage',
'Well-structured headings',
'Good use of visuals',
'Strong call-to-actions',
'Includes original data',
'Expert quotes included',
'Mobile-optimized',
'Fast page load',
'Interactive elements',
'Downloadable resources',
];
return allStrengths.slice(index % 3, (index % 3) + 3);
}
private identifyWeaknesses(index: number): string[] {
const allWeaknesses = [
'Thin content',
'Outdated information',
'No visual elements',
'Poor readability',
'Missing key subtopics',
'Slow page load',
'No internal links',
'Weak call-to-actions',
'No social proof',
'Generic content',
];
return allWeaknesses.slice(index % 3, (index % 3) + 2);
}
}

View File

@@ -0,0 +1,454 @@
// Content Optimization Service - SEO scoring and optimization
// Path: src/modules/seo/services/content-optimization.service.ts
import { Injectable, Logger } from '@nestjs/common';
export interface SeoScore {
overall: number; // 0-100
breakdown: {
titleOptimization: number;
metaDescription: number;
headingStructure: number;
keywordDensity: number;
contentLength: number;
readability: number;
internalLinks: number;
externalLinks: number;
imageOptimization: number;
urlStructure: number;
};
issues: SeoIssue[];
suggestions: SeoSuggestion[];
}
export interface SeoIssue {
type: 'error' | 'warning' | 'info';
category: string;
message: string;
fix: string;
}
export interface SeoSuggestion {
priority: 'high' | 'medium' | 'low';
category: string;
suggestion: string;
impact: string;
}
export interface OptimizedMeta {
title: string;
description: string;
keywords: string[];
ogTitle: string;
ogDescription: string;
twitterTitle: string;
twitterDescription: string;
}
@Injectable()
export class ContentOptimizationService {
private readonly logger = new Logger(ContentOptimizationService.name);
// Optimal content parameters
private readonly optimalParams = {
titleLength: { min: 50, max: 60 },
metaDescLength: { min: 150, max: 160 },
contentWords: { min: 1000, max: 2500 },
keywordDensity: { min: 1, max: 3 }, // percentage
paragraphLength: { max: 150 }, // words
sentenceLength: { max: 20 }, // words
h1Count: 1,
h2MinCount: 2,
imageAltMinPercent: 100,
};
/**
* Analyze content for SEO optimization
*/
analyze(content: string, options?: {
targetKeyword?: string;
title?: string;
metaDescription?: string;
url?: string;
}): SeoScore {
const breakdown = {
titleOptimization: this.scoreTitleOptimization(options?.title, options?.targetKeyword),
metaDescription: this.scoreMetaDescription(options?.metaDescription, options?.targetKeyword),
headingStructure: this.scoreHeadingStructure(content),
keywordDensity: this.scoreKeywordDensity(content, options?.targetKeyword),
contentLength: this.scoreContentLength(content),
readability: this.scoreReadability(content),
internalLinks: this.scoreInternalLinks(content),
externalLinks: this.scoreExternalLinks(content),
imageOptimization: this.scoreImageOptimization(content),
urlStructure: this.scoreUrlStructure(options?.url),
};
const overall = Math.round(
Object.values(breakdown).reduce((sum, score) => sum + score, 0) / 10
);
const issues = this.identifyIssues(content, options);
const suggestions = this.generateSuggestions(breakdown, options);
return { overall, breakdown, issues, suggestions };
}
/**
* Generate optimized meta tags
*/
generateMeta(
content: string,
targetKeyword: string,
options?: { brandName?: string }
): OptimizedMeta {
const firstParagraph = content.split('\n\n')[0] || content.substring(0, 200);
const brand = options?.brandName || '';
// Generate title variations
const title = this.generateTitle(targetKeyword, brand);
const description = this.generateDescription(firstParagraph, targetKeyword);
const keywords = this.extractKeywords(content, targetKeyword);
return {
title,
description,
keywords,
ogTitle: title,
ogDescription: description,
twitterTitle: this.truncate(title, 70),
twitterDescription: this.truncate(description, 200),
};
}
/**
* Optimize content for target keyword
*/
optimize(
content: string,
targetKeyword: string,
): {
optimizedContent: string;
changes: { type: string; before: string; after: string }[];
newScore: number;
} {
let optimizedContent = content;
const changes: { type: string; before: string; after: string }[] = [];
// Check keyword in first paragraph
const firstPara = content.split('\n\n')[0];
if (firstPara && !firstPara.toLowerCase().includes(targetKeyword.toLowerCase())) {
const newFirstPara = `${targetKeyword} is essential. ${firstPara}`;
optimizedContent = optimizedContent.replace(firstPara, newFirstPara);
changes.push({
type: 'keyword_placement',
before: firstPara.substring(0, 50),
after: newFirstPara.substring(0, 50),
});
}
// Calculate new score
const newScore = this.analyze(optimizedContent, { targetKeyword }).overall;
return { optimizedContent, changes, newScore };
}
/**
* Generate SEO-optimized title variations
*/
generateTitleVariations(keyword: string, count: number = 5): string[] {
const templates = [
`${keyword}: The Complete Guide for 2024`,
`How to Master ${keyword} in 7 Steps`,
`${keyword} 101: Everything You Need to Know`,
`The Ultimate ${keyword} Guide (Updated 2024)`,
`${keyword}: Tips, Tricks & Best Practices`,
`Why ${keyword} Matters + How to Get Started`,
`${keyword} Secrets: What Experts Don't Tell You`,
`${keyword} Made Simple: A Beginner's Guide`,
`10 ${keyword} Strategies That Actually Work`,
`The Science Behind Effective ${keyword}`,
];
return templates.slice(0, count).map((t) => this.truncate(t, 60));
}
/**
* Generate meta description variations
*/
generateDescriptionVariations(
keyword: string,
content: string,
count: number = 3,
): string[] {
const templates = [
`Discover how ${keyword} can transform your approach. Learn proven strategies, tips, and best practices in this comprehensive guide.`,
`Looking to improve your ${keyword}? This guide covers everything from basics to advanced techniques. Start mastering ${keyword} today.`,
`${keyword} doesn't have to be complicated. Our step-by-step guide breaks down everything you need to know for success.`,
];
return templates.slice(0, count).map((t) => this.truncate(t, 160));
}
/**
* Check content for keyword cannibalization risk
*/
checkCannibalization(
newKeyword: string,
existingKeywords: string[],
): { risk: 'low' | 'medium' | 'high'; conflictingKeywords: string[] } {
const conflicts: string[] = [];
const newWords = new Set(newKeyword.toLowerCase().split(' '));
for (const existing of existingKeywords) {
const existingWords = new Set(existing.toLowerCase().split(' '));
const overlap = [...newWords].filter((w) => existingWords.has(w));
if (overlap.length / newWords.size > 0.5) {
conflicts.push(existing);
}
}
let risk: 'low' | 'medium' | 'high' = 'low';
if (conflicts.length > 2) risk = 'high';
else if (conflicts.length > 0) risk = 'medium';
return { risk, conflictingKeywords: conflicts };
}
// Private scoring methods
private scoreTitleOptimization(title?: string, keyword?: string): number {
if (!title) return 0;
let score = 50;
const length = title.length;
if (length >= this.optimalParams.titleLength.min &&
length <= this.optimalParams.titleLength.max) {
score += 25;
} else if (length < this.optimalParams.titleLength.min) {
score += 10;
}
if (keyword && title.toLowerCase().includes(keyword.toLowerCase())) {
score += 25;
}
return Math.min(score, 100);
}
private scoreMetaDescription(description?: string, keyword?: string): number {
if (!description) return 0;
let score = 50;
const length = description.length;
if (length >= this.optimalParams.metaDescLength.min &&
length <= this.optimalParams.metaDescLength.max) {
score += 25;
}
if (keyword && description.toLowerCase().includes(keyword.toLowerCase())) {
score += 25;
}
return Math.min(score, 100);
}
private scoreHeadingStructure(content: string): number {
let score = 50;
const h1Count = (content.match(/^#\s/gm) || []).length;
const h2Count = (content.match(/^##\s/gm) || []).length;
if (h1Count === 1) score += 25;
if (h2Count >= 2) score += 25;
return Math.min(score, 100);
}
private scoreKeywordDensity(content: string, keyword?: string): number {
if (!keyword) return 50;
const words = content.split(/\s+/).length;
const kwCount = (content.toLowerCase().match(new RegExp(keyword.toLowerCase(), 'g')) || []).length;
const density = (kwCount / words) * 100;
if (density >= this.optimalParams.keywordDensity.min &&
density <= this.optimalParams.keywordDensity.max) {
return 100;
}
if (density < this.optimalParams.keywordDensity.min) {
return 40;
}
return 60; // Over-optimized
}
private scoreContentLength(content: string): number {
const words = content.split(/\s+/).length;
if (words >= this.optimalParams.contentWords.min &&
words <= this.optimalParams.contentWords.max) {
return 100;
}
if (words < this.optimalParams.contentWords.min) {
return Math.floor((words / this.optimalParams.contentWords.min) * 80);
}
return 80; // Very long content
}
private scoreReadability(content: string): number {
const sentences = content.split(/[.!?]+/).filter(Boolean);
const avgLength = sentences.reduce((sum, s) => sum + s.split(/\s+/).length, 0) / sentences.length;
if (avgLength <= this.optimalParams.sentenceLength.max) {
return 100;
}
return Math.max(50, 100 - (avgLength - this.optimalParams.sentenceLength.max) * 3);
}
private scoreInternalLinks(content: string): number {
const internalLinks = (content.match(/\[.*?\]\(\/.*?\)/g) || []).length;
if (internalLinks >= 3) return 100;
if (internalLinks >= 1) return 70;
return 30;
}
private scoreExternalLinks(content: string): number {
const externalLinks = (content.match(/\[.*?\]\(https?:\/\/.*?\)/g) || []).length;
if (externalLinks >= 2) return 100;
if (externalLinks >= 1) return 70;
return 50;
}
private scoreImageOptimization(content: string): number {
const images = content.match(/!\[.*?\]\(.*?\)/g) || [];
if (images.length === 0) return 50;
const withAlt = images.filter((img) => !/!\[\]/.test(img)).length;
return Math.floor((withAlt / images.length) * 100);
}
private scoreUrlStructure(url?: string): number {
if (!url) return 50;
let score = 50;
if (url.length < 75) score += 20;
if (!url.includes('?')) score += 15;
if (url === url.toLowerCase()) score += 15;
return Math.min(score, 100);
}
private identifyIssues(content: string, options?: {
targetKeyword?: string;
title?: string;
metaDescription?: string;
}): SeoIssue[] {
const issues: SeoIssue[] = [];
if (!options?.title) {
issues.push({
type: 'error',
category: 'title',
message: 'Missing title tag',
fix: 'Add a compelling title with your target keyword',
});
}
if (!options?.metaDescription) {
issues.push({
type: 'error',
category: 'meta',
message: 'Missing meta description',
fix: 'Add a meta description between 150-160 characters',
});
}
const words = content.split(/\s+/).length;
if (words < 300) {
issues.push({
type: 'warning',
category: 'content',
message: 'Content is too short',
fix: 'Aim for at least 1000 words for better SEO',
});
}
return issues;
}
private generateSuggestions(
breakdown: SeoScore['breakdown'],
options?: { targetKeyword?: string },
): SeoSuggestion[] {
const suggestions: SeoSuggestion[] = [];
if (breakdown.keywordDensity < 70 && options?.targetKeyword) {
suggestions.push({
priority: 'high',
category: 'keywords',
suggestion: `Increase usage of "${options.targetKeyword}" in your content`,
impact: 'Better keyword relevance signals to search engines',
});
}
if (breakdown.contentLength < 70) {
suggestions.push({
priority: 'medium',
category: 'content',
suggestion: 'Expand your content with more detailed information',
impact: 'Longer, comprehensive content typically ranks better',
});
}
if (breakdown.internalLinks < 70) {
suggestions.push({
priority: 'medium',
category: 'links',
suggestion: 'Add more internal links to related content',
impact: 'Improves site structure and helps with crawling',
});
}
return suggestions;
}
private generateTitle(keyword: string, brand: string): string {
const title = `${keyword}: Complete Guide & Best Practices${brand ? ` | ${brand}` : ''}`;
return this.truncate(title, 60);
}
private generateDescription(firstParagraph: string, keyword: string): string {
let desc = firstParagraph.substring(0, 150);
if (!desc.toLowerCase().includes(keyword.toLowerCase())) {
desc = `${keyword} - ${desc}`;
}
return this.truncate(desc, 160);
}
private extractKeywords(content: string, targetKeyword: string): string[] {
const words = content.toLowerCase().split(/\s+/);
const frequency: Map<string, number> = new Map();
const stopWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'was', 'were', 'to', 'of', 'in', 'for', 'on', 'with']);
for (const word of words) {
const clean = word.replace(/[^a-z]/g, '');
if (clean.length > 3 && !stopWords.has(clean)) {
frequency.set(clean, (frequency.get(clean) || 0) + 1);
}
}
const sorted = [...frequency.entries()].sort((a, b) => b[1] - a[1]);
const keywords = sorted.slice(0, 10).map(([word]) => word);
if (!keywords.includes(targetKeyword.toLowerCase())) {
keywords.unshift(targetKeyword.toLowerCase());
}
return keywords;
}
private truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
}
}

View File

@@ -0,0 +1,379 @@
// Keyword Research Service - Keyword discovery and analysis
// Path: src/modules/seo/services/keyword-research.service.ts
import { Injectable, Logger } from '@nestjs/common';
export interface Keyword {
term: string;
volume: number; // Monthly search volume (estimated)
difficulty: number; // 0-100
cpc: number; // Cost per click estimate
trend: 'rising' | 'stable' | 'declining';
intent: 'informational' | 'navigational' | 'transactional' | 'commercial';
related: string[];
questions: string[];
}
export interface KeywordCluster {
mainKeyword: string;
keywords: Keyword[];
totalVolume: number;
avgDifficulty: number;
contentSuggestions: string[];
}
export interface LongTailSuggestion {
keyword: string;
baseKeyword: string;
modifier: string;
estimatedVolume: number;
competitionLevel: 'low' | 'medium' | 'high';
}
@Injectable()
export class KeywordResearchService {
private readonly logger = new Logger(KeywordResearchService.name);
// Common keyword modifiers for long-tail generation
private readonly modifiers = {
prefix: [
'how to', 'what is', 'why', 'when to', 'where to', 'who uses',
'best', 'top', 'free', 'cheap', 'affordable', 'premium',
'easy', 'quick', 'simple', 'ultimate', 'complete', 'beginner',
'advanced', 'professional', 'expert', 'step by step',
],
suffix: [
'guide', 'tutorial', 'tips', 'tricks', 'examples', 'templates',
'tools', 'software', 'apps', 'services', 'strategies', 'techniques',
'for beginners', 'for experts', 'in 2024', 'vs', 'alternatives',
'review', 'comparison', 'checklist', 'resources', 'ideas',
],
questions: [
'what is', 'how to', 'why should', 'when to use', 'where can I find',
'who needs', 'which is best', 'can you', 'should I', 'does',
'is it worth', 'how much does', 'how long does', 'what are the benefits',
],
};
// Content type keywords
private readonly contentTypeKeywords = {
blog: ['guide', 'how to', 'tips', 'best practices', 'examples'],
video: ['tutorial', 'walkthrough', 'demo', 'explained', 'in action'],
product: ['buy', 'price', 'discount', 'coupon', 'review'],
local: ['near me', 'in [city]', 'local', 'best [city]'],
comparison: ['vs', 'comparison', 'alternatives', 'versus', 'or'],
};
/**
* Generate keyword suggestions for a topic
*/
async suggestKeywords(topic: string, options?: {
count?: number;
includeQuestions?: boolean;
includeLongTail?: boolean;
}): Promise<{
main: Keyword;
related: Keyword[];
questions: string[];
longTail: LongTailSuggestion[];
}> {
const count = options?.count || 20;
// Generate main keyword data
const main: Keyword = {
term: topic.toLowerCase(),
volume: this.estimateVolume(topic),
difficulty: this.estimateDifficulty(topic),
cpc: this.estimateCPC(topic),
trend: 'stable',
intent: this.detectIntent(topic),
related: this.generateRelatedTerms(topic, 5),
questions: this.generateQuestions(topic, 5),
};
// Generate related keywords
const related = this.generateRelatedKeywords(topic, count);
// Generate question-based keywords
const questions = options?.includeQuestions !== false
? this.generateQuestions(topic, 10)
: [];
// Generate long-tail variations
const longTail = options?.includeLongTail !== false
? this.generateLongTail(topic, 15)
: [];
return { main, related, questions, longTail };
}
/**
* Generate long-tail keyword variations
*/
generateLongTail(baseKeyword: string, count: number = 20): LongTailSuggestion[] {
const suggestions: LongTailSuggestion[] = [];
const base = baseKeyword.toLowerCase();
// Add prefix modifiers
for (const prefix of this.modifiers.prefix.slice(0, Math.ceil(count / 3))) {
suggestions.push({
keyword: `${prefix} ${base}`,
baseKeyword: base,
modifier: prefix,
estimatedVolume: Math.floor(this.estimateVolume(base) * 0.1 * Math.random()),
competitionLevel: this.estimateCompetition(),
});
}
// Add suffix modifiers
for (const suffix of this.modifiers.suffix.slice(0, Math.ceil(count / 3))) {
suggestions.push({
keyword: `${base} ${suffix}`,
baseKeyword: base,
modifier: suffix,
estimatedVolume: Math.floor(this.estimateVolume(base) * 0.1 * Math.random()),
competitionLevel: this.estimateCompetition(),
});
}
// Add question modifiers
for (const question of this.modifiers.questions.slice(0, Math.ceil(count / 3))) {
suggestions.push({
keyword: `${question} ${base}`,
baseKeyword: base,
modifier: question,
estimatedVolume: Math.floor(this.estimateVolume(base) * 0.15 * Math.random()),
competitionLevel: 'low',
});
}
return suggestions.slice(0, count);
}
/**
* Cluster keywords by topic similarity
*/
clusterKeywords(keywords: string[]): KeywordCluster[] {
// Group similar keywords into clusters
const clusters: Map<string, Keyword[]> = new Map();
for (const kw of keywords) {
const mainWord = this.extractMainWord(kw);
const existing = clusters.get(mainWord) || [];
existing.push({
term: kw,
volume: this.estimateVolume(kw),
difficulty: this.estimateDifficulty(kw),
cpc: this.estimateCPC(kw),
trend: 'stable',
intent: this.detectIntent(kw),
related: [],
questions: [],
});
clusters.set(mainWord, existing);
}
return Array.from(clusters.entries()).map(([mainKeyword, kws]) => ({
mainKeyword,
keywords: kws,
totalVolume: kws.reduce((sum, k) => sum + k.volume, 0),
avgDifficulty: kws.reduce((sum, k) => sum + k.difficulty, 0) / kws.length,
contentSuggestions: this.generateContentSuggestions(mainKeyword),
}));
}
/**
* Generate LSI (Latent Semantic Indexing) keywords
*/
generateLSIKeywords(keyword: string, count: number = 10): string[] {
const words = keyword.toLowerCase().split(' ');
const lsiTerms: string[] = [];
// Add synonyms and related concepts
const synonymMap: Record<string, string[]> = {
content: ['posts', 'articles', 'material', 'copy', 'text'],
marketing: ['promotion', 'advertising', 'branding', 'outreach'],
social: ['community', 'network', 'platform', 'engagement'],
media: ['content', 'posts', 'videos', 'images', 'stories'],
strategy: ['plan', 'approach', 'tactics', 'framework', 'method'],
growth: ['scaling', 'expansion', 'increase', 'improvement'],
engagement: ['interaction', 'response', 'participation', 'activity'],
audience: ['followers', 'community', 'readers', 'subscribers'],
};
for (const word of words) {
const synonyms = synonymMap[word] || [];
lsiTerms.push(...synonyms);
}
// Add related terms
lsiTerms.push(...this.generateRelatedTerms(keyword, 5));
return [...new Set(lsiTerms)].slice(0, count);
}
/**
* Analyze keyword difficulty
*/
analyzeKeywordDifficulty(keyword: string): {
score: number;
factors: { name: string; impact: 'positive' | 'negative'; description: string }[];
recommendation: string;
} {
const difficulty = this.estimateDifficulty(keyword);
const factors: { name: string; impact: 'positive' | 'negative'; description: string }[] = [];
// Word count factor
const wordCount = keyword.split(' ').length;
if (wordCount >= 4) {
factors.push({
name: 'Long-tail keyword',
impact: 'positive',
description: 'Longer keywords typically have lower competition',
});
} else if (wordCount === 1) {
factors.push({
name: 'Single word',
impact: 'negative',
description: 'Single-word keywords are highly competitive',
});
}
// Question format
if (this.modifiers.questions.some((q) => keyword.toLowerCase().startsWith(q))) {
factors.push({
name: 'Question format',
impact: 'positive',
description: 'Question-based keywords often have lower competition',
});
}
// Commercial intent
const commercialTerms = ['buy', 'price', 'discount', 'deal', 'sale'];
if (commercialTerms.some((t) => keyword.toLowerCase().includes(t))) {
factors.push({
name: 'Commercial intent',
impact: 'negative',
description: 'Commercial keywords have higher competition',
});
}
let recommendation: string;
if (difficulty < 30) {
recommendation = 'Great opportunity! This keyword has low competition.';
} else if (difficulty < 60) {
recommendation = 'Moderate competition. Focus on high-quality content.';
} else {
recommendation = 'High competition. Consider targeting long-tail variations.';
}
return { score: difficulty, factors, recommendation };
}
// Private helper methods
private generateRelatedKeywords(topic: string, count: number): Keyword[] {
const related = this.generateRelatedTerms(topic, count);
return related.map((term) => ({
term,
volume: this.estimateVolume(term),
difficulty: this.estimateDifficulty(term),
cpc: this.estimateCPC(term),
trend: 'stable' as const,
intent: this.detectIntent(term),
related: [],
questions: [],
}));
}
private generateRelatedTerms(topic: string, count: number): string[] {
const base = topic.toLowerCase();
const related: string[] = [];
// Add common variations
related.push(`${base} tips`);
related.push(`${base} guide`);
related.push(`best ${base}`);
related.push(`${base} strategies`);
related.push(`${base} examples`);
related.push(`${base} tools`);
related.push(`${base} for beginners`);
related.push(`advanced ${base}`);
related.push(`${base} best practices`);
related.push(`${base} trends`);
return related.slice(0, count);
}
private generateQuestions(topic: string, count: number): string[] {
const base = topic.toLowerCase();
return [
`What is ${base}?`,
`How to use ${base}?`,
`Why is ${base} important?`,
`When should I use ${base}?`,
`Where can I learn ${base}?`,
`Who needs ${base}?`,
`What are the benefits of ${base}?`,
`How does ${base} work?`,
`Is ${base} worth it?`,
`What are ${base} best practices?`,
].slice(0, count);
}
private generateContentSuggestions(keyword: string): string[] {
return [
`Ultimate Guide to ${keyword}`,
`${keyword}: Everything You Need to Know`,
`10 ${keyword} Tips for Beginners`,
`How to Master ${keyword} in 2024`,
`${keyword} vs Alternatives: Complete Comparison`,
];
}
private estimateVolume(keyword: string): number {
// Simple estimation based on keyword characteristics
const wordCount = keyword.split(' ').length;
const baseVolume = 10000;
return Math.floor(baseVolume / Math.pow(wordCount, 1.5) * (Math.random() + 0.5));
}
private estimateDifficulty(keyword: string): number {
const wordCount = keyword.split(' ').length;
const base = 70 - (wordCount * 10);
return Math.min(95, Math.max(5, base + Math.floor(Math.random() * 20)));
}
private estimateCPC(keyword: string): number {
return Math.round((Math.random() * 5 + 0.5) * 100) / 100;
}
private estimateCompetition(): 'low' | 'medium' | 'high' {
const rand = Math.random();
if (rand < 0.4) return 'low';
if (rand < 0.7) return 'medium';
return 'high';
}
private detectIntent(keyword: string): 'informational' | 'navigational' | 'transactional' | 'commercial' {
const kw = keyword.toLowerCase();
if (['buy', 'price', 'discount', 'order', 'purchase'].some((t) => kw.includes(t))) {
return 'transactional';
}
if (['best', 'top', 'review', 'comparison', 'vs'].some((t) => kw.includes(t))) {
return 'commercial';
}
if (['login', 'sign in', 'website', 'official'].some((t) => kw.includes(t))) {
return 'navigational';
}
return 'informational';
}
private extractMainWord(keyword: string): string {
const stopWords = ['how', 'to', 'what', 'is', 'the', 'a', 'an', 'for', 'in', 'of', 'and', 'or'];
const words = keyword.toLowerCase().split(' ');
const mainWords = words.filter((w) => !stopWords.includes(w));
return mainWords[0] || words[0] || keyword;
}
}