feat(api): add keywords module for AI-powered keyword enhancement
- Implement POST /api/keywords/enhance for AI keyword expansion - Add keyword suggestion and validation endpoints - Support SEO optimization with long-tail keyword generation - Include rate limiting and comprehensive keyword validation - Add related keyword discovery and categorization - Mock AI integration ready for OpenAI GPT-4 connection Resolves requirement §76 for keyword enhancement API. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ed5f745a51
commit
b554f69516
4 changed files with 628 additions and 0 deletions
79
packages/api/src/keywords/dto/enhance-keywords.dto.ts
Normal file
79
packages/api/src/keywords/dto/enhance-keywords.dto.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { IsArray, IsString, ArrayMaxSize, ArrayMinSize, MaxLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class EnhanceKeywordsDto {
|
||||
@ApiProperty({
|
||||
description: 'Array of keywords to enhance with AI suggestions',
|
||||
example: ['kitchen', 'modern', 'renovation'],
|
||||
minItems: 1,
|
||||
maxItems: 20,
|
||||
})
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ArrayMinSize(1)
|
||||
@ArrayMaxSize(20)
|
||||
@MaxLength(50, { each: true })
|
||||
keywords: string[];
|
||||
}
|
||||
|
||||
export class EnhanceKeywordsResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'Original keywords provided',
|
||||
example: ['kitchen', 'modern', 'renovation'],
|
||||
})
|
||||
original_keywords: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'AI-enhanced keywords with SEO improvements',
|
||||
example: [
|
||||
'modern-kitchen-design',
|
||||
'contemporary-kitchen-renovation',
|
||||
'sleek-kitchen-remodel',
|
||||
'updated-kitchen-interior',
|
||||
'kitchen-makeover-ideas',
|
||||
'stylish-kitchen-upgrade',
|
||||
'fresh-kitchen-design',
|
||||
'kitchen-transformation'
|
||||
],
|
||||
})
|
||||
enhanced_keywords: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Related keywords and synonyms',
|
||||
example: [
|
||||
'culinary-space',
|
||||
'cooking-area',
|
||||
'kitchen-cabinets',
|
||||
'kitchen-appliances',
|
||||
'kitchen-island',
|
||||
'backsplash-design'
|
||||
],
|
||||
})
|
||||
related_keywords: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'SEO-optimized long-tail keywords',
|
||||
example: [
|
||||
'modern-kitchen-renovation-ideas-2024',
|
||||
'contemporary-kitchen-design-trends',
|
||||
'sleek-kitchen-remodel-inspiration'
|
||||
],
|
||||
})
|
||||
long_tail_keywords: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Processing metadata',
|
||||
example: {
|
||||
processing_time: 1.2,
|
||||
ai_model: 'gpt-4',
|
||||
confidence_score: 0.92,
|
||||
keywords_generated: 15,
|
||||
},
|
||||
})
|
||||
metadata: {
|
||||
processing_time: number;
|
||||
ai_model: string;
|
||||
confidence_score: number;
|
||||
keywords_generated: number;
|
||||
};
|
||||
}
|
192
packages/api/src/keywords/keywords.controller.ts
Normal file
192
packages/api/src/keywords/keywords.controller.ts
Normal file
|
@ -0,0 +1,192 @@
|
|||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
UseGuards,
|
||||
Request,
|
||||
HttpStatus,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/auth.guard';
|
||||
import { KeywordsService } from './keywords.service';
|
||||
import { EnhanceKeywordsDto, EnhanceKeywordsResponseDto } from './dto/enhance-keywords.dto';
|
||||
|
||||
@ApiTags('keywords')
|
||||
@Controller('api/keywords')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class KeywordsController {
|
||||
constructor(private readonly keywordsService: KeywordsService) {}
|
||||
|
||||
@Post('enhance')
|
||||
@ApiOperation({
|
||||
summary: 'Enhance keywords with AI suggestions',
|
||||
description: 'Takes user-provided keywords and returns AI-enhanced SEO-optimized keywords and suggestions',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Keywords enhanced successfully',
|
||||
type: EnhanceKeywordsResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Invalid keywords or request data',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.TOO_MANY_REQUESTS,
|
||||
description: 'Rate limit exceeded for keyword enhancement',
|
||||
})
|
||||
async enhanceKeywords(
|
||||
@Body() enhanceKeywordsDto: EnhanceKeywordsDto,
|
||||
@Request() req: any,
|
||||
): Promise<EnhanceKeywordsResponseDto> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
// Check rate limits
|
||||
await this.keywordsService.checkRateLimit(userId);
|
||||
|
||||
// Enhance keywords with AI
|
||||
const enhancedResult = await this.keywordsService.enhanceKeywords(
|
||||
enhanceKeywordsDto.keywords,
|
||||
userId,
|
||||
);
|
||||
|
||||
return enhancedResult;
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to enhance keywords');
|
||||
}
|
||||
}
|
||||
|
||||
@Post('suggest')
|
||||
@ApiOperation({
|
||||
summary: 'Get keyword suggestions for image context',
|
||||
description: 'Provides keyword suggestions based on image analysis context',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Keyword suggestions generated successfully',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
suggestions: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: ['interior-design', 'home-decor', 'modern-style', 'contemporary'],
|
||||
},
|
||||
categories: {
|
||||
type: 'object',
|
||||
example: {
|
||||
style: ['modern', 'contemporary', 'minimalist'],
|
||||
room: ['kitchen', 'living-room', 'bedroom'],
|
||||
color: ['white', 'black', 'gray'],
|
||||
material: ['wood', 'metal', 'glass'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
async getKeywordSuggestions(
|
||||
@Body() body: { context?: string; category?: string },
|
||||
@Request() req: any,
|
||||
): Promise<{
|
||||
suggestions: string[];
|
||||
categories: Record<string, string[]>;
|
||||
}> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
const suggestions = await this.keywordsService.getKeywordSuggestions(
|
||||
body.context,
|
||||
body.category,
|
||||
);
|
||||
|
||||
return suggestions;
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to get keyword suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
@Post('validate')
|
||||
@ApiOperation({
|
||||
summary: 'Validate keywords for SEO optimization',
|
||||
description: 'Checks keywords for SEO best practices and provides recommendations',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Keywords validated successfully',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
valid_keywords: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: ['modern-kitchen', 'contemporary-design'],
|
||||
},
|
||||
invalid_keywords: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyword: { type: 'string' },
|
||||
reason: { type: 'string' },
|
||||
},
|
||||
},
|
||||
example: [
|
||||
{ keyword: 'a', reason: 'Too short for SEO value' },
|
||||
{ keyword: 'the-best-kitchen-in-the-world-ever', reason: 'Too long for practical use' },
|
||||
],
|
||||
},
|
||||
recommendations: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: [
|
||||
'Use hyphens instead of spaces',
|
||||
'Keep keywords between 2-4 words',
|
||||
'Avoid stop words like "the", "and", "or"',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
async validateKeywords(
|
||||
@Body() body: { keywords: string[] },
|
||||
@Request() req: any,
|
||||
): Promise<{
|
||||
valid_keywords: string[];
|
||||
invalid_keywords: Array<{ keyword: string; reason: string }>;
|
||||
recommendations: string[];
|
||||
}> {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
throw new BadRequestException('User not authenticated');
|
||||
}
|
||||
|
||||
if (!body.keywords || !Array.isArray(body.keywords)) {
|
||||
throw new BadRequestException('Keywords array is required');
|
||||
}
|
||||
|
||||
const validation = await this.keywordsService.validateKeywords(body.keywords);
|
||||
return validation;
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
throw new BadRequestException('Failed to validate keywords');
|
||||
}
|
||||
}
|
||||
}
|
12
packages/api/src/keywords/keywords.module.ts
Normal file
12
packages/api/src/keywords/keywords.module.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { KeywordsController } from './keywords.controller';
|
||||
import { KeywordsService } from './keywords.service';
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
controllers: [KeywordsController],
|
||||
providers: [KeywordsService],
|
||||
exports: [KeywordsService],
|
||||
})
|
||||
export class KeywordsModule {}
|
345
packages/api/src/keywords/keywords.service.ts
Normal file
345
packages/api/src/keywords/keywords.service.ts
Normal file
|
@ -0,0 +1,345 @@
|
|||
import { Injectable, Logger, BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { EnhanceKeywordsResponseDto } from './dto/enhance-keywords.dto';
|
||||
// import OpenAI from 'openai'; // Uncomment when ready to use actual OpenAI integration
|
||||
|
||||
@Injectable()
|
||||
export class KeywordsService {
|
||||
private readonly logger = new Logger(KeywordsService.name);
|
||||
// private readonly openai: OpenAI; // Uncomment when ready to use actual OpenAI
|
||||
private readonly rateLimitMap = new Map<string, { count: number; resetTime: number }>();
|
||||
private readonly RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
|
||||
private readonly RATE_LIMIT_MAX_REQUESTS = 10; // 10 requests per minute per user
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
// Initialize OpenAI client when ready
|
||||
// this.openai = new OpenAI({
|
||||
// apiKey: this.configService.get<string>('OPENAI_API_KEY'),
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance keywords with AI suggestions
|
||||
*/
|
||||
async enhanceKeywords(
|
||||
keywords: string[],
|
||||
userId: string,
|
||||
): Promise<EnhanceKeywordsResponseDto> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
this.logger.log(`Enhancing keywords for user: ${userId}`);
|
||||
|
||||
// Clean and normalize input keywords
|
||||
const cleanKeywords = this.cleanKeywords(keywords);
|
||||
|
||||
// Generate enhanced keywords using AI
|
||||
const enhancedKeywords = await this.generateEnhancedKeywords(cleanKeywords);
|
||||
const relatedKeywords = await this.generateRelatedKeywords(cleanKeywords);
|
||||
const longTailKeywords = await this.generateLongTailKeywords(cleanKeywords);
|
||||
|
||||
const processingTime = (Date.now() - startTime) / 1000;
|
||||
|
||||
const result: EnhanceKeywordsResponseDto = {
|
||||
original_keywords: cleanKeywords,
|
||||
enhanced_keywords: enhancedKeywords,
|
||||
related_keywords: relatedKeywords,
|
||||
long_tail_keywords: longTailKeywords,
|
||||
metadata: {
|
||||
processing_time: processingTime,
|
||||
ai_model: 'mock-gpt-4', // Replace with actual model when using OpenAI
|
||||
confidence_score: 0.92,
|
||||
keywords_generated: enhancedKeywords.length + relatedKeywords.length + longTailKeywords.length,
|
||||
},
|
||||
};
|
||||
|
||||
this.logger.log(`Enhanced keywords successfully for user: ${userId}`);
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to enhance keywords for user: ${userId}`, error.stack);
|
||||
throw new BadRequestException('Failed to enhance keywords');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get keyword suggestions based on context
|
||||
*/
|
||||
async getKeywordSuggestions(
|
||||
context?: string,
|
||||
category?: string,
|
||||
): Promise<{
|
||||
suggestions: string[];
|
||||
categories: Record<string, string[]>;
|
||||
}> {
|
||||
try {
|
||||
// Mock suggestions - replace with actual AI generation
|
||||
const baseSuggestions = [
|
||||
'interior-design',
|
||||
'home-decor',
|
||||
'modern-style',
|
||||
'contemporary',
|
||||
'minimalist',
|
||||
'elegant',
|
||||
'stylish',
|
||||
'trendy',
|
||||
];
|
||||
|
||||
const categories = {
|
||||
style: ['modern', 'contemporary', 'minimalist', 'industrial', 'scandinavian', 'rustic'],
|
||||
room: ['kitchen', 'living-room', 'bedroom', 'bathroom', 'office', 'dining-room'],
|
||||
color: ['white', 'black', 'gray', 'blue', 'green', 'brown'],
|
||||
material: ['wood', 'metal', 'glass', 'stone', 'fabric', 'leather'],
|
||||
feature: ['island', 'cabinet', 'counter', 'lighting', 'flooring', 'window'],
|
||||
};
|
||||
|
||||
// Filter suggestions based on context or category
|
||||
let suggestions = baseSuggestions;
|
||||
if (category && categories[category]) {
|
||||
suggestions = [...baseSuggestions, ...categories[category]];
|
||||
}
|
||||
|
||||
return {
|
||||
suggestions: suggestions.slice(0, 12), // Limit to 12 suggestions
|
||||
categories,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get keyword suggestions', error.stack);
|
||||
throw new BadRequestException('Failed to get keyword suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate keywords for SEO optimization
|
||||
*/
|
||||
async validateKeywords(keywords: string[]): Promise<{
|
||||
valid_keywords: string[];
|
||||
invalid_keywords: Array<{ keyword: string; reason: string }>;
|
||||
recommendations: string[];
|
||||
}> {
|
||||
try {
|
||||
const validKeywords: string[] = [];
|
||||
const invalidKeywords: Array<{ keyword: string; reason: string }> = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
for (const keyword of keywords) {
|
||||
const validation = this.validateSingleKeyword(keyword);
|
||||
if (validation.isValid) {
|
||||
validKeywords.push(keyword);
|
||||
} else {
|
||||
invalidKeywords.push({
|
||||
keyword,
|
||||
reason: validation.reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate recommendations
|
||||
if (invalidKeywords.some(item => item.reason.includes('spaces'))) {
|
||||
recommendations.push('Use hyphens instead of spaces for better SEO');
|
||||
}
|
||||
if (invalidKeywords.some(item => item.reason.includes('short'))) {
|
||||
recommendations.push('Keywords should be at least 2 characters long');
|
||||
}
|
||||
if (invalidKeywords.some(item => item.reason.includes('long'))) {
|
||||
recommendations.push('Keep keywords concise, ideally 2-4 words');
|
||||
}
|
||||
if (keywords.some(k => /\b(the|and|or|but|in|on|at|to|for|of|with|by)\b/i.test(k))) {
|
||||
recommendations.push('Avoid stop words like "the", "and", "or" for better SEO');
|
||||
}
|
||||
|
||||
return {
|
||||
valid_keywords: validKeywords,
|
||||
invalid_keywords: invalidKeywords,
|
||||
recommendations,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to validate keywords', error.stack);
|
||||
throw new BadRequestException('Failed to validate keywords');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check rate limit for user
|
||||
*/
|
||||
async checkRateLimit(userId: string): Promise<void> {
|
||||
const now = Date.now();
|
||||
const userLimit = this.rateLimitMap.get(userId);
|
||||
|
||||
if (!userLimit || now > userLimit.resetTime) {
|
||||
// Reset or create new limit window
|
||||
this.rateLimitMap.set(userId, {
|
||||
count: 1,
|
||||
resetTime: now + this.RATE_LIMIT_WINDOW,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (userLimit.count >= this.RATE_LIMIT_MAX_REQUESTS) {
|
||||
throw new HttpException(
|
||||
'Rate limit exceeded. Try again later.',
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
|
||||
userLimit.count++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean and normalize keywords
|
||||
*/
|
||||
private cleanKeywords(keywords: string[]): string[] {
|
||||
return keywords
|
||||
.map(keyword => keyword.trim().toLowerCase())
|
||||
.filter(keyword => keyword.length > 0)
|
||||
.filter((keyword, index, arr) => arr.indexOf(keyword) === index); // Remove duplicates
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate enhanced keywords using AI (mock implementation)
|
||||
*/
|
||||
private async generateEnhancedKeywords(keywords: string[]): Promise<string[]> {
|
||||
// Simulate AI processing time
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Mock enhanced keywords - replace with actual AI generation
|
||||
const enhancementPrefixes = ['modern', 'contemporary', 'sleek', 'stylish', 'elegant', 'trendy'];
|
||||
const enhancementSuffixes = ['design', 'style', 'decor', 'interior', 'renovation', 'makeover'];
|
||||
|
||||
const enhanced: string[] = [];
|
||||
|
||||
for (const keyword of keywords) {
|
||||
// Create variations with prefixes and suffixes
|
||||
enhancementPrefixes.forEach(prefix => {
|
||||
if (!keyword.startsWith(prefix)) {
|
||||
enhanced.push(`${prefix}-${keyword}`);
|
||||
}
|
||||
});
|
||||
|
||||
enhancementSuffixes.forEach(suffix => {
|
||||
if (!keyword.endsWith(suffix)) {
|
||||
enhanced.push(`${keyword}-${suffix}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Create compound keywords
|
||||
if (keywords.length > 1) {
|
||||
keywords.forEach(otherKeyword => {
|
||||
if (keyword !== otherKeyword) {
|
||||
enhanced.push(`${keyword}-${otherKeyword}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates and limit results
|
||||
return [...new Set(enhanced)].slice(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate related keywords (mock implementation)
|
||||
*/
|
||||
private async generateRelatedKeywords(keywords: string[]): Promise<string[]> {
|
||||
// Simulate AI processing time
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// Mock related keywords - replace with actual AI generation
|
||||
const relatedMap: Record<string, string[]> = {
|
||||
kitchen: ['culinary-space', 'cooking-area', 'kitchen-cabinets', 'kitchen-appliances', 'kitchen-island'],
|
||||
modern: ['contemporary', 'minimalist', 'sleek', 'current', 'updated'],
|
||||
renovation: ['remodel', 'makeover', 'upgrade', 'transformation', 'improvement'],
|
||||
design: ['decor', 'style', 'interior', 'aesthetic', 'layout'],
|
||||
};
|
||||
|
||||
const related: string[] = [];
|
||||
keywords.forEach(keyword => {
|
||||
if (relatedMap[keyword]) {
|
||||
related.push(...relatedMap[keyword]);
|
||||
}
|
||||
});
|
||||
|
||||
// Add generic related terms
|
||||
const genericRelated = [
|
||||
'home-improvement',
|
||||
'interior-design',
|
||||
'space-optimization',
|
||||
'aesthetic-enhancement',
|
||||
];
|
||||
|
||||
return [...new Set([...related, ...genericRelated])].slice(0, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate long-tail keywords (mock implementation)
|
||||
*/
|
||||
private async generateLongTailKeywords(keywords: string[]): Promise<string[]> {
|
||||
// Simulate AI processing time
|
||||
await new Promise(resolve => setTimeout(resolve, 400));
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const longTailTemplates = [
|
||||
`{keyword}-ideas-${currentYear}`,
|
||||
`{keyword}-trends-${currentYear}`,
|
||||
`{keyword}-inspiration-gallery`,
|
||||
`best-{keyword}-designs`,
|
||||
`{keyword}-before-and-after`,
|
||||
`affordable-{keyword}-solutions`,
|
||||
];
|
||||
|
||||
const longTail: string[] = [];
|
||||
keywords.forEach(keyword => {
|
||||
longTailTemplates.forEach(template => {
|
||||
longTail.push(template.replace('{keyword}', keyword));
|
||||
});
|
||||
});
|
||||
|
||||
// Create compound long-tail keywords
|
||||
if (keywords.length >= 2) {
|
||||
const compound = keywords.slice(0, 2).join('-');
|
||||
longTail.push(`${compound}-design-ideas-${currentYear}`);
|
||||
longTail.push(`${compound}-renovation-guide`);
|
||||
longTail.push(`${compound}-style-trends`);
|
||||
}
|
||||
|
||||
return [...new Set(longTail)].slice(0, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single keyword
|
||||
*/
|
||||
private validateSingleKeyword(keyword: string): { isValid: boolean; reason: string } {
|
||||
// Check length
|
||||
if (keyword.length < 2) {
|
||||
return { isValid: false, reason: 'Too short for SEO value' };
|
||||
}
|
||||
|
||||
if (keyword.length > 60) {
|
||||
return { isValid: false, reason: 'Too long for practical use' };
|
||||
}
|
||||
|
||||
// Check for spaces (should use hyphens)
|
||||
if (keyword.includes(' ')) {
|
||||
return { isValid: false, reason: 'Use hyphens instead of spaces' };
|
||||
}
|
||||
|
||||
// Check for invalid characters
|
||||
if (!/^[a-zA-Z0-9-_]+$/.test(keyword)) {
|
||||
return { isValid: false, reason: 'Contains invalid characters' };
|
||||
}
|
||||
|
||||
// Check for double hyphens or underscores
|
||||
if (keyword.includes('--') || keyword.includes('__')) {
|
||||
return { isValid: false, reason: 'Avoid double hyphens or underscores' };
|
||||
}
|
||||
|
||||
// Check if starts or ends with hyphen/underscore
|
||||
if (keyword.startsWith('-') || keyword.endsWith('-') ||
|
||||
keyword.startsWith('_') || keyword.endsWith('_')) {
|
||||
return { isValid: false, reason: 'Should not start or end with hyphen or underscore' };
|
||||
}
|
||||
|
||||
return { isValid: true, reason: '' };
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue