diff --git a/backend/api/blog_writer/seo_analysis.py b/backend/api/blog_writer/seo_analysis.py index fb7cc3a1..dee0df5e 100644 --- a/backend/api/blog_writer/seo_analysis.py +++ b/backend/api/blog_writer/seo_analysis.py @@ -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', '') ) diff --git a/backend/models/blog_models.py b/backend/models/blog_models.py index 2b9e935c..9b53661e 100644 --- a/backend/models/blog_models.py +++ b/backend/models/blog_models.py @@ -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): diff --git a/backend/services/blog_writer/core/blog_writer_service.py b/backend/services/blog_writer/core/blog_writer_service.py index 0dc5240b..ac8ce29d 100644 --- a/backend/services/blog_writer/core/blog_writer_service.py +++ b/backend/services/blog_writer/core/blog_writer_service.py @@ -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.""" diff --git a/backend/services/blog_writer/seo/blog_content_seo_analyzer.py b/backend/services/blog_writer/seo/blog_content_seo_analyzer.py index 00fe4f8c..9826bd80 100644 --- a/backend/services/blog_writer/seo/blog_content_seo_analyzer.py +++ b/backend/services/blog_writer/seo/blog_content_seo_analyzer.py @@ -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 diff --git a/backend/services/blog_writer/seo/blog_seo_metadata_generator.py b/backend/services/blog_writer/seo/blog_seo_metadata_generator.py new file mode 100644 index 00000000..c180a1aa --- /dev/null +++ b/backend/services/blog_writer/seo/blog_seo_metadata_generator.py @@ -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 diff --git a/backend/test_api_endpoint.py b/backend/test_api_endpoint.py new file mode 100644 index 00000000..2b5871f5 --- /dev/null +++ b/backend/test_api_endpoint.py @@ -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() diff --git a/backend/test_seo_metadata_generator.py b/backend/test_seo_metadata_generator.py new file mode 100644 index 00000000..afd82b6c --- /dev/null +++ b/backend/test_seo_metadata_generator.py @@ -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()) diff --git a/frontend/src/components/BlogWriter/BlogWriter.tsx b/frontend/src/components/BlogWriter/BlogWriter.tsx index 9b203c4e..db1b2cea 100644 --- a/frontend/src/components/BlogWriter/BlogWriter.tsx +++ b/frontend/src/components/BlogWriter/BlogWriter.tsx @@ -29,6 +29,7 @@ import { OutlineProgressModal } from './OutlineProgressModal'; import OutlineFeedbackForm from './OutlineFeedbackForm'; import { BlogEditor } from './WYSIWYG'; import { SEOAnalysisModal } from './SEOAnalysisModal'; +import { SEOMetadataModal } from './SEOMetadataModal'; // Type assertion for CopilotKit action const useCopilotActionTyped = useCopilotAction as any; @@ -159,6 +160,7 @@ export const BlogWriter: React.FC = () => { // SEO Analysis Modal state const [isSEOAnalysisModalOpen, setIsSEOAnalysisModalOpen] = useState(false); + const [isSEOMetadataModalOpen, setIsSEOMetadataModalOpen] = useState(false); useEffect(() => { if ((mediumPolling.isPolling || rewritePolling.isPolling || isMediumGenerationStarting) && !showModal) { @@ -268,6 +270,42 @@ export const BlogWriter: React.FC = () => { } }); + // Generate SEO Metadata Action + useCopilotActionTyped({ + name: "generateSEOMetadata", + description: "Generate comprehensive SEO metadata including titles, descriptions, Open Graph tags, Twitter cards, and structured data", + parameters: [ + { + name: "title", + type: "string", + description: "Optional blog title to use for metadata generation", + required: false + } + ], + handler: async ({ title }: { title?: string }) => { + console.log('šŸš€ Generate SEO Metadata Action Triggered!'); + console.log('Title provided:', title); + console.log('Selected title:', selectedTitle); + console.log('Sections available:', !!sections && Object.keys(sections).length > 0); + console.log('Research data available:', !!research && !!research.keyword_analysis); + + // Check if we have content to generate metadata for + if (!sections || Object.keys(sections).length === 0) { + return "Please generate blog content first before creating SEO metadata. Use the content generation features to create your blog post."; + } + + if (!research || !research.keyword_analysis) { + return "Please complete research first to get keyword data for SEO metadata generation. Use the research features to gather keyword insights."; + } + + // Open the SEO metadata modal + setIsSEOMetadataModalOpen(true); + console.log('SEO Metadata modal opened'); + + return "Opening SEO metadata generator! This will create optimized titles, descriptions, Open Graph tags, Twitter cards, and structured data for your blog post."; + } + }); + @@ -373,10 +411,10 @@ export const BlogWriter: React.FC = () => {
{outlineConfirmed ? ( /* WYSIWYG Editor - Show when outline is confirmed */ - setIsSEOAnalysisModalOpen(false)} blogContent={buildFullMarkdown()} + blogTitle={selectedTitle} researchData={research} onApplyRecommendations={(recommendations) => { console.log('Applying SEO recommendations:', recommendations); // TODO: Implement recommendation application logic }} /> + + {/* SEO Metadata Modal */} + setIsSEOMetadataModalOpen(false)} + blogContent={buildFullMarkdown()} + blogTitle={selectedTitle} + researchData={research} + onMetadataGenerated={(metadata) => { + console.log('SEO metadata generated:', metadata); + setSeoMetadata(metadata); + // TODO: Implement metadata application logic + }} + />
); }; diff --git a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/CoreMetadataTab.tsx b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/CoreMetadataTab.tsx new file mode 100644 index 00000000..92c39bf6 --- /dev/null +++ b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/CoreMetadataTab.tsx @@ -0,0 +1,394 @@ +/** + * Core Metadata Tab Component + * + * Displays and allows editing of core SEO metadata including: + * - SEO Title + * - Meta Description + * - URL Slug + * - Blog Tags + * - Blog Categories + * - Social Hashtags + * - Reading Time + * - Focus Keyword + */ + +import React from 'react'; +import { + Box, + Typography, + TextField, + Chip, + Paper, + Grid, + IconButton, + Tooltip, + InputAdornment, + FormControl, + InputLabel, + Select, + MenuItem, + OutlinedInput, + Alert +} from '@mui/material'; +import { + ContentCopy as CopyIcon, + Check as CheckIcon, + Search as SearchIcon, + Link as LinkIcon, + Tag as TagIcon, + Category as CategoryIcon, + Schedule as ScheduleIcon, + TrendingUp as TrendingUpIcon +} from '@mui/icons-material'; + +interface CoreMetadataTabProps { + metadata: any; + onMetadataEdit: (field: string, value: any) => void; + onCopyToClipboard: (text: string, itemId: string) => void; + copiedItems: Set; +} + +export const CoreMetadataTab: React.FC = ({ + metadata, + onMetadataEdit, + onCopyToClipboard, + copiedItems +}) => { + const handleTextFieldChange = (field: string) => (event: React.ChangeEvent) => { + onMetadataEdit(field, event.target.value); + }; + + const handleTagsChange = (field: string) => (event: any) => { + const value = typeof event.target.value === 'string' ? event.target.value.split(',') : event.target.value; + onMetadataEdit(field, value); + }; + + const getCharacterCountColor = (current: number, max: number) => { + if (current > max) return 'error'; + if (current > max * 0.9) return 'warning'; + return 'success'; + }; + + const getCharacterCountText = (current: number, max: number) => { + if (current > max) return `${current}/${max} (Too long)`; + if (current > max * 0.9) return `${current}/${max} (Near limit)`; + return `${current}/${max}`; + }; + + return ( + + + + Core SEO Metadata + + + + {/* SEO Title */} + + + + + + SEO Title + + + onCopyToClipboard(metadata.seo_title || '', 'seo_title')} + > + {copiedItems.has('seo_title') ? : } + + + + + + {getCharacterCountText((metadata.seo_title || '').length, 60)} + + + ) + }} + /> + + Include your primary keyword and make it compelling for clicks + + + + + {/* Meta Description */} + + + + + + Meta Description + + + onCopyToClipboard(metadata.meta_description || '', 'meta_description')} + > + {copiedItems.has('meta_description') ? : } + + + + + + {getCharacterCountText((metadata.meta_description || '').length, 160)} + + + ) + }} + /> + + Include a call-to-action and your primary keyword + + + + + {/* URL Slug */} + + + + + + URL Slug + + + onCopyToClipboard(metadata.url_slug || '', 'url_slug')} + > + {copiedItems.has('url_slug') ? : } + + + + + + + + {/* Focus Keyword */} + + + + + + Focus Keyword + + + onCopyToClipboard(metadata.focus_keyword || '', 'focus_keyword')} + > + {copiedItems.has('focus_keyword') ? : } + + + + + + + + {/* Blog Tags */} + + + + + + Blog Tags + + + onCopyToClipboard((metadata.blog_tags || []).join(', '), 'blog_tags')} + > + {copiedItems.has('blog_tags') ? : } + + + + + Tags + + + + Add relevant tags for better categorization and discoverability + + + + + {/* Blog Categories */} + + + + + + Blog Categories + + + onCopyToClipboard((metadata.blog_categories || []).join(', '), 'blog_categories')} + > + {copiedItems.has('blog_categories') ? : } + + + + + Categories + + + + Select 2-3 primary categories for your content + + + + + {/* Social Hashtags */} + + + + + + Social Hashtags + + + onCopyToClipboard((metadata.social_hashtags || []).join(' '), 'social_hashtags')} + > + {copiedItems.has('social_hashtags') ? : } + + + + + Hashtags + + + + Include # symbol for social media platforms + + + + + {/* Reading Time */} + + + + + + Reading Time + + + onCopyToClipboard(`${metadata.reading_time || 0} minutes`, 'reading_time')} + > + {copiedItems.has('reading_time') ? : } + + + + minutes + }} + helperText="Estimated reading time for your content" + /> + + + + + ); +}; diff --git a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/PreviewCard.tsx b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/PreviewCard.tsx new file mode 100644 index 00000000..551f60d9 --- /dev/null +++ b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/PreviewCard.tsx @@ -0,0 +1,388 @@ +/** + * Preview Card Component + * + * Displays live previews of how the metadata will appear in: + * - Search engine results + * - Social media platforms + * - Rich snippets + */ + +import React from 'react'; +import { + Box, + Typography, + Paper, + Grid, + Card, + CardContent, + Chip, + Divider, + Alert, + IconButton, + Tooltip, + Button +} from '@mui/material'; +import { + ContentCopy as CopyIcon, + Check as CheckIcon, + Search as SearchIcon, + Share as ShareIcon, + Code as CodeIcon, + Facebook as FacebookIcon, + Twitter as TwitterIcon, + LinkedIn as LinkedInIcon, + Google as GoogleIcon +} from '@mui/icons-material'; + +interface PreviewCardProps { + metadata: any; + blogTitle: string; +} + +export const PreviewCard: React.FC = ({ + metadata, + blogTitle +}) => { + const copyToClipboard = async (text: string, itemId: string) => { + try { + await navigator.clipboard.writeText(text); + // You could add a state to show "Copied!" feedback here + } catch (err) { + console.error('Failed to copy to clipboard:', err); + } + }; + + const getCurrentDate = () => { + return new Date().toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + const getCurrentTime = () => { + return new Date().toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit' + }); + }; + + return ( + + + + Live Preview + + + + {/* Google Search Results Preview */} + + + + + + Google Search Results + + + + + + + {/* URL */} + + {metadata.canonical_url || 'https://yourwebsite.com/blog-post'} + + + {/* Title */} + + {metadata.seo_title || blogTitle} + + + {/* Description */} + + {metadata.meta_description || 'Your meta description will appear here in Google search results...'} + + + {/* Additional Info */} + + + {getCurrentDate()} + + + • + + + {metadata.reading_time || 5} min read + + {metadata.blog_tags && metadata.blog_tags.length > 0 && ( + <> + + • + + + {metadata.blog_tags.slice(0, 2).join(', ')} + + + )} + + + + + + This is how your blog post will appear in Google search results + + + + + {/* Social Media Previews */} + + + + + + Facebook Preview + + + + + + + {/* Image placeholder */} + + + {metadata.open_graph?.image ? 'Image loaded' : 'No image set'} + + + + + {/* URL */} + + {metadata.canonical_url || 'yourwebsite.com'} + + + {/* Title */} + + {metadata.open_graph?.title || metadata.seo_title || blogTitle} + + + {/* Description */} + + {metadata.open_graph?.description || metadata.meta_description || 'Your description will appear here...'} + + + + + + + + + + + + + Twitter Preview + + + + + + + {/* Image placeholder */} + + + {metadata.twitter_card?.image ? 'Image loaded' : 'No image set'} + + + + + {/* URL */} + + {metadata.canonical_url || 'yourwebsite.com'} + + + {/* Title */} + + {metadata.twitter_card?.title || metadata.seo_title || blogTitle} + + + {/* Description */} + + {metadata.twitter_card?.description || metadata.meta_description || 'Your description will appear here...'} + + + {/* Twitter handle */} + {metadata.twitter_card?.site && ( + + {metadata.twitter_card.site} + + )} + + + + + + + {/* Rich Snippets Preview */} + + + + + + Rich Snippets Preview + + + + + + + {/* Article Schema Preview */} + + + {metadata.json_ld_schema?.headline || metadata.seo_title || blogTitle} + + + + + + {metadata.json_ld_schema?.description || metadata.meta_description || 'Article description...'} + + + + {metadata.json_ld_schema?.author?.name && ( + + + By {metadata.json_ld_schema.author.name} + + + )} + + {metadata.json_ld_schema?.datePublished && ( + + + {new Date(metadata.json_ld_schema.datePublished).toLocaleDateString()} + + + )} + + {metadata.reading_time && ( + + + {metadata.reading_time} min read + + + )} + + {metadata.json_ld_schema?.wordCount && ( + + + {metadata.json_ld_schema.wordCount} words + + + )} + + + {metadata.json_ld_schema?.keywords && metadata.json_ld_schema.keywords.length > 0 && ( + + + Keywords: + + + {metadata.json_ld_schema.keywords.slice(0, 5).map((keyword: string, index: number) => ( + + ))} + + + )} + + + + + Rich snippets help search engines understand your content and may display additional information in search results + + + + + {/* Metadata Summary */} + + + + + Metadata Summary + + + + + + + {metadata.optimization_score || 0}% + + + Optimization Score + + + + + + + + {metadata.reading_time || 0} + + + Reading Time (min) + + + + + + + + {metadata.blog_tags?.length || 0} + + + Tags + + + + + + + + {metadata.blog_categories?.length || 0} + + + Categories + + + + + + + + + ); +}; diff --git a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/SocialMediaTab.tsx b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/SocialMediaTab.tsx new file mode 100644 index 00000000..5225a9de --- /dev/null +++ b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/SocialMediaTab.tsx @@ -0,0 +1,444 @@ +/** + * Social Media Tab Component + * + * Displays and allows editing of social media metadata including: + * - Open Graph tags (Facebook, LinkedIn) + * - Twitter Card tags + * - Social media previews + */ + +import React from 'react'; +import { + Box, + Typography, + TextField, + Paper, + Grid, + IconButton, + Tooltip, + InputAdornment, + Alert, + Card, + CardContent, + Divider, + Chip +} from '@mui/material'; +import { + ContentCopy as CopyIcon, + Check as CheckIcon, + Share as ShareIcon, + Facebook as FacebookIcon, + Twitter as TwitterIcon, + LinkedIn as LinkedInIcon, + Image as ImageIcon, + Link as LinkIcon +} from '@mui/icons-material'; + +interface SocialMediaTabProps { + metadata: any; + onMetadataEdit: (field: string, value: any) => void; + onCopyToClipboard: (text: string, itemId: string) => void; + copiedItems: Set; +} + +export const SocialMediaTab: React.FC = ({ + metadata, + onMetadataEdit, + onCopyToClipboard, + copiedItems +}) => { + const handleTextFieldChange = (field: string) => (event: React.ChangeEvent) => { + onMetadataEdit(field, event.target.value); + }; + + const handleNestedFieldChange = (parentField: string, childField: string) => (event: React.ChangeEvent) => { + const currentValue = metadata[parentField] || {}; + onMetadataEdit(parentField, { + ...currentValue, + [childField]: event.target.value + }); + }; + + const getCharacterCountColor = (current: number, max: number) => { + if (current > max) return 'error'; + if (current > max * 0.9) return 'warning'; + return 'success'; + }; + + const getCharacterCountText = (current: number, max: number) => { + if (current > max) return `${current}/${max} (Too long)`; + if (current > max * 0.9) return `${current}/${max} (Near limit)`; + return `${current}/${max}`; + }; + + const openGraph = metadata.open_graph || {}; + const twitterCard = metadata.twitter_card || {}; + + return ( + + + + Social Media Metadata + + + + {/* Open Graph Section */} + + + + + + + Open Graph Tags + + + + + + + + + OG Title + + + onCopyToClipboard(openGraph.title || '', 'og_title')} + > + {copiedItems.has('og_title') ? : } + + + + + + {getCharacterCountText((openGraph.title || '').length, 60)} + + + ) + }} + /> + + + + + + OG Description + + + onCopyToClipboard(openGraph.description || '', 'og_description')} + > + {copiedItems.has('og_description') ? : } + + + + + + {getCharacterCountText((openGraph.description || '').length, 160)} + + + ) + }} + /> + + + + + + OG Image URL + + + onCopyToClipboard(openGraph.image || '', 'og_image')} + > + {copiedItems.has('og_image') ? : } + + + + + + + ) + }} + /> + + + + + + OG URL + + + onCopyToClipboard(openGraph.url || '', 'og_url')} + > + {copiedItems.has('og_url') ? : } + + + + + + + ) + }} + /> + + + + + Open Graph tags are used by Facebook, LinkedIn, and other social platforms to display rich previews + + + + + {/* Twitter Card Section */} + + + + + + Twitter Card Tags + + + + + + + + + Twitter Title + + + onCopyToClipboard(twitterCard.title || '', 'twitter_title')} + > + {copiedItems.has('twitter_title') ? : } + + + + + + {getCharacterCountText((twitterCard.title || '').length, 70)} + + + ) + }} + /> + + + + + + Twitter Description + + + onCopyToClipboard(twitterCard.description || '', 'twitter_description')} + > + {copiedItems.has('twitter_description') ? : } + + + + + + {getCharacterCountText((twitterCard.description || '').length, 200)} + + + ) + }} + /> + + + + + + Twitter Image URL + + + onCopyToClipboard(twitterCard.image || '', 'twitter_image')} + > + {copiedItems.has('twitter_image') ? : } + + + + + + + ) + }} + /> + + + + + + Twitter Site Handle + + + onCopyToClipboard(twitterCard.site || '', 'twitter_site')} + > + {copiedItems.has('twitter_site') ? : } + + + + + + + ) + }} + /> + + + + + Twitter cards provide rich previews when your content is shared on Twitter/X + + + + + {/* Social Media Preview */} + + + + + Social Media Preview + + + + {/* Facebook Preview */} + + + + + + + Facebook Preview + + + + + {openGraph.title || 'Your Blog Title'} + + + {openGraph.url || 'yourwebsite.com'} + + + {openGraph.description || 'Your meta description will appear here...'} + + + + + + + {/* Twitter Preview */} + + + + + + + Twitter Preview + + + + + {twitterCard.title || 'Your Blog Title'} + + + {twitterCard.site || '@yourwebsite'} + + + {twitterCard.description || 'Your Twitter description will appear here...'} + + + + + + + + + + + ); +}; diff --git a/frontend/src/components/BlogWriter/SEO/MetadataDisplay/StructuredDataTab.tsx b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/StructuredDataTab.tsx new file mode 100644 index 00000000..770c51c0 --- /dev/null +++ b/frontend/src/components/BlogWriter/SEO/MetadataDisplay/StructuredDataTab.tsx @@ -0,0 +1,509 @@ +/** + * Structured Data Tab Component + * + * Displays and allows editing of JSON-LD structured data including: + * - Article schema + * - Author information + * - Publisher details + * - Publication dates + * - Keywords and categories + */ + +import React, { useState } from 'react'; +import { + Box, + Typography, + TextField, + Paper, + Grid, + IconButton, + Tooltip, + InputAdornment, + Alert, + Card, + CardContent, + Divider, + Chip, + Accordion, + AccordionSummary, + AccordionDetails, + Button +} from '@mui/material'; +import { + ContentCopy as CopyIcon, + Check as CheckIcon, + Code as CodeIcon, + Person as PersonIcon, + Business as BusinessIcon, + CalendarToday as CalendarIcon, + ExpandMore as ExpandMoreIcon, + Visibility as VisibilityIcon, + Edit as EditIcon +} from '@mui/icons-material'; + +interface StructuredDataTabProps { + metadata: any; + onMetadataEdit: (field: string, value: any) => void; + onCopyToClipboard: (text: string, itemId: string) => void; + copiedItems: Set; +} + +export const StructuredDataTab: React.FC = ({ + metadata, + onMetadataEdit, + onCopyToClipboard, + copiedItems +}) => { + const [showRawJson, setShowRawJson] = useState(false); + + const handleTextFieldChange = (field: string) => (event: React.ChangeEvent) => { + onMetadataEdit(field, event.target.value); + }; + + const handleNestedFieldChange = (parentField: string, childField: string) => (event: React.ChangeEvent) => { + const currentValue = metadata[parentField] || {}; + onMetadataEdit(parentField, { + ...currentValue, + [childField]: event.target.value + }); + }; + + const handleAuthorFieldChange = (field: string) => (event: React.ChangeEvent) => { + const currentSchema = metadata.json_ld_schema || {}; + const currentAuthor = currentSchema.author || {}; + onMetadataEdit('json_ld_schema', { + ...currentSchema, + author: { + ...currentAuthor, + [field]: event.target.value + } + }); + }; + + const handlePublisherFieldChange = (field: string) => (event: React.ChangeEvent) => { + const currentSchema = metadata.json_ld_schema || {}; + const currentPublisher = currentSchema.publisher || {}; + onMetadataEdit('json_ld_schema', { + ...currentSchema, + publisher: { + ...currentPublisher, + [field]: event.target.value + } + }); + }; + + const handleSchemaFieldChange = (field: string) => (event: React.ChangeEvent) => { + const currentSchema = metadata.json_ld_schema || {}; + onMetadataEdit('json_ld_schema', { + ...currentSchema, + [field]: event.target.value + }); + }; + + const getJsonLdSchema = () => { + const schema = metadata.json_ld_schema || {}; + return JSON.stringify(schema, null, 2); + }; + + const copyJsonLdSchema = () => { + onCopyToClipboard(getJsonLdSchema(), 'json_ld_schema'); + }; + + const jsonLdSchema = metadata.json_ld_schema || {}; + const author = jsonLdSchema.author || {}; + const publisher = jsonLdSchema.publisher || {}; + + return ( + + + + Structured Data (JSON-LD) + + + + {/* Article Information */} + + + + + Article Schema + + + + + + + Headline + + + onCopyToClipboard(jsonLdSchema.headline || '', 'schema_headline')} + > + {copiedItems.has('schema_headline') ? : } + + + + + + + + + + Description + + + onCopyToClipboard(jsonLdSchema.description || '', 'schema_description')} + > + {copiedItems.has('schema_description') ? : } + + + + + + + + + + Main Entity URL + + + onCopyToClipboard(jsonLdSchema.mainEntityOfPage || '', 'schema_url')} + > + {copiedItems.has('schema_url') ? : } + + + + + + + ) + }} + /> + + + + + + Word Count + + + onCopyToClipboard(jsonLdSchema.wordCount?.toString() || '', 'schema_wordcount')} + > + {copiedItems.has('schema_wordcount') ? : } + + + + words + }} + /> + + + + + + {/* Author Information */} + + + + + Author Information + + + + + + + Author Name + + + onCopyToClipboard(author.name || '', 'author_name')} + > + {copiedItems.has('author_name') ? : } + + + + + + + + + + Author Type + + + onCopyToClipboard(author['@type'] || '', 'author_type')} + > + {copiedItems.has('author_type') ? : } + + + + + + + + + + {/* Publisher Information */} + + + + + Publisher Information + + + + + + + Publisher Name + + + onCopyToClipboard(publisher.name || '', 'publisher_name')} + > + {copiedItems.has('publisher_name') ? : } + + + + + + + + + + Publisher Logo + + + onCopyToClipboard(publisher.logo || '', 'publisher_logo')} + > + {copiedItems.has('publisher_logo') ? : } + + + + + + + + + + {/* Publication Dates */} + + + + + Publication Dates + + + + + + + Date Published + + + onCopyToClipboard(jsonLdSchema.datePublished || '', 'date_published')} + > + {copiedItems.has('date_published') ? : } + + + + + + + + + + Date Modified + + + onCopyToClipboard(jsonLdSchema.dateModified || '', 'date_modified')} + > + {copiedItems.has('date_modified') ? : } + + + + + + + + + + {/* Keywords */} + + + + + Keywords & Categories + + + + + + + Keywords + + + onCopyToClipboard((jsonLdSchema.keywords || []).join(', '), 'schema_keywords')} + > + {copiedItems.has('schema_keywords') ? : } + + + + { + const keywords = e.target.value.split(',').map(k => k.trim()).filter(k => k); + handleSchemaFieldChange('keywords')({ target: { value: keywords } } as any); + }} + placeholder="keyword1, keyword2, keyword3" + helperText="Separate keywords with commas" + /> + + + + + + {/* Raw JSON View */} + + + }> + + + + Raw JSON-LD Schema + + + + + + + + Complete JSON-LD Schema + + + + + + + + + + {/* Information Alert */} + + + + JSON-LD Structured Data: This schema helps search engines understand your content + and may enable rich snippets in search results. The data follows Schema.org Article guidelines. + + + + + + ); +}; diff --git a/frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx b/frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx index 8b184fd2..82356711 100644 --- a/frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx +++ b/frontend/src/components/BlogWriter/SEO/StructureAnalysis.tsx @@ -11,7 +11,8 @@ import { Typography, Paper, Grid, - Chip + Chip, + Tooltip } from '@mui/material'; import { BarChart @@ -29,6 +30,15 @@ interface StructureAnalysisProps { structure_score: number; recommendations: string[]; }; + content_quality?: { + word_count: number; + unique_words: number; + vocabulary_diversity: number; + transition_words_used: number; + content_depth_score: number; + flow_score: number; + recommendations: string[]; + }; heading_structure?: { h1_count: number; h2_count: number; @@ -43,11 +53,6 @@ interface StructureAnalysisProps { } export const StructureAnalysis: React.FC = ({ detailedAnalysis }) => { - // Debug logging - console.log('šŸ—ļø StructureAnalysis received data:', detailedAnalysis); - console.log('šŸ“Š Content Structure:', detailedAnalysis?.content_structure); - console.log('šŸ“‹ Heading Structure:', detailedAnalysis?.heading_structure); - return ( @@ -64,30 +69,113 @@ export const StructureAnalysis: React.FC = ({ detailedAn Structure Overview - - Total Sections - - {detailedAnalysis?.content_structure?.total_sections || 'N/A'} - - - - Total Paragraphs - - {detailedAnalysis?.content_structure?.total_paragraphs || 'N/A'} - - - - Total Sentences - - {detailedAnalysis?.content_structure?.total_sentences || 'N/A'} - - - - Structure Score - - {detailedAnalysis?.content_structure?.structure_score || 'N/A'} - - + + + Total Sections + + + Number of main content sections (H2 headings) in your blog post. + + + Optimal Range: 3-8 sections for most blog posts + + + Why it matters: Good sectioning improves readability and helps search engines understand your content structure. + + + } + arrow + > + + Total Sections + + {detailedAnalysis?.content_structure?.total_sections || 'N/A'} + + + + + + + Total Paragraphs + + + Number of paragraphs in your content (excluding headings). + + + Optimal Range: 8-20 paragraphs for most blog posts + + + Why it matters: Appropriate paragraph count indicates good content depth and organization. + + + } + arrow + > + + Total Paragraphs + + {detailedAnalysis?.content_structure?.total_paragraphs || 'N/A'} + + + + + + + Total Sentences + + + Total number of sentences in your content. + + + Optimal Range: 40-100 sentences for most blog posts + + + Why it matters: Sentence count affects readability and content comprehensiveness. + + + } + arrow + > + + Total Sentences + + {detailedAnalysis?.content_structure?.total_sentences || 'N/A'} + + + + + + + Structure Score + + + Overall score (0-100) for your content's structural organization. + + + Scoring Factors: Section count, paragraph count, introduction/conclusion presence + + + Why it matters: Well-structured content ranks better and provides better user experience. + + + } + arrow + > + + Structure Score + + {detailedAnalysis?.content_structure?.structure_score || 'N/A'} + + + @@ -99,35 +187,296 @@ export const StructureAnalysis: React.FC = ({ detailedAn Content Elements - - Has Introduction - - - - Has Conclusion - - - - Has Call to Action - - + + + Introduction Section + + + Whether your content has a clear introduction that sets context and expectations. + + + Why it matters: Introductions help readers understand what they'll learn and improve engagement. + + + SEO Impact: Clear introductions help search engines understand your content's purpose. + + + } + arrow + > + + Has Introduction + + + + + + + Conclusion Section + + + Whether your content has a clear conclusion that summarizes key points. + + + Why it matters: Conclusions help readers retain information and provide closure. + + + SEO Impact: Good conclusions can improve time on page and reduce bounce rate. + + + } + arrow + > + + Has Conclusion + + + + + + + Call to Action + + + Whether your content includes a clear call to action for readers. + + + Why it matters: CTAs guide readers to take desired actions and improve conversion rates. + + + SEO Impact: Strong CTAs can improve user engagement metrics. + + + } + arrow + > + + Has Call to Action + + + + {/* Content Quality Metrics */} + + + + + Content Quality Metrics + + + + + + Word Count + + + Total number of words in your content. + + + Optimal Range: 800-2000 words for most blog posts + + + Why it matters: Longer content typically ranks better and provides more value to readers. + + + } + arrow + > + + + Word Count + + + {detailedAnalysis?.content_quality?.word_count || 'N/A'} + + + + + + + + + Vocabulary Diversity + + + Ratio of unique words to total words, indicating content variety. + + + Optimal Range: 0.4-0.7 (40-70% unique words) + + + Why it matters: Higher diversity indicates richer, more engaging content. + + + } + arrow + > + + + Vocabulary Diversity + + + {detailedAnalysis?.content_quality?.vocabulary_diversity ? + (detailedAnalysis.content_quality.vocabulary_diversity * 100).toFixed(1) + '%' : 'N/A'} + + + + + + + + + Content Depth Score + + + Score (0-100) indicating how comprehensive and detailed your content is. + + + Scoring Factors: Word count, section depth, information density + + + Why it matters: Deeper content provides more value and ranks better in search results. + + + } + arrow + > + + + Content Depth Score + + + {detailedAnalysis?.content_quality?.content_depth_score || 'N/A'} + + + + + + + + + Flow Score + + + Score (0-100) indicating how well your content flows from one idea to the next. + + + Scoring Factors: Transition words, sentence variety, logical progression + + + Why it matters: Good flow improves readability and keeps readers engaged. + + + } + arrow + > + + + Flow Score + + + {detailedAnalysis?.content_quality?.flow_score || 'N/A'} + + + + + + + + + Transition Words + + + Number of transition words used to connect ideas and improve flow. + + + Optimal Range: 5-15 transition words for most blog posts + + + Why it matters: Transition words improve readability and help readers follow your logic. + + + } + arrow + > + + + Transition Words + + + {detailedAnalysis?.content_quality?.transition_words_used || 'N/A'} + + + + + + + + + Unique Words + + + Number of unique words used in your content. + + + Why it matters: More unique words indicate richer vocabulary and better content variety. + + + SEO Impact: Diverse vocabulary can help with semantic SEO and topic coverage. + + + } + arrow + > + + + Unique Words + + + {detailedAnalysis?.content_quality?.unique_words || 'N/A'} + + + + + + + + + {/* Heading Structure */} @@ -184,10 +533,78 @@ export const StructureAnalysis: React.FC = ({ detailedAn - - Heading Hierarchy Score: {detailedAnalysis?.heading_structure?.heading_hierarchy_score || 'N/A'} - + + + Heading Hierarchy Score + + + Score (0-100) indicating how well your heading structure follows SEO best practices. + + + Scoring Factors: H1 presence, logical hierarchy, keyword usage in headings + + + Why it matters: Good heading structure helps search engines understand your content and improves readability. + + + } + arrow + > + + Heading Hierarchy Score: {detailedAnalysis?.heading_structure?.heading_hierarchy_score || 'N/A'} + + + + {/* Structure Recommendations */} + {detailedAnalysis?.content_structure?.recommendations && detailedAnalysis.content_structure.recommendations.length > 0 && ( + + + Structure Recommendations + + + {detailedAnalysis.content_structure.recommendations.map((recommendation: string, index: number) => ( + + • {recommendation} + + ))} + + + )} + + {/* Heading Recommendations */} + {detailedAnalysis?.heading_structure?.recommendations && detailedAnalysis.heading_structure.recommendations.length > 0 && ( + + + Heading Recommendations + + + {detailedAnalysis.heading_structure.recommendations.map((recommendation: string, index: number) => ( + + • {recommendation} + + ))} + + + )} + + {/* Content Quality Recommendations */} + {detailedAnalysis?.content_quality?.recommendations && detailedAnalysis.content_quality.recommendations.length > 0 && ( + + + Content Quality Recommendations + + + {detailedAnalysis.content_quality.recommendations.map((recommendation: string, index: number) => ( + + • {recommendation} + + ))} + + + )} diff --git a/frontend/src/components/BlogWriter/SEOAnalysisModal.tsx b/frontend/src/components/BlogWriter/SEOAnalysisModal.tsx index c91c6ced..c597e626 100644 --- a/frontend/src/components/BlogWriter/SEOAnalysisModal.tsx +++ b/frontend/src/components/BlogWriter/SEOAnalysisModal.tsx @@ -141,6 +141,7 @@ interface SEOAnalysisModalProps { isOpen: boolean; onClose: () => void; blogContent: string; + blogTitle?: string; researchData: any; onApplyRecommendations?: (recommendations: any[]) => void; } @@ -149,6 +150,7 @@ export const SEOAnalysisModal: React.FC = ({ isOpen, onClose, blogContent, + blogTitle, researchData, onApplyRecommendations }) => { @@ -192,6 +194,7 @@ export const SEOAnalysisModal: React.FC = ({ }, body: JSON.stringify({ blog_content: blogContent, + blog_title: blogTitle, research_data: researchData }) }); @@ -202,12 +205,6 @@ export const SEOAnalysisModal: React.FC = ({ const result = await response.json(); console.log('šŸ” Backend SEO Analysis Response:', result); - console.log('šŸ“Š Category Scores:', result.category_scores); - console.log('šŸ’” Recommendations:', result.actionable_recommendations); - console.log('šŸ” Visualization Data:', result.visualization_data); - console.log('šŸ“ Detailed Analysis:', result.detailed_analysis); - console.log('šŸ—ļø Content Structure:', result.detailed_analysis?.content_structure); - console.log('šŸ“‹ Heading Structure:', result.detailed_analysis?.heading_structure); // Convert API response to frontend format - fail fast if data is missing if (!result.success) { @@ -610,9 +607,9 @@ export const SEOAnalysisModal: React.FC = ({ )} - {tabValue === 'keywords' && ( - - )} + {tabValue === 'keywords' && ( + + )} {tabValue === 'readability' && ( = ({ )} {tabValue === 'structure' && ( - + analysisResult ? ( + + ) : ( + + + Loading structure analysis... + + + ) )} {tabValue === 'insights' && ( diff --git a/frontend/src/components/BlogWriter/SEOMetadataModal.tsx b/frontend/src/components/BlogWriter/SEOMetadataModal.tsx new file mode 100644 index 00000000..f9ec2868 --- /dev/null +++ b/frontend/src/components/BlogWriter/SEOMetadataModal.tsx @@ -0,0 +1,377 @@ +/** + * SEO Metadata Modal Component + * + * Comprehensive SEO metadata generation and editing interface with: + * - Tabbed interface for different metadata types + * - Live preview of social media cards + * - Character counters and validation + * - Copy-to-clipboard functionality + * - Integration with backend metadata generation + */ + +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Box, + Typography, + Tabs, + Tab, + Paper, + CircularProgress, + Alert, + IconButton, + Tooltip, + Chip, + Grid, + Card, + CardContent, + Divider, + TextField, + InputAdornment +} from '@mui/material'; +import { + Close as CloseIcon, + ContentCopy as CopyIcon, + Check as CheckIcon, + Preview as PreviewIcon, + Search as SearchIcon, + Share as ShareIcon, + Code as CodeIcon, + Tag as TagIcon, + Refresh as RefreshIcon +} from '@mui/icons-material'; + +// Import metadata display components +import { CoreMetadataTab } from './SEO/MetadataDisplay/CoreMetadataTab'; +import { SocialMediaTab } from './SEO/MetadataDisplay/SocialMediaTab'; +import { StructuredDataTab } from './SEO/MetadataDisplay/StructuredDataTab'; +import { PreviewCard } from './SEO/MetadataDisplay/PreviewCard'; + +interface SEOMetadataModalProps { + isOpen: boolean; + onClose: () => void; + blogContent: string; + blogTitle: string; + researchData: any; + onMetadataGenerated: (metadata: any) => void; +} + +interface SEOMetadataResult { + success: boolean; + seo_title?: string; + meta_description?: string; + url_slug?: string; + blog_tags?: string[]; + blog_categories?: string[]; + social_hashtags?: string[]; + open_graph?: any; + twitter_card?: any; + json_ld_schema?: any; + canonical_url?: string; + reading_time?: number; + focus_keyword?: string; + generated_at?: string; + optimization_score?: number; + error?: string; +} + +export const SEOMetadataModal: React.FC = ({ + isOpen, + onClose, + blogContent, + blogTitle, + researchData, + onMetadataGenerated +}) => { + const [isGenerating, setIsGenerating] = useState(false); + const [metadataResult, setMetadataResult] = useState(null); + const [error, setError] = useState(null); + const [tabValue, setTabValue] = useState('core'); + const [copiedItems, setCopiedItems] = useState>(new Set()); + const [editableMetadata, setEditableMetadata] = useState(null); + + // Debug logging + useEffect(() => { + console.log('šŸ” SEOMetadataModal render:', { + isOpen, + blogContent: blogContent?.length, + blogTitle, + researchData: !!researchData + }); + }, [isOpen, blogContent, blogTitle, researchData]); + + const generateMetadata = async () => { + try { + setIsGenerating(true); + setError(null); + setMetadataResult(null); + + console.log('šŸš€ Starting SEO metadata generation...'); + + // Make API call to generate metadata + const response = await fetch('/api/blog/seo/metadata', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: blogContent, + title: blogTitle, + research_data: researchData + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('āœ… SEO metadata generation response:', result); + + if (!result.success) { + throw new Error(result.error || 'Metadata generation failed'); + } + + setMetadataResult(result); + setEditableMetadata(result); + console.log('šŸ“Š Metadata result set:', result); + + } catch (err) { + console.error('āŒ SEO metadata generation failed:', err); + setError(err instanceof Error ? err.message : 'Failed to generate SEO metadata'); + } finally { + setIsGenerating(false); + } + }; + + const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { + setTabValue(newValue); + }; + + const handleCopyToClipboard = async (text: string, itemId: string) => { + try { + await navigator.clipboard.writeText(text); + setCopiedItems(prev => new Set([...prev, itemId])); + setTimeout(() => { + setCopiedItems(prev => { + const newSet = new Set(prev); + newSet.delete(itemId); + return newSet; + }); + }, 2000); + } catch (err) { + console.error('Failed to copy to clipboard:', err); + } + }; + + const handleMetadataEdit = (field: string, value: any) => { + if (editableMetadata) { + setEditableMetadata(prev => ({ + ...prev!, + [field]: value + })); + } + }; + + const handleApplyMetadata = () => { + if (editableMetadata) { + onMetadataGenerated(editableMetadata); + onClose(); + } + }; + + const getTabIcon = (tabValue: string) => { + switch (tabValue) { + case 'core': return ; + case 'social': return ; + case 'structured': return ; + case 'preview': return ; + default: return ; + } + }; + + const getTabLabel = (tabValue: string) => { + switch (tabValue) { + case 'core': return 'Core SEO'; + case 'social': return 'Social Media'; + case 'structured': return 'Structured Data'; + case 'preview': return 'Preview'; + default: return 'Metadata'; + } + }; + + return ( + + + + + + SEO Metadata Generator + + {metadataResult && ( + = 80 ? 'success' : + metadataResult.optimization_score && metadataResult.optimization_score >= 60 ? 'warning' : 'error'} + size="small" + /> + )} + + + + + + + + {!metadataResult && !isGenerating && ( + + + Generate Comprehensive SEO Metadata + + + Create optimized titles, descriptions, Open Graph tags, Twitter cards, and structured data for your blog post. + + + + )} + + {isGenerating && ( + + + + Generating SEO Metadata... + + + Creating optimized titles, descriptions, and social media tags + + + )} + + {error && ( + + + {error} + + + + )} + + {metadataResult && ( + + {/* Tabs */} + + + {['core', 'social', 'structured', 'preview'].map((tab) => ( + + ))} + + + + {/* Tab Content */} + + {tabValue === 'core' && ( + + )} + + {tabValue === 'social' && ( + + )} + + {tabValue === 'structured' && ( + + )} + + {tabValue === 'preview' && ( + + )} + + + )} + + + {metadataResult && ( + + + + + )} + + ); +}; diff --git a/frontend/src/components/BlogWriter/WYSIWYG/BlogEditor.tsx b/frontend/src/components/BlogWriter/WYSIWYG/BlogEditor.tsx index 89e0f4ea..df178d05 100644 --- a/frontend/src/components/BlogWriter/WYSIWYG/BlogEditor.tsx +++ b/frontend/src/components/BlogWriter/WYSIWYG/BlogEditor.tsx @@ -114,9 +114,9 @@ const BlogEditor: React.FC = ({

{