Blog SEO Analysis Modal - Updated with SEO Metadata Generator, Core Metadata Tab, and Metadata Display Components
This commit is contained in:
@@ -21,6 +21,7 @@ router = APIRouter(prefix="/api/blog-writer/seo", tags=["Blog SEO Analysis"])
|
||||
class SEOAnalysisRequest(BaseModel):
|
||||
"""Request model for SEO analysis"""
|
||||
blog_content: str
|
||||
blog_title: Optional[str] = None
|
||||
research_data: Dict[str, Any]
|
||||
user_id: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
@@ -34,6 +35,8 @@ class SEOAnalysisResponse(BaseModel):
|
||||
category_scores: Dict[str, float]
|
||||
analysis_summary: Dict[str, Any]
|
||||
actionable_recommendations: list
|
||||
detailed_analysis: Optional[Dict[str, Any]] = None
|
||||
visualization_data: Optional[Dict[str, Any]] = None
|
||||
generated_at: str
|
||||
error: Optional[str] = None
|
||||
|
||||
@@ -87,7 +90,8 @@ async def analyze_blog_seo(request: SEOAnalysisRequest):
|
||||
# Perform SEO analysis
|
||||
analysis_results = await seo_analyzer.analyze_blog_content(
|
||||
blog_content=request.blog_content,
|
||||
research_data=request.research_data
|
||||
research_data=request.research_data,
|
||||
blog_title=request.blog_title
|
||||
)
|
||||
|
||||
# Check for errors
|
||||
@@ -100,6 +104,8 @@ async def analyze_blog_seo(request: SEOAnalysisRequest):
|
||||
category_scores={},
|
||||
analysis_summary={},
|
||||
actionable_recommendations=[],
|
||||
detailed_analysis=None,
|
||||
visualization_data=None,
|
||||
generated_at=analysis_results.get('generated_at', ''),
|
||||
error=analysis_results['error']
|
||||
)
|
||||
@@ -112,6 +118,8 @@ async def analyze_blog_seo(request: SEOAnalysisRequest):
|
||||
category_scores=analysis_results.get('category_scores', {}),
|
||||
analysis_summary=analysis_results.get('analysis_summary', {}),
|
||||
actionable_recommendations=analysis_results.get('actionable_recommendations', []),
|
||||
detailed_analysis=analysis_results.get('detailed_analysis'),
|
||||
visualization_data=analysis_results.get('visualization_data'),
|
||||
generated_at=analysis_results.get('generated_at', '')
|
||||
)
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ class BlogOptimizeResponse(BaseModel):
|
||||
|
||||
class BlogSEOAnalyzeRequest(BaseModel):
|
||||
content: str
|
||||
blog_title: Optional[str] = None
|
||||
keywords: List[str] = []
|
||||
research_data: Optional[Dict[str, Any]] = None
|
||||
|
||||
@@ -181,15 +182,28 @@ class BlogSEOMetadataRequest(BaseModel):
|
||||
content: str
|
||||
title: Optional[str] = None
|
||||
keywords: List[str] = []
|
||||
research_data: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class BlogSEOMetadataResponse(BaseModel):
|
||||
success: bool = True
|
||||
title_options: List[str]
|
||||
meta_descriptions: List[str]
|
||||
open_graph: Dict[str, Any]
|
||||
twitter_card: Dict[str, Any]
|
||||
schema_data: Dict[str, Any]
|
||||
title_options: List[str] = []
|
||||
meta_descriptions: List[str] = []
|
||||
seo_title: Optional[str] = None
|
||||
meta_description: Optional[str] = None
|
||||
url_slug: Optional[str] = None
|
||||
blog_tags: List[str] = []
|
||||
blog_categories: List[str] = []
|
||||
social_hashtags: List[str] = []
|
||||
open_graph: Dict[str, Any] = {}
|
||||
twitter_card: Dict[str, Any] = {}
|
||||
json_ld_schema: Dict[str, Any] = {}
|
||||
canonical_url: Optional[str] = None
|
||||
reading_time: float = 0.0
|
||||
focus_keyword: Optional[str] = None
|
||||
generated_at: Optional[str] = None
|
||||
optimization_score: int = 0
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class BlogPublishRequest(BaseModel):
|
||||
|
||||
@@ -268,16 +268,53 @@ class BlogWriterService:
|
||||
)
|
||||
|
||||
async def seo_metadata(self, request: BlogSEOMetadataRequest) -> BlogSEOMetadataResponse:
|
||||
"""Generate SEO metadata for content."""
|
||||
# TODO: Move to optimization module
|
||||
return BlogSEOMetadataResponse(
|
||||
success=True,
|
||||
title_options=[request.title or "Generated SEO Title"],
|
||||
meta_descriptions=["Compelling meta description..."],
|
||||
open_graph={"title": request.title or "OG Title", "image": ""},
|
||||
twitter_card={"card": "summary_large_image"},
|
||||
schema={"@type": "Article"},
|
||||
)
|
||||
"""Generate comprehensive SEO metadata for content."""
|
||||
try:
|
||||
from services.blog_writer.seo.blog_seo_metadata_generator import BlogSEOMetadataGenerator
|
||||
|
||||
# Initialize metadata generator
|
||||
metadata_generator = BlogSEOMetadataGenerator()
|
||||
|
||||
# Generate comprehensive metadata
|
||||
metadata_results = await metadata_generator.generate_comprehensive_metadata(
|
||||
blog_content=request.content,
|
||||
blog_title=request.title or "Untitled Blog Post",
|
||||
research_data=request.research_data or {}
|
||||
)
|
||||
|
||||
# Convert to BlogSEOMetadataResponse format
|
||||
return BlogSEOMetadataResponse(
|
||||
success=metadata_results.get('success', True),
|
||||
title_options=metadata_results.get('title_options', []),
|
||||
meta_descriptions=metadata_results.get('meta_descriptions', []),
|
||||
seo_title=metadata_results.get('seo_title'),
|
||||
meta_description=metadata_results.get('meta_description'),
|
||||
url_slug=metadata_results.get('url_slug', ''),
|
||||
blog_tags=metadata_results.get('blog_tags', []),
|
||||
blog_categories=metadata_results.get('blog_categories', []),
|
||||
social_hashtags=metadata_results.get('social_hashtags', []),
|
||||
open_graph=metadata_results.get('open_graph', {}),
|
||||
twitter_card=metadata_results.get('twitter_card', {}),
|
||||
json_ld_schema=metadata_results.get('json_ld_schema', {}),
|
||||
canonical_url=metadata_results.get('canonical_url', ''),
|
||||
reading_time=metadata_results.get('reading_time', 0.0),
|
||||
focus_keyword=metadata_results.get('focus_keyword', ''),
|
||||
generated_at=metadata_results.get('generated_at', ''),
|
||||
optimization_score=metadata_results.get('metadata_summary', {}).get('optimization_score', 0)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SEO metadata generation failed: {e}")
|
||||
# Return fallback response
|
||||
return BlogSEOMetadataResponse(
|
||||
success=False,
|
||||
title_options=[request.title or "Generated SEO Title"],
|
||||
meta_descriptions=["Compelling meta description..."],
|
||||
open_graph={"title": request.title or "OG Title", "image": ""},
|
||||
twitter_card={"card": "summary_large_image"},
|
||||
json_ld_schema={"@type": "Article"},
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def publish(self, request: BlogPublishRequest) -> BlogPublishResponse:
|
||||
"""Publish content to specified platform."""
|
||||
|
||||
@@ -32,7 +32,7 @@ class BlogContentSEOAnalyzer:
|
||||
|
||||
logger.info("BlogContentSEOAnalyzer initialized")
|
||||
|
||||
async def analyze_blog_content(self, blog_content: str, research_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
async def analyze_blog_content(self, blog_content: str, research_data: Dict[str, Any], blog_title: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Main analysis method with parallel processing
|
||||
|
||||
|
||||
488
backend/services/blog_writer/seo/blog_seo_metadata_generator.py
Normal file
488
backend/services/blog_writer/seo/blog_seo_metadata_generator.py
Normal file
@@ -0,0 +1,488 @@
|
||||
"""
|
||||
Blog SEO Metadata Generator
|
||||
|
||||
Optimized SEO metadata generation service that uses maximum 2 AI calls
|
||||
to generate comprehensive metadata including titles, descriptions,
|
||||
Open Graph tags, Twitter cards, and structured data.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from loguru import logger
|
||||
|
||||
from services.llm_providers.gemini_provider import gemini_structured_json_response
|
||||
|
||||
|
||||
class BlogSEOMetadataGenerator:
|
||||
"""Optimized SEO metadata generator with maximum 2 AI calls"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the metadata generator"""
|
||||
self.gemini_provider = gemini_structured_json_response
|
||||
logger.info("BlogSEOMetadataGenerator initialized")
|
||||
|
||||
async def generate_comprehensive_metadata(
|
||||
self,
|
||||
blog_content: str,
|
||||
blog_title: str,
|
||||
research_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate comprehensive SEO metadata using maximum 2 AI calls
|
||||
|
||||
Args:
|
||||
blog_content: The blog content to analyze
|
||||
blog_title: The blog title
|
||||
research_data: Research data containing keywords and insights
|
||||
|
||||
Returns:
|
||||
Comprehensive metadata including all SEO elements
|
||||
"""
|
||||
try:
|
||||
logger.info("Starting comprehensive SEO metadata generation")
|
||||
|
||||
# Extract keywords and context from research data
|
||||
keywords_data = self._extract_keywords_from_research(research_data)
|
||||
logger.info(f"Extracted keywords: {keywords_data}")
|
||||
|
||||
# Call 1: Generate core SEO metadata (parallel with Call 2)
|
||||
logger.info("Generating core SEO metadata")
|
||||
core_metadata_task = self._generate_core_metadata(blog_content, blog_title, keywords_data)
|
||||
|
||||
# Call 2: Generate social media and structured data (parallel with Call 1)
|
||||
logger.info("Generating social media and structured data")
|
||||
social_metadata_task = self._generate_social_metadata(blog_content, blog_title, keywords_data)
|
||||
|
||||
# Wait for both calls to complete
|
||||
core_metadata, social_metadata = await asyncio.gather(
|
||||
core_metadata_task,
|
||||
social_metadata_task
|
||||
)
|
||||
|
||||
# Compile final response
|
||||
results = self._compile_metadata_response(core_metadata, social_metadata, blog_title)
|
||||
|
||||
logger.info(f"SEO metadata generation completed successfully")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SEO metadata generation failed: {e}")
|
||||
# Fail fast - don't return fallback data
|
||||
raise e
|
||||
|
||||
def _extract_keywords_from_research(self, research_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Extract keywords and context from research data"""
|
||||
try:
|
||||
keyword_analysis = research_data.get('keyword_analysis', {})
|
||||
|
||||
# Handle both 'semantic' and 'semantic_keywords' field names
|
||||
semantic_keywords = keyword_analysis.get('semantic', []) or keyword_analysis.get('semantic_keywords', [])
|
||||
|
||||
return {
|
||||
'primary_keywords': keyword_analysis.get('primary', []),
|
||||
'long_tail_keywords': keyword_analysis.get('long_tail', []),
|
||||
'semantic_keywords': semantic_keywords,
|
||||
'all_keywords': keyword_analysis.get('all_keywords', []),
|
||||
'search_intent': keyword_analysis.get('search_intent', 'informational'),
|
||||
'target_audience': research_data.get('target_audience', 'general'),
|
||||
'industry': research_data.get('industry', 'general')
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to extract keywords from research: {e}")
|
||||
return {
|
||||
'primary_keywords': [],
|
||||
'long_tail_keywords': [],
|
||||
'semantic_keywords': [],
|
||||
'all_keywords': [],
|
||||
'search_intent': 'informational',
|
||||
'target_audience': 'general',
|
||||
'industry': 'general'
|
||||
}
|
||||
|
||||
async def _generate_core_metadata(
|
||||
self,
|
||||
blog_content: str,
|
||||
blog_title: str,
|
||||
keywords_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate core SEO metadata (Call 1)"""
|
||||
try:
|
||||
# Create comprehensive prompt for core metadata
|
||||
prompt = self._create_core_metadata_prompt(blog_content, blog_title, keywords_data)
|
||||
|
||||
# Define simplified structured schema for core metadata
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"seo_title": {
|
||||
"type": "string",
|
||||
"description": "SEO-optimized title (50-60 characters)"
|
||||
},
|
||||
"meta_description": {
|
||||
"type": "string",
|
||||
"description": "Meta description (150-160 characters)"
|
||||
},
|
||||
"url_slug": {
|
||||
"type": "string",
|
||||
"description": "URL slug (lowercase, hyphens)"
|
||||
},
|
||||
"blog_tags": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Blog tags array"
|
||||
},
|
||||
"blog_categories": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Blog categories array"
|
||||
},
|
||||
"social_hashtags": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Social media hashtags array"
|
||||
},
|
||||
"reading_time": {
|
||||
"type": "integer",
|
||||
"description": "Reading time in minutes"
|
||||
},
|
||||
"focus_keyword": {
|
||||
"type": "string",
|
||||
"description": "Primary focus keyword"
|
||||
}
|
||||
},
|
||||
"required": ["seo_title", "meta_description", "url_slug", "blog_tags", "blog_categories", "social_hashtags", "reading_time", "focus_keyword"]
|
||||
}
|
||||
|
||||
# Get structured response from Gemini
|
||||
ai_response = self.gemini_provider(
|
||||
prompt=prompt,
|
||||
schema=schema,
|
||||
temperature=0.3,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
# Check if we got a valid response
|
||||
if not ai_response or not isinstance(ai_response, dict):
|
||||
logger.error("Core metadata generation failed: Invalid response from Gemini")
|
||||
# Return fallback response
|
||||
return {
|
||||
'seo_title': blog_title,
|
||||
'meta_description': f'Learn about {primary_keywords.split(", ")[0] if primary_keywords else "this topic"}.',
|
||||
'url_slug': blog_title.lower().replace(' ', '-').replace(':', '').replace(',', '')[:50],
|
||||
'blog_tags': primary_keywords.split(', ') if primary_keywords else ['content'],
|
||||
'blog_categories': ['Content Marketing', 'Technology'],
|
||||
'social_hashtags': ['#content', '#marketing', '#technology'],
|
||||
'reading_time': max(1, word_count // 200),
|
||||
'focus_keyword': primary_keywords.split(', ')[0] if primary_keywords else 'content'
|
||||
}
|
||||
|
||||
logger.info(f"Core metadata generation completed. Response keys: {list(ai_response.keys())}")
|
||||
logger.info(f"Core metadata response: {ai_response}")
|
||||
|
||||
return ai_response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Core metadata generation failed: {e}")
|
||||
raise e
|
||||
|
||||
async def _generate_social_metadata(
|
||||
self,
|
||||
blog_content: str,
|
||||
blog_title: str,
|
||||
keywords_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate social media and structured data (Call 2)"""
|
||||
try:
|
||||
# Create comprehensive prompt for social metadata
|
||||
prompt = self._create_social_metadata_prompt(blog_content, blog_title, keywords_data)
|
||||
|
||||
# Define simplified structured schema for social metadata
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"open_graph": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"image": {"type": "string"},
|
||||
"type": {"type": "string"},
|
||||
"site_name": {"type": "string"},
|
||||
"url": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"twitter_card": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"card": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"image": {"type": "string"},
|
||||
"site": {"type": "string"},
|
||||
"creator": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"json_ld_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"@context": {"type": "string"},
|
||||
"@type": {"type": "string"},
|
||||
"headline": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
"author": {"type": "object"},
|
||||
"publisher": {"type": "object"},
|
||||
"datePublished": {"type": "string"},
|
||||
"dateModified": {"type": "string"},
|
||||
"mainEntityOfPage": {"type": "string"},
|
||||
"keywords": {"type": "array"},
|
||||
"wordCount": {"type": "integer"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["open_graph", "twitter_card", "json_ld_schema"]
|
||||
}
|
||||
|
||||
# Get structured response from Gemini
|
||||
ai_response = self.gemini_provider(
|
||||
prompt=prompt,
|
||||
schema=schema,
|
||||
temperature=0.3,
|
||||
max_tokens=2048
|
||||
)
|
||||
|
||||
# Check if we got a valid response
|
||||
if not ai_response or not isinstance(ai_response, dict) or not ai_response.get('open_graph') or not ai_response.get('twitter_card') or not ai_response.get('json_ld_schema'):
|
||||
logger.error("Social metadata generation failed: Invalid or empty response from Gemini")
|
||||
# Return fallback response
|
||||
return {
|
||||
'open_graph': {
|
||||
'title': blog_title,
|
||||
'description': f'Learn about {keywords_data.get("primary_keywords", ["this topic"])[0] if keywords_data.get("primary_keywords") else "this topic"}.',
|
||||
'image': 'https://example.com/image.jpg',
|
||||
'type': 'article',
|
||||
'site_name': 'Your Website',
|
||||
'url': 'https://example.com/blog'
|
||||
},
|
||||
'twitter_card': {
|
||||
'card': 'summary_large_image',
|
||||
'title': blog_title,
|
||||
'description': f'Learn about {keywords_data.get("primary_keywords", ["this topic"])[0] if keywords_data.get("primary_keywords") else "this topic"}.',
|
||||
'image': 'https://example.com/image.jpg',
|
||||
'site': '@yourwebsite',
|
||||
'creator': '@author'
|
||||
},
|
||||
'json_ld_schema': {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
'headline': blog_title,
|
||||
'description': f'Learn about {keywords_data.get("primary_keywords", ["this topic"])[0] if keywords_data.get("primary_keywords") else "this topic"}.',
|
||||
'author': {'@type': 'Person', 'name': 'Author Name'},
|
||||
'publisher': {'@type': 'Organization', 'name': 'Your Website'},
|
||||
'datePublished': '2025-01-01T00:00:00Z',
|
||||
'dateModified': '2025-01-01T00:00:00Z',
|
||||
'mainEntityOfPage': 'https://example.com/blog',
|
||||
'keywords': keywords_data.get('primary_keywords', ['content']),
|
||||
'wordCount': len(blog_content.split())
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"Social metadata generation completed. Response keys: {list(ai_response.keys())}")
|
||||
logger.info(f"Open Graph data: {ai_response.get('open_graph', 'Not found')}")
|
||||
logger.info(f"Twitter Card data: {ai_response.get('twitter_card', 'Not found')}")
|
||||
logger.info(f"JSON-LD data: {ai_response.get('json_ld_schema', 'Not found')}")
|
||||
|
||||
return ai_response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Social metadata generation failed: {e}")
|
||||
raise e
|
||||
|
||||
def _create_core_metadata_prompt(
|
||||
self,
|
||||
blog_content: str,
|
||||
blog_title: str,
|
||||
keywords_data: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Create high-quality prompt for core metadata generation"""
|
||||
|
||||
primary_keywords = ", ".join(keywords_data.get('primary_keywords', []))
|
||||
semantic_keywords = ", ".join(keywords_data.get('semantic_keywords', []))
|
||||
search_intent = keywords_data.get('search_intent', 'informational')
|
||||
target_audience = keywords_data.get('target_audience', 'general')
|
||||
industry = keywords_data.get('industry', 'general')
|
||||
|
||||
# Calculate word count for reading time estimation
|
||||
word_count = len(blog_content.split())
|
||||
|
||||
prompt = f"""
|
||||
Generate SEO metadata for this blog post.
|
||||
|
||||
BLOG TITLE: {blog_title}
|
||||
BLOG CONTENT: {blog_content[:1000]}...
|
||||
PRIMARY KEYWORDS: {primary_keywords}
|
||||
SEMANTIC KEYWORDS: {semantic_keywords}
|
||||
WORD COUNT: {word_count}
|
||||
|
||||
Generate:
|
||||
1. SEO TITLE (50-60 characters) - include primary keyword
|
||||
2. META DESCRIPTION (150-160 characters) - include CTA
|
||||
3. URL SLUG (lowercase, hyphens, 3-5 words)
|
||||
4. BLOG TAGS (5-8 relevant tags)
|
||||
5. BLOG CATEGORIES (2-3 categories)
|
||||
6. SOCIAL HASHTAGS (5-10 hashtags with #)
|
||||
7. READING TIME (calculate from {word_count} words)
|
||||
8. FOCUS KEYWORD (primary keyword for SEO)
|
||||
|
||||
Make it compelling and SEO-optimized.
|
||||
"""
|
||||
return prompt
|
||||
|
||||
def _create_social_metadata_prompt(
|
||||
self,
|
||||
blog_content: str,
|
||||
blog_title: str,
|
||||
keywords_data: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Create high-quality prompt for social metadata generation"""
|
||||
|
||||
primary_keywords = ", ".join(keywords_data.get('primary_keywords', []))
|
||||
search_intent = keywords_data.get('search_intent', 'informational')
|
||||
target_audience = keywords_data.get('target_audience', 'general')
|
||||
industry = keywords_data.get('industry', 'general')
|
||||
|
||||
current_date = datetime.now().isoformat()
|
||||
|
||||
prompt = f"""
|
||||
Generate social media metadata for this blog post.
|
||||
|
||||
BLOG TITLE: {blog_title}
|
||||
BLOG CONTENT: {blog_content[:800]}...
|
||||
PRIMARY KEYWORDS: {primary_keywords}
|
||||
CURRENT DATE: {current_date}
|
||||
|
||||
Generate:
|
||||
|
||||
1. OPEN GRAPH (Facebook/LinkedIn):
|
||||
- title: 60 chars max
|
||||
- description: 160 chars max
|
||||
- image: image URL
|
||||
- type: "article"
|
||||
- site_name: site name
|
||||
- url: canonical URL
|
||||
|
||||
2. TWITTER CARD:
|
||||
- card: "summary_large_image"
|
||||
- title: 70 chars max
|
||||
- description: 200 chars max with hashtags
|
||||
- image: image URL
|
||||
- site: @sitename
|
||||
- creator: @author
|
||||
|
||||
3. JSON-LD SCHEMA:
|
||||
- @context: "https://schema.org"
|
||||
- @type: "Article"
|
||||
- headline: article title
|
||||
- description: article description
|
||||
- author: {{"@type": "Person", "name": "Author Name"}}
|
||||
- publisher: {{"@type": "Organization", "name": "Site Name"}}
|
||||
- datePublished: ISO date
|
||||
- dateModified: ISO date
|
||||
- mainEntityOfPage: canonical URL
|
||||
- keywords: array of keywords
|
||||
- wordCount: word count
|
||||
|
||||
Make it engaging and SEO-optimized.
|
||||
"""
|
||||
return prompt
|
||||
|
||||
def _compile_metadata_response(
|
||||
self,
|
||||
core_metadata: Dict[str, Any],
|
||||
social_metadata: Dict[str, Any],
|
||||
original_title: str
|
||||
) -> Dict[str, Any]:
|
||||
"""Compile final metadata response"""
|
||||
try:
|
||||
# Extract data from AI responses
|
||||
seo_title = core_metadata.get('seo_title', original_title)
|
||||
meta_description = core_metadata.get('meta_description', '')
|
||||
url_slug = core_metadata.get('url_slug', '')
|
||||
blog_tags = core_metadata.get('blog_tags', [])
|
||||
blog_categories = core_metadata.get('blog_categories', [])
|
||||
social_hashtags = core_metadata.get('social_hashtags', [])
|
||||
canonical_url = core_metadata.get('canonical_url', '')
|
||||
reading_time = core_metadata.get('reading_time', 0)
|
||||
focus_keyword = core_metadata.get('focus_keyword', '')
|
||||
|
||||
open_graph = social_metadata.get('open_graph', {})
|
||||
twitter_card = social_metadata.get('twitter_card', {})
|
||||
json_ld_schema = social_metadata.get('json_ld_schema', {})
|
||||
|
||||
# Compile comprehensive response
|
||||
response = {
|
||||
'success': True,
|
||||
'title_options': [seo_title], # For backward compatibility
|
||||
'meta_descriptions': [meta_description], # For backward compatibility
|
||||
'seo_title': seo_title,
|
||||
'meta_description': meta_description,
|
||||
'url_slug': url_slug,
|
||||
'blog_tags': blog_tags,
|
||||
'blog_categories': blog_categories,
|
||||
'social_hashtags': social_hashtags,
|
||||
'canonical_url': canonical_url,
|
||||
'reading_time': reading_time,
|
||||
'focus_keyword': focus_keyword,
|
||||
'open_graph': open_graph,
|
||||
'twitter_card': twitter_card,
|
||||
'json_ld_schema': json_ld_schema,
|
||||
'generated_at': datetime.utcnow().isoformat(),
|
||||
'metadata_summary': {
|
||||
'total_metadata_types': 10,
|
||||
'ai_calls_used': 2,
|
||||
'optimization_score': self._calculate_optimization_score(core_metadata, social_metadata)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"Metadata compilation completed. Generated {len(response)} metadata fields")
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Metadata compilation failed: {e}")
|
||||
raise e
|
||||
|
||||
def _calculate_optimization_score(self, core_metadata: Dict[str, Any], social_metadata: Dict[str, Any]) -> int:
|
||||
"""Calculate overall optimization score for the generated metadata"""
|
||||
try:
|
||||
score = 0
|
||||
|
||||
# Check core metadata completeness
|
||||
if core_metadata.get('seo_title'):
|
||||
score += 15
|
||||
if core_metadata.get('meta_description'):
|
||||
score += 15
|
||||
if core_metadata.get('url_slug'):
|
||||
score += 10
|
||||
if core_metadata.get('blog_tags'):
|
||||
score += 10
|
||||
if core_metadata.get('blog_categories'):
|
||||
score += 10
|
||||
if core_metadata.get('social_hashtags'):
|
||||
score += 10
|
||||
if core_metadata.get('focus_keyword'):
|
||||
score += 10
|
||||
|
||||
# Check social metadata completeness
|
||||
if social_metadata.get('open_graph'):
|
||||
score += 10
|
||||
if social_metadata.get('twitter_card'):
|
||||
score += 5
|
||||
if social_metadata.get('json_ld_schema'):
|
||||
score += 5
|
||||
|
||||
return min(score, 100) # Cap at 100
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to calculate optimization score: {e}")
|
||||
return 0
|
||||
101
backend/test_api_endpoint.py
Normal file
101
backend/test_api_endpoint.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Test script for the SEO metadata API endpoint
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
def test_seo_metadata_endpoint():
|
||||
"""Test the SEO metadata API endpoint"""
|
||||
|
||||
# Test data
|
||||
test_data = {
|
||||
"content": "# The Future of AI in Content Marketing\n\nArtificial Intelligence is revolutionizing the way we create and distribute content. From automated content generation to personalized marketing campaigns, AI is transforming the content marketing landscape.\n\n## Key Benefits of AI in Content Marketing\n\n1. **Automated Content Creation**: AI can generate high-quality content at scale\n2. **Personalization**: AI enables hyper-personalized content for different audiences\n3. **Optimization**: AI helps optimize content for better performance\n4. **Analytics**: AI provides deeper insights into content performance",
|
||||
"title": "The Future of AI in Content Marketing",
|
||||
"research_data": {
|
||||
"keyword_analysis": {
|
||||
"primary": ["AI content marketing", "artificial intelligence marketing", "content automation"],
|
||||
"long_tail": ["AI content marketing tools 2024", "automated content generation benefits"],
|
||||
"semantic": ["machine learning", "content strategy", "digital marketing", "automation"],
|
||||
"search_intent": "informational",
|
||||
"target_audience": "marketing professionals",
|
||||
"industry": "technology"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
print("🚀 Testing SEO Metadata API Endpoint...")
|
||||
print(f"📡 Making request to: http://localhost:8000/api/blog/seo/metadata")
|
||||
|
||||
# Make the API request
|
||||
response = requests.post(
|
||||
"http://localhost:8000/api/blog/seo/metadata",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json=test_data,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
print(f"📊 Response Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✅ API Endpoint Test Successful!")
|
||||
print("=" * 50)
|
||||
|
||||
# Debug: Print the full response structure
|
||||
print("🔍 Full API Response Structure:")
|
||||
for key, value in result.items():
|
||||
if isinstance(value, dict):
|
||||
print(f" {key}: {type(value)} with {len(value)} keys")
|
||||
elif isinstance(value, list):
|
||||
print(f" {key}: {type(value)} with {len(value)} items")
|
||||
else:
|
||||
print(f" {key}: {type(value)} = {value}")
|
||||
print("-" * 50)
|
||||
|
||||
# Display key results
|
||||
print(f"Success: {result.get('success', False)}")
|
||||
print(f"SEO Title: {result.get('seo_title', 'N/A')}")
|
||||
print(f"Meta Description: {result.get('meta_description', 'N/A')}")
|
||||
print(f"URL Slug: {result.get('url_slug', 'N/A')}")
|
||||
print(f"Blog Tags: {result.get('blog_tags', [])}")
|
||||
print(f"Blog Categories: {result.get('blog_categories', [])}")
|
||||
print(f"Social Hashtags: {result.get('social_hashtags', [])}")
|
||||
print(f"Reading Time: {result.get('reading_time', 0)} minutes")
|
||||
print(f"Focus Keyword: {result.get('focus_keyword', 'N/A')}")
|
||||
print(f"Optimization Score: {result.get('optimization_score', 0)}%")
|
||||
|
||||
# Social media metadata
|
||||
open_graph = result.get('open_graph', {})
|
||||
twitter_card = result.get('twitter_card', {})
|
||||
print(f"\n📱 Social Media Metadata:")
|
||||
print(f"OG Title: {open_graph.get('title', 'N/A')}")
|
||||
print(f"OG Description: {open_graph.get('description', 'N/A')}")
|
||||
print(f"Twitter Title: {twitter_card.get('title', 'N/A')}")
|
||||
print(f"Twitter Description: {twitter_card.get('description', 'N/A')}")
|
||||
|
||||
# Structured data
|
||||
json_ld = result.get('json_ld_schema', {})
|
||||
print(f"\n🔍 Structured Data:")
|
||||
print(f"Schema Type: {json_ld.get('@type', 'N/A')}")
|
||||
print(f"Headline: {json_ld.get('headline', 'N/A')}")
|
||||
|
||||
print(f"\n⏱️ Generated at: {result.get('generated_at', 'N/A')}")
|
||||
print("🎉 API endpoint test completed successfully!")
|
||||
|
||||
else:
|
||||
print(f"❌ API Endpoint Test Failed!")
|
||||
print(f"Status Code: {response.status_code}")
|
||||
print(f"Response: {response.text}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("❌ Connection Error: Could not connect to the server")
|
||||
print("Make sure the backend server is running on http://localhost:8000")
|
||||
except requests.exceptions.Timeout:
|
||||
print("❌ Timeout Error: Request took too long")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_seo_metadata_endpoint()
|
||||
109
backend/test_seo_metadata_generator.py
Normal file
109
backend/test_seo_metadata_generator.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Test script for BlogSEOMetadataGenerator
|
||||
Run this to verify the service works correctly
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the backend directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from services.blog_writer.seo.blog_seo_metadata_generator import BlogSEOMetadataGenerator
|
||||
|
||||
|
||||
async def test_metadata_generation():
|
||||
"""Test the metadata generation service"""
|
||||
|
||||
# Sample blog content
|
||||
blog_content = """
|
||||
# The Future of AI in Content Marketing
|
||||
|
||||
Artificial Intelligence is revolutionizing the way we create and distribute content.
|
||||
From automated content generation to personalized marketing campaigns, AI is transforming
|
||||
the content marketing landscape.
|
||||
|
||||
## Key Benefits of AI in Content Marketing
|
||||
|
||||
1. **Automated Content Creation**: AI can generate high-quality content at scale
|
||||
2. **Personalization**: AI enables hyper-personalized content for different audiences
|
||||
3. **Optimization**: AI helps optimize content for better performance
|
||||
4. **Analytics**: AI provides deeper insights into content performance
|
||||
|
||||
## The Road Ahead
|
||||
|
||||
As AI technology continues to evolve, we can expect even more sophisticated
|
||||
content marketing tools and strategies. The future is bright for AI-powered content marketing.
|
||||
"""
|
||||
|
||||
blog_title = "The Future of AI in Content Marketing"
|
||||
|
||||
# Sample research data
|
||||
research_data = {
|
||||
"keyword_analysis": {
|
||||
"primary": ["AI content marketing", "artificial intelligence marketing", "content automation"],
|
||||
"long_tail": ["AI content marketing tools 2024", "automated content generation benefits"],
|
||||
"semantic": ["machine learning", "content strategy", "digital marketing", "automation"],
|
||||
"search_intent": "informational",
|
||||
"target_audience": "marketing professionals",
|
||||
"industry": "technology"
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
print("🚀 Testing BlogSEOMetadataGenerator...")
|
||||
|
||||
# Initialize the generator
|
||||
generator = BlogSEOMetadataGenerator()
|
||||
|
||||
# Generate metadata
|
||||
print("📝 Generating comprehensive SEO metadata...")
|
||||
results = await generator.generate_comprehensive_metadata(
|
||||
blog_content=blog_content,
|
||||
blog_title=blog_title,
|
||||
research_data=research_data
|
||||
)
|
||||
|
||||
# Display results
|
||||
print("\n✅ Metadata Generation Results:")
|
||||
print("=" * 50)
|
||||
|
||||
print(f"Success: {results.get('success', False)}")
|
||||
print(f"SEO Title: {results.get('seo_title', 'N/A')}")
|
||||
print(f"Meta Description: {results.get('meta_description', 'N/A')}")
|
||||
print(f"URL Slug: {results.get('url_slug', 'N/A')}")
|
||||
print(f"Blog Tags: {results.get('blog_tags', [])}")
|
||||
print(f"Blog Categories: {results.get('blog_categories', [])}")
|
||||
print(f"Social Hashtags: {results.get('social_hashtags', [])}")
|
||||
print(f"Reading Time: {results.get('reading_time', 0)} minutes")
|
||||
print(f"Focus Keyword: {results.get('focus_keyword', 'N/A')}")
|
||||
print(f"Optimization Score: {results.get('metadata_summary', {}).get('optimization_score', 0)}%")
|
||||
|
||||
print("\n📱 Social Media Metadata:")
|
||||
print("-" * 30)
|
||||
open_graph = results.get('open_graph', {})
|
||||
print(f"OG Title: {open_graph.get('title', 'N/A')}")
|
||||
print(f"OG Description: {open_graph.get('description', 'N/A')}")
|
||||
|
||||
twitter_card = results.get('twitter_card', {})
|
||||
print(f"Twitter Title: {twitter_card.get('title', 'N/A')}")
|
||||
print(f"Twitter Description: {twitter_card.get('description', 'N/A')}")
|
||||
|
||||
print("\n🔍 Structured Data:")
|
||||
print("-" * 20)
|
||||
json_ld = results.get('json_ld_schema', {})
|
||||
print(f"Schema Type: {json_ld.get('@type', 'N/A')}")
|
||||
print(f"Headline: {json_ld.get('headline', 'N/A')}")
|
||||
|
||||
print(f"\n⏱️ Generation completed in: {results.get('generated_at', 'N/A')}")
|
||||
print("🎉 Test completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Test failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_metadata_generation())
|
||||
Reference in New Issue
Block a user