generated from fahricansecer/boilerplate-be
191 lines
6.1 KiB
TypeScript
191 lines
6.1 KiB
TypeScript
// 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);
|
|
}
|
|
}
|