Blog SEO Analysis Modal - Updated with SEO Metadata Generator, Core Metadata Tab, and Metadata Display Components

This commit is contained in:
ajaysi
2025-09-23 16:21:09 +05:30
parent 12119d418b
commit a91677782e
16 changed files with 3433 additions and 89 deletions

View File

@@ -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', '')
)

View File

@@ -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):

View File

@@ -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."""

View File

@@ -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

View 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

View 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()

View 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())

View File

@@ -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 = () => {
<div>
{outlineConfirmed ? (
/* WYSIWYG Editor - Show when outline is confirmed */
<BlogEditor
<BlogEditor
outline={outline}
research={research}
initialTitle={selectedTitle}
initialTitle={selectedTitle || (typeof window !== 'undefined' ? localStorage.getItem('blog_selected_title') : '') || 'Your Amazing Blog Title'}
titleOptions={titleOptions}
researchTitles={researchTitles}
aiGeneratedTitles={aiGeneratedTitles}
@@ -586,12 +624,27 @@ Available tools:
isOpen={isSEOAnalysisModalOpen}
onClose={() => setIsSEOAnalysisModalOpen(false)}
blogContent={buildFullMarkdown()}
blogTitle={selectedTitle}
researchData={research}
onApplyRecommendations={(recommendations) => {
console.log('Applying SEO recommendations:', recommendations);
// TODO: Implement recommendation application logic
}}
/>
{/* SEO Metadata Modal */}
<SEOMetadataModal
isOpen={isSEOMetadataModalOpen}
onClose={() => setIsSEOMetadataModalOpen(false)}
blogContent={buildFullMarkdown()}
blogTitle={selectedTitle}
researchData={research}
onMetadataGenerated={(metadata) => {
console.log('SEO metadata generated:', metadata);
setSeoMetadata(metadata);
// TODO: Implement metadata application logic
}}
/>
</div>
);
};

View File

@@ -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<string>;
}
export const CoreMetadataTab: React.FC<CoreMetadataTabProps> = ({
metadata,
onMetadataEdit,
onCopyToClipboard,
copiedItems
}) => {
const handleTextFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
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 (
<Box>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon sx={{ color: 'primary.main' }} />
Core SEO Metadata
</Typography>
<Grid container spacing={3}>
{/* SEO Title */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon sx={{ fontSize: 20 }} />
SEO Title
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(metadata.seo_title || '', 'seo_title')}
>
{copiedItems.has('seo_title') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={metadata.seo_title || ''}
onChange={handleTextFieldChange('seo_title')}
placeholder="Enter SEO-optimized title (50-60 characters)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((metadata.seo_title || '').length, 60)}
>
{getCharacterCountText((metadata.seo_title || '').length, 60)}
</Typography>
</InputAdornment>
)
}}
/>
<Alert severity="info" sx={{ mt: 1 }}>
Include your primary keyword and make it compelling for clicks
</Alert>
</Paper>
</Grid>
{/* Meta Description */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon sx={{ fontSize: 20 }} />
Meta Description
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(metadata.meta_description || '', 'meta_description')}
>
{copiedItems.has('meta_description') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={3}
value={metadata.meta_description || ''}
onChange={handleTextFieldChange('meta_description')}
placeholder="Enter compelling meta description (150-160 characters)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((metadata.meta_description || '').length, 160)}
>
{getCharacterCountText((metadata.meta_description || '').length, 160)}
</Typography>
</InputAdornment>
)
}}
/>
<Alert severity="info" sx={{ mt: 1 }}>
Include a call-to-action and your primary keyword
</Alert>
</Paper>
</Grid>
{/* URL Slug */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<LinkIcon sx={{ fontSize: 20 }} />
URL Slug
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(metadata.url_slug || '', 'url_slug')}
>
{copiedItems.has('url_slug') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={metadata.url_slug || ''}
onChange={handleTextFieldChange('url_slug')}
placeholder="seo-friendly-url-slug"
helperText="Use lowercase letters, numbers, and hyphens only"
/>
</Paper>
</Grid>
{/* Focus Keyword */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<TrendingUpIcon sx={{ fontSize: 20 }} />
Focus Keyword
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(metadata.focus_keyword || '', 'focus_keyword')}
>
{copiedItems.has('focus_keyword') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={metadata.focus_keyword || ''}
onChange={handleTextFieldChange('focus_keyword')}
placeholder="primary-keyword"
helperText="Your main SEO keyword for this post"
/>
</Paper>
</Grid>
{/* Blog Tags */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<TagIcon sx={{ fontSize: 20 }} />
Blog Tags
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard((metadata.blog_tags || []).join(', '), 'blog_tags')}
>
{copiedItems.has('blog_tags') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<FormControl fullWidth>
<InputLabel>Tags</InputLabel>
<Select
multiple
value={metadata.blog_tags || []}
onChange={handleTagsChange('blog_tags')}
input={<OutlinedInput label="Tags" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value: string) => (
<Chip key={value} label={value} size="small" />
))}
</Box>
)}
>
{(metadata.blog_tags || []).map((tag: string) => (
<MenuItem key={tag} value={tag}>
{tag}
</MenuItem>
))}
</Select>
</FormControl>
<Alert severity="info" sx={{ mt: 1 }}>
Add relevant tags for better categorization and discoverability
</Alert>
</Paper>
</Grid>
{/* Blog Categories */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<CategoryIcon sx={{ fontSize: 20 }} />
Blog Categories
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard((metadata.blog_categories || []).join(', '), 'blog_categories')}
>
{copiedItems.has('blog_categories') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<FormControl fullWidth>
<InputLabel>Categories</InputLabel>
<Select
multiple
value={metadata.blog_categories || []}
onChange={handleTagsChange('blog_categories')}
input={<OutlinedInput label="Categories" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value: string) => (
<Chip key={value} label={value} size="small" color="primary" />
))}
</Box>
)}
>
{(metadata.blog_categories || []).map((category: string) => (
<MenuItem key={category} value={category}>
{category}
</MenuItem>
))}
</Select>
</FormControl>
<Alert severity="info" sx={{ mt: 1 }}>
Select 2-3 primary categories for your content
</Alert>
</Paper>
</Grid>
{/* Social Hashtags */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<TagIcon sx={{ fontSize: 20 }} />
Social Hashtags
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard((metadata.social_hashtags || []).join(' '), 'social_hashtags')}
>
{copiedItems.has('social_hashtags') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<FormControl fullWidth>
<InputLabel>Hashtags</InputLabel>
<Select
multiple
value={metadata.social_hashtags || []}
onChange={handleTagsChange('social_hashtags')}
input={<OutlinedInput label="Hashtags" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value: string) => (
<Chip key={value} label={value} size="small" color="secondary" />
))}
</Box>
)}
>
{(metadata.social_hashtags || []).map((hashtag: string) => (
<MenuItem key={hashtag} value={hashtag}>
{hashtag}
</MenuItem>
))}
</Select>
</FormControl>
<Alert severity="info" sx={{ mt: 1 }}>
Include # symbol for social media platforms
</Alert>
</Paper>
</Grid>
{/* Reading Time */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, display: 'flex', alignItems: 'center', gap: 1 }}>
<ScheduleIcon sx={{ fontSize: 20 }} />
Reading Time
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(`${metadata.reading_time || 0} minutes`, 'reading_time')}
>
{copiedItems.has('reading_time') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
type="number"
value={metadata.reading_time || 0}
onChange={handleTextFieldChange('reading_time')}
placeholder="5"
InputProps={{
endAdornment: <InputAdornment position="end">minutes</InputAdornment>
}}
helperText="Estimated reading time for your content"
/>
</Paper>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -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<PreviewCardProps> = ({
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 (
<Box>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon sx={{ color: 'primary.main' }} />
Live Preview
</Typography>
<Grid container spacing={3}>
{/* Google Search Results Preview */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<GoogleIcon sx={{ color: '#4285F4' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Google Search Results
</Typography>
<Chip label="SERP Preview" size="small" color="primary" />
</Box>
<Card sx={{ border: '1px solid #e0e0e0', boxShadow: 'none' }}>
<CardContent sx={{ p: 2 }}>
{/* URL */}
<Typography variant="caption" sx={{ color: '#1a0dab', mb: 1, display: 'block' }}>
{metadata.canonical_url || 'https://yourwebsite.com/blog-post'}
</Typography>
{/* Title */}
<Typography
variant="h6"
sx={{
color: '#1a0dab',
fontWeight: 400,
fontSize: '1.1rem',
lineHeight: 1.3,
mb: 1,
cursor: 'pointer',
'&:hover': { textDecoration: 'underline' }
}}
>
{metadata.seo_title || blogTitle}
</Typography>
{/* Description */}
<Typography variant="body2" sx={{ color: '#4d5156', lineHeight: 1.4, mb: 1 }}>
{metadata.meta_description || 'Your meta description will appear here in Google search results...'}
</Typography>
{/* Additional Info */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{getCurrentDate()}
</Typography>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
</Typography>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{metadata.reading_time || 5} min read
</Typography>
{metadata.blog_tags && metadata.blog_tags.length > 0 && (
<>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
</Typography>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{metadata.blog_tags.slice(0, 2).join(', ')}
</Typography>
</>
)}
</Box>
</CardContent>
</Card>
<Alert severity="info" sx={{ mt: 2 }}>
This is how your blog post will appear in Google search results
</Alert>
</Paper>
</Grid>
{/* Social Media Previews */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<FacebookIcon sx={{ color: '#1877F2' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Facebook Preview
</Typography>
<Chip label="Open Graph" size="small" color="primary" />
</Box>
<Card sx={{ border: '1px solid #e0e0e0', boxShadow: 'none', maxWidth: 400 }}>
<CardContent sx={{ p: 0 }}>
{/* Image placeholder */}
<Box sx={{
height: 200,
bgcolor: '#f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderBottom: '1px solid #e0e0e0'
}}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{metadata.open_graph?.image ? 'Image loaded' : 'No image set'}
</Typography>
</Box>
<Box sx={{ p: 2 }}>
{/* URL */}
<Typography variant="caption" sx={{ color: '#65676b', mb: 1, display: 'block' }}>
{metadata.canonical_url || 'yourwebsite.com'}
</Typography>
{/* Title */}
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1, lineHeight: 1.3 }}>
{metadata.open_graph?.title || metadata.seo_title || blogTitle}
</Typography>
{/* Description */}
<Typography variant="body2" sx={{ color: '#65676b', lineHeight: 1.4 }}>
{metadata.open_graph?.description || metadata.meta_description || 'Your description will appear here...'}
</Typography>
</Box>
</CardContent>
</Card>
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<TwitterIcon sx={{ color: '#1DA1F2' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Twitter Preview
</Typography>
<Chip label="Twitter Card" size="small" color="info" />
</Box>
<Card sx={{ border: '1px solid #e0e0e0', boxShadow: 'none', maxWidth: 400 }}>
<CardContent sx={{ p: 0 }}>
{/* Image placeholder */}
<Box sx={{
height: 200,
bgcolor: '#f5f5f5',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderBottom: '1px solid #e0e0e0'
}}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{metadata.twitter_card?.image ? 'Image loaded' : 'No image set'}
</Typography>
</Box>
<Box sx={{ p: 2 }}>
{/* URL */}
<Typography variant="caption" sx={{ color: '#536471', mb: 1, display: 'block' }}>
{metadata.canonical_url || 'yourwebsite.com'}
</Typography>
{/* Title */}
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1, lineHeight: 1.3 }}>
{metadata.twitter_card?.title || metadata.seo_title || blogTitle}
</Typography>
{/* Description */}
<Typography variant="body2" sx={{ color: '#536471', lineHeight: 1.4 }}>
{metadata.twitter_card?.description || metadata.meta_description || 'Your description will appear here...'}
</Typography>
{/* Twitter handle */}
{metadata.twitter_card?.site && (
<Typography variant="caption" sx={{ color: '#536471', mt: 1, display: 'block' }}>
{metadata.twitter_card.site}
</Typography>
)}
</Box>
</CardContent>
</Card>
</Paper>
</Grid>
{/* Rich Snippets Preview */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<CodeIcon sx={{ color: '#34A853' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Rich Snippets Preview
</Typography>
<Chip label="JSON-LD Schema" size="small" color="success" />
</Box>
<Card sx={{ border: '1px solid #e0e0e0', boxShadow: 'none' }}>
<CardContent sx={{ p: 2 }}>
{/* Article Schema Preview */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{metadata.json_ld_schema?.headline || metadata.seo_title || blogTitle}
</Typography>
<Chip label="Article" size="small" color="success" />
</Box>
<Typography variant="body2" sx={{ color: '#4d5156', mb: 2 }}>
{metadata.json_ld_schema?.description || metadata.meta_description || 'Article description...'}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
{metadata.json_ld_schema?.author?.name && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
By {metadata.json_ld_schema.author.name}
</Typography>
</Box>
)}
{metadata.json_ld_schema?.datePublished && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{new Date(metadata.json_ld_schema.datePublished).toLocaleDateString()}
</Typography>
</Box>
)}
{metadata.reading_time && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{metadata.reading_time} min read
</Typography>
</Box>
)}
{metadata.json_ld_schema?.wordCount && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="caption" sx={{ color: '#4d5156' }}>
{metadata.json_ld_schema.wordCount} words
</Typography>
</Box>
)}
</Box>
{metadata.json_ld_schema?.keywords && metadata.json_ld_schema.keywords.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="caption" sx={{ color: '#4d5156', display: 'block', mb: 1 }}>
Keywords:
</Typography>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
{metadata.json_ld_schema.keywords.slice(0, 5).map((keyword: string, index: number) => (
<Chip key={index} label={keyword} size="small" variant="outlined" />
))}
</Box>
</Box>
)}
</CardContent>
</Card>
<Alert severity="success" sx={{ mt: 2 }}>
Rich snippets help search engines understand your content and may display additional information in search results
</Alert>
</Paper>
</Grid>
{/* Metadata Summary */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<SearchIcon />
Metadata Summary
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'rgba(76, 175, 80, 0.1)', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'success.main' }}>
{metadata.optimization_score || 0}%
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Optimization Score
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'rgba(33, 150, 243, 0.1)', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'primary.main' }}>
{metadata.reading_time || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Reading Time (min)
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'rgba(156, 39, 176, 0.1)', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'secondary.main' }}>
{metadata.blog_tags?.length || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Tags
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Box sx={{ textAlign: 'center', p: 2, bgcolor: 'rgba(255, 152, 0, 0.1)', borderRadius: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 600, color: 'warning.main' }}>
{metadata.blog_categories?.length || 0}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Categories
</Typography>
</Box>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -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<string>;
}
export const SocialMediaTab: React.FC<SocialMediaTabProps> = ({
metadata,
onMetadataEdit,
onCopyToClipboard,
copiedItems
}) => {
const handleTextFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
onMetadataEdit(field, event.target.value);
};
const handleNestedFieldChange = (parentField: string, childField: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
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 (
<Box>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<ShareIcon sx={{ color: 'primary.main' }} />
Social Media Metadata
</Typography>
<Grid container spacing={3}>
{/* Open Graph Section */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<FacebookIcon sx={{ color: '#1877F2' }} />
<LinkedInIcon sx={{ color: '#0077B5' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Open Graph Tags
</Typography>
<Chip label="Facebook & LinkedIn" size="small" color="primary" />
</Box>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OG Title
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(openGraph.title || '', 'og_title')}
>
{copiedItems.has('og_title') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={openGraph.title || ''}
onChange={handleNestedFieldChange('open_graph', 'title')}
placeholder="Open Graph title (60 characters max)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((openGraph.title || '').length, 60)}
>
{getCharacterCountText((openGraph.title || '').length, 60)}
</Typography>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OG Description
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(openGraph.description || '', 'og_description')}
>
{copiedItems.has('og_description') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={openGraph.description || ''}
onChange={handleNestedFieldChange('open_graph', 'description')}
placeholder="Open Graph description (160 characters max)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((openGraph.description || '').length, 160)}
>
{getCharacterCountText((openGraph.description || '').length, 160)}
</Typography>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OG Image URL
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(openGraph.image || '', 'og_image')}
>
{copiedItems.has('og_image') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={openGraph.image || ''}
onChange={handleNestedFieldChange('open_graph', 'image')}
placeholder="https://example.com/image.jpg"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<ImageIcon />
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
OG URL
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(openGraph.url || '', 'og_url')}
>
{copiedItems.has('og_url') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={openGraph.url || ''}
onChange={handleNestedFieldChange('open_graph', 'url')}
placeholder="https://example.com/blog-post"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<LinkIcon />
</InputAdornment>
)
}}
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
Open Graph tags are used by Facebook, LinkedIn, and other social platforms to display rich previews
</Alert>
</Paper>
</Grid>
{/* Twitter Card Section */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
<TwitterIcon sx={{ color: '#1DA1F2' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Twitter Card Tags
</Typography>
<Chip label="Twitter & X" size="small" color="info" />
</Box>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Title
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(twitterCard.title || '', 'twitter_title')}
>
{copiedItems.has('twitter_title') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={twitterCard.title || ''}
onChange={handleNestedFieldChange('twitter_card', 'title')}
placeholder="Twitter card title (70 characters max)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((twitterCard.title || '').length, 70)}
>
{getCharacterCountText((twitterCard.title || '').length, 70)}
</Typography>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Description
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(twitterCard.description || '', 'twitter_description')}
>
{copiedItems.has('twitter_description') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={twitterCard.description || ''}
onChange={handleNestedFieldChange('twitter_card', 'description')}
placeholder="Twitter card description (200 characters max)"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Typography
variant="caption"
color={getCharacterCountColor((twitterCard.description || '').length, 200)}
>
{getCharacterCountText((twitterCard.description || '').length, 200)}
</Typography>
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Image URL
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(twitterCard.image || '', 'twitter_image')}
>
{copiedItems.has('twitter_image') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={twitterCard.image || ''}
onChange={handleNestedFieldChange('twitter_card', 'image')}
placeholder="https://example.com/twitter-image.jpg"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<ImageIcon />
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Site Handle
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(twitterCard.site || '', 'twitter_site')}
>
{copiedItems.has('twitter_site') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={twitterCard.site || ''}
onChange={handleNestedFieldChange('twitter_card', 'site')}
placeholder="@yourwebsite"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<TwitterIcon />
</InputAdornment>
)
}}
/>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 2 }}>
Twitter cards provide rich previews when your content is shared on Twitter/X
</Alert>
</Paper>
</Grid>
{/* Social Media Preview */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}>
<ShareIcon />
Social Media Preview
</Typography>
<Grid container spacing={2}>
{/* Facebook Preview */}
<Grid item xs={12} md={6}>
<Card sx={{ border: '1px solid #e0e0e0' }}>
<CardContent sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<FacebookIcon sx={{ color: '#1877F2' }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Facebook Preview
</Typography>
</Box>
<Box sx={{ border: '1px solid #e0e0e0', borderRadius: 1, p: 2, bgcolor: '#f5f5f5' }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
{openGraph.title || 'Your Blog Title'}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary', mb: 1, display: 'block' }}>
{openGraph.url || 'yourwebsite.com'}
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.875rem' }}>
{openGraph.description || 'Your meta description will appear here...'}
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
{/* Twitter Preview */}
<Grid item xs={12} md={6}>
<Card sx={{ border: '1px solid #e0e0e0' }}>
<CardContent sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<TwitterIcon sx={{ color: '#1DA1F2' }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Twitter Preview
</Typography>
</Box>
<Box sx={{ border: '1px solid #e0e0e0', borderRadius: 1, p: 2, bgcolor: '#f5f5f5' }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
{twitterCard.title || 'Your Blog Title'}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary', mb: 1, display: 'block' }}>
{twitterCard.site || '@yourwebsite'}
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.875rem' }}>
{twitterCard.description || 'Your Twitter description will appear here...'}
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -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<string>;
}
export const StructuredDataTab: React.FC<StructuredDataTabProps> = ({
metadata,
onMetadataEdit,
onCopyToClipboard,
copiedItems
}) => {
const [showRawJson, setShowRawJson] = useState(false);
const handleTextFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
onMetadataEdit(field, event.target.value);
};
const handleNestedFieldChange = (parentField: string, childField: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
const currentValue = metadata[parentField] || {};
onMetadataEdit(parentField, {
...currentValue,
[childField]: event.target.value
});
};
const handleAuthorFieldChange = (field: string) => (event: React.ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 (
<Box>
<Typography variant="h6" sx={{ mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<CodeIcon sx={{ color: 'primary.main' }} />
Structured Data (JSON-LD)
</Typography>
<Grid container spacing={3}>
{/* Article Information */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<CodeIcon />
Article Schema
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Headline
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.headline || '', 'schema_headline')}
>
{copiedItems.has('schema_headline') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={jsonLdSchema.headline || ''}
onChange={handleSchemaFieldChange('headline')}
placeholder="Article headline"
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Description
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.description || '', 'schema_description')}
>
{copiedItems.has('schema_description') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={jsonLdSchema.description || ''}
onChange={handleSchemaFieldChange('description')}
placeholder="Article description"
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Main Entity URL
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.mainEntityOfPage || '', 'schema_url')}
>
{copiedItems.has('schema_url') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={jsonLdSchema.mainEntityOfPage || ''}
onChange={handleSchemaFieldChange('mainEntityOfPage')}
placeholder="https://example.com/blog-post"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<CodeIcon />
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Word Count
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.wordCount?.toString() || '', 'schema_wordcount')}
>
{copiedItems.has('schema_wordcount') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
type="number"
value={jsonLdSchema.wordCount || ''}
onChange={handleSchemaFieldChange('wordCount')}
placeholder="1500"
InputProps={{
endAdornment: <InputAdornment position="end">words</InputAdornment>
}}
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Author Information */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<PersonIcon />
Author Information
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Author Name
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(author.name || '', 'author_name')}
>
{copiedItems.has('author_name') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={author.name || ''}
onChange={handleAuthorFieldChange('name')}
placeholder="Author Name"
/>
</Grid>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Author Type
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(author['@type'] || '', 'author_type')}
>
{copiedItems.has('author_type') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={author['@type'] || ''}
onChange={handleAuthorFieldChange('@type')}
placeholder="Person"
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Publisher Information */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<BusinessIcon />
Publisher Information
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Publisher Name
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(publisher.name || '', 'publisher_name')}
>
{copiedItems.has('publisher_name') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={publisher.name || ''}
onChange={handlePublisherFieldChange('name')}
placeholder="Publisher Name"
/>
</Grid>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Publisher Logo
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(publisher.logo || '', 'publisher_logo')}
>
{copiedItems.has('publisher_logo') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
value={publisher.logo || ''}
onChange={handlePublisherFieldChange('logo')}
placeholder="https://example.com/logo.png"
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Publication Dates */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<CalendarIcon />
Publication Dates
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Date Published
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.datePublished || '', 'date_published')}
>
{copiedItems.has('date_published') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
type="datetime-local"
value={jsonLdSchema.datePublished || ''}
onChange={handleSchemaFieldChange('datePublished')}
InputLabelProps={{ shrink: true }}
/>
</Grid>
<Grid item xs={12} md={6}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Date Modified
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard(jsonLdSchema.dateModified || '', 'date_modified')}
>
{copiedItems.has('date_modified') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
type="datetime-local"
value={jsonLdSchema.dateModified || ''}
onChange={handleSchemaFieldChange('dateModified')}
InputLabelProps={{ shrink: true }}
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Keywords */}
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
<CodeIcon />
Keywords & Categories
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Keywords
</Typography>
<Tooltip title="Copy to clipboard">
<IconButton
size="small"
onClick={() => onCopyToClipboard((jsonLdSchema.keywords || []).join(', '), 'schema_keywords')}
>
{copiedItems.has('schema_keywords') ? <CheckIcon color="success" /> : <CopyIcon />}
</IconButton>
</Tooltip>
</Box>
<TextField
fullWidth
multiline
rows={2}
value={(jsonLdSchema.keywords || []).join(', ')}
onChange={(e) => {
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"
/>
</Grid>
</Grid>
</Paper>
</Grid>
{/* Raw JSON View */}
<Grid item xs={12}>
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CodeIcon />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Raw JSON-LD Schema
</Typography>
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ position: 'relative' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
Complete JSON-LD Schema
</Typography>
<Button
variant="outlined"
size="small"
startIcon={copiedItems.has('json_ld_schema') ? <CheckIcon /> : <CopyIcon />}
onClick={copyJsonLdSchema}
>
{copiedItems.has('json_ld_schema') ? 'Copied!' : 'Copy JSON'}
</Button>
</Box>
<TextField
fullWidth
multiline
rows={15}
value={getJsonLdSchema()}
InputProps={{
readOnly: true,
sx: {
fontFamily: 'monospace',
fontSize: '0.875rem'
}
}}
sx={{
'& .MuiInputBase-input': {
fontFamily: 'monospace',
fontSize: '0.875rem'
}
}}
/>
</Box>
</AccordionDetails>
</Accordion>
</Grid>
{/* Information Alert */}
<Grid item xs={12}>
<Alert severity="info">
<Typography variant="body2">
<strong>JSON-LD Structured Data:</strong> This schema helps search engines understand your content
and may enable rich snippets in search results. The data follows Schema.org Article guidelines.
</Typography>
</Alert>
</Grid>
</Grid>
</Box>
);
};

View File

@@ -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<StructureAnalysisProps> = ({ detailedAnalysis }) => {
// Debug logging
console.log('🏗️ StructureAnalysis received data:', detailedAnalysis);
console.log('📊 Content Structure:', detailedAnalysis?.content_structure);
console.log('📋 Heading Structure:', detailedAnalysis?.heading_structure);
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
@@ -64,30 +69,113 @@ export const StructureAnalysis: React.FC<StructureAnalysisProps> = ({ detailedAn
Structure Overview
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Total Sections</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sections || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Total Paragraphs</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_paragraphs || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Total Sentences</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sentences || 'N/A'}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Structure Score</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.structure_score || 'N/A'}
</Typography>
</Box>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Total Sections
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Number of main content sections (H2 headings) in your blog post.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 3-8 sections for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Good sectioning improves readability and helps search engines understand your content structure.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Total Sections</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sections || 'N/A'}
</Typography>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Total Paragraphs
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Number of paragraphs in your content (excluding headings).
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 8-20 paragraphs for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Appropriate paragraph count indicates good content depth and organization.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Total Paragraphs</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_paragraphs || 'N/A'}
</Typography>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Total Sentences
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Total number of sentences in your content.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 40-100 sentences for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Sentence count affects readability and content comprehensiveness.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Total Sentences</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.total_sentences || 'N/A'}
</Typography>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Structure Score
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Overall score (0-100) for your content's structural organization.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Scoring Factors:</strong> Section count, paragraph count, introduction/conclusion presence
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Well-structured content ranks better and provides better user experience.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Structure Score</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{detailedAnalysis?.content_structure?.structure_score || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Box>
</Paper>
</Grid>
@@ -99,35 +187,296 @@ export const StructureAnalysis: React.FC<StructureAnalysisProps> = ({ detailedAn
Content Elements
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Has Introduction</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_introduction ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_introduction ? 'success' : 'error'}
size="small"
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Has Conclusion</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_conclusion ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_conclusion ? 'success' : 'error'}
size="small"
/>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2">Has Call to Action</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_call_to_action ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_call_to_action ? 'success' : 'error'}
size="small"
/>
</Box>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Introduction Section
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Whether your content has a clear introduction that sets context and expectations.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Why it matters:</strong> Introductions help readers understand what they'll learn and improve engagement.
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>SEO Impact:</strong> Clear introductions help search engines understand your content's purpose.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Has Introduction</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_introduction ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_introduction ? 'success' : 'error'}
size="small"
/>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Conclusion Section
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Whether your content has a clear conclusion that summarizes key points.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Why it matters:</strong> Conclusions help readers retain information and provide closure.
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>SEO Impact:</strong> Good conclusions can improve time on page and reduce bounce rate.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Has Conclusion</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_conclusion ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_conclusion ? 'success' : 'error'}
size="small"
/>
</Box>
</Tooltip>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Call to Action
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Whether your content includes a clear call to action for readers.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Why it matters:</strong> CTAs guide readers to take desired actions and improve conversion rates.
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>SEO Impact:</strong> Strong CTAs can improve user engagement metrics.
</Typography>
</Box>
}
arrow
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'help' }}>
<Typography variant="body2">Has Call to Action</Typography>
<Chip
label={detailedAnalysis?.content_structure?.has_call_to_action ? 'Yes' : 'No'}
color={detailedAnalysis?.content_structure?.has_call_to_action ? 'success' : 'error'}
size="small"
/>
</Box>
</Tooltip>
</Box>
</Paper>
</Grid>
</Grid>
{/* Content Quality Metrics */}
<Grid container spacing={3} sx={{ mt: 2 }}>
<Grid item xs={12}>
<Paper sx={{ p: 3, background: 'rgba(255,255,255,0.8)', border: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Content Quality Metrics
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Word Count
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Total number of words in your content.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 800-2000 words for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Longer content typically ranks better and provides more value to readers.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(76, 175, 80, 0.1)', borderRadius: 2, border: '1px solid rgba(76, 175, 80, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'success.main', mb: 1 }}>
Word Count
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.word_count || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Vocabulary Diversity
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Ratio of unique words to total words, indicating content variety.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 0.4-0.7 (40-70% unique words)
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Higher diversity indicates richer, more engaging content.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(33, 150, 243, 0.1)', borderRadius: 2, border: '1px solid rgba(33, 150, 243, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'primary.main', mb: 1 }}>
Vocabulary Diversity
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.vocabulary_diversity ?
(detailedAnalysis.content_quality.vocabulary_diversity * 100).toFixed(1) + '%' : 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Content Depth Score
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Score (0-100) indicating how comprehensive and detailed your content is.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Scoring Factors:</strong> Word count, section depth, information density
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Deeper content provides more value and ranks better in search results.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(156, 39, 176, 0.1)', borderRadius: 2, border: '1px solid rgba(156, 39, 176, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'secondary.main', mb: 1 }}>
Content Depth Score
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.content_depth_score || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Flow Score
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Score (0-100) indicating how well your content flows from one idea to the next.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Scoring Factors:</strong> Transition words, sentence variety, logical progression
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Good flow improves readability and keeps readers engaged.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(255, 152, 0, 0.1)', borderRadius: 2, border: '1px solid rgba(255, 152, 0, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'warning.main', mb: 1 }}>
Flow Score
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.flow_score || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Transition Words
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Number of transition words used to connect ideas and improve flow.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Optimal Range:</strong> 5-15 transition words for most blog posts
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Transition words improve readability and help readers follow your logic.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(244, 67, 54, 0.1)', borderRadius: 2, border: '1px solid rgba(244, 67, 54, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'error.main', mb: 1 }}>
Transition Words
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.transition_words_used || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
<Grid item xs={12} md={4}>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Unique Words
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Number of unique words used in your content.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Why it matters:</strong> More unique words indicate richer vocabulary and better content variety.
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>SEO Impact:</strong> Diverse vocabulary can help with semantic SEO and topic coverage.
</Typography>
</Box>
}
arrow
>
<Box sx={{ p: 2, background: 'rgba(0, 150, 136, 0.1)', borderRadius: 2, border: '1px solid rgba(0, 150, 136, 0.3)', cursor: 'help' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'info.main', mb: 1 }}>
Unique Words
</Typography>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>
{detailedAnalysis?.content_quality?.unique_words || 'N/A'}
</Typography>
</Box>
</Tooltip>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid>
{/* Heading Structure */}
<Grid container spacing={3} sx={{ mt: 2 }}>
<Grid item xs={12}>
@@ -184,10 +533,78 @@ export const StructureAnalysis: React.FC<StructureAnalysisProps> = ({ detailedAn
</Grid>
</Grid>
<Box sx={{ mt: 2, p: 2, background: 'rgba(0,0,0,0.02)', borderRadius: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1 }}>
Heading Hierarchy Score: {detailedAnalysis?.heading_structure?.heading_hierarchy_score || 'N/A'}
</Typography>
<Tooltip
title={
<Box sx={{ p: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
Heading Hierarchy Score
</Typography>
<Typography variant="body2" sx={{ mb: 1 }}>
Score (0-100) indicating how well your heading structure follows SEO best practices.
</Typography>
<Typography variant="caption" sx={{ display: 'block', mb: 1 }}>
<strong>Scoring Factors:</strong> H1 presence, logical hierarchy, keyword usage in headings
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
<strong>Why it matters:</strong> Good heading structure helps search engines understand your content and improves readability.
</Typography>
</Box>
}
arrow
>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1, cursor: 'help' }}>
Heading Hierarchy Score: {detailedAnalysis?.heading_structure?.heading_hierarchy_score || 'N/A'}
</Typography>
</Tooltip>
</Box>
{/* Structure Recommendations */}
{detailedAnalysis?.content_structure?.recommendations && detailedAnalysis.content_structure.recommendations.length > 0 && (
<Box sx={{ mt: 2, p: 2, background: 'rgba(255, 193, 7, 0.1)', borderRadius: 2, border: '1px solid rgba(255, 193, 7, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: 'warning.main' }}>
Structure Recommendations
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{detailedAnalysis.content_structure.recommendations.map((recommendation: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block' }}>
• {recommendation}
</Typography>
))}
</Box>
</Box>
)}
{/* Heading Recommendations */}
{detailedAnalysis?.heading_structure?.recommendations && detailedAnalysis.heading_structure.recommendations.length > 0 && (
<Box sx={{ mt: 2, p: 2, background: 'rgba(33, 150, 243, 0.1)', borderRadius: 2, border: '1px solid rgba(33, 150, 243, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: 'primary.main' }}>
Heading Recommendations
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{detailedAnalysis.heading_structure.recommendations.map((recommendation: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block' }}>
• {recommendation}
</Typography>
))}
</Box>
</Box>
)}
{/* Content Quality Recommendations */}
{detailedAnalysis?.content_quality?.recommendations && detailedAnalysis.content_quality.recommendations.length > 0 && (
<Box sx={{ mt: 2, p: 2, background: 'rgba(76, 175, 80, 0.1)', borderRadius: 2, border: '1px solid rgba(76, 175, 80, 0.3)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1, color: 'success.main' }}>
Content Quality Recommendations
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{detailedAnalysis.content_quality.recommendations.map((recommendation: string, index: number) => (
<Typography key={index} variant="caption" sx={{ display: 'block' }}>
{recommendation}
</Typography>
))}
</Box>
</Box>
)}
</Paper>
</Grid>
</Grid>

View File

@@ -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<SEOAnalysisModalProps> = ({
isOpen,
onClose,
blogContent,
blogTitle,
researchData,
onApplyRecommendations
}) => {
@@ -192,6 +194,7 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
},
body: JSON.stringify({
blog_content: blogContent,
blog_title: blogTitle,
research_data: researchData
})
});
@@ -202,12 +205,6 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
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<SEOAnalysisModalProps> = ({
<Recommendations recommendations={analysisResult.actionable_recommendations} />
)}
{tabValue === 'keywords' && (
<KeywordAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
)}
{tabValue === 'keywords' && (
<KeywordAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
)}
{tabValue === 'readability' && (
<ReadabilityAnalysis
@@ -622,7 +619,15 @@ export const SEOAnalysisModal: React.FC<SEOAnalysisModalProps> = ({
)}
{tabValue === 'structure' && (
<StructureAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
analysisResult ? (
<StructureAnalysis detailedAnalysis={analysisResult.detailed_analysis} />
) : (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Loading structure analysis...
</Typography>
</Box>
)
)}
{tabValue === 'insights' && (

View File

@@ -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<SEOMetadataModalProps> = ({
isOpen,
onClose,
blogContent,
blogTitle,
researchData,
onMetadataGenerated
}) => {
const [isGenerating, setIsGenerating] = useState(false);
const [metadataResult, setMetadataResult] = useState<SEOMetadataResult | null>(null);
const [error, setError] = useState<string | null>(null);
const [tabValue, setTabValue] = useState('core');
const [copiedItems, setCopiedItems] = useState<Set<string>>(new Set());
const [editableMetadata, setEditableMetadata] = useState<SEOMetadataResult | null>(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 <SearchIcon />;
case 'social': return <ShareIcon />;
case 'structured': return <CodeIcon />;
case 'preview': return <PreviewIcon />;
default: return <TagIcon />;
}
};
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 (
<Dialog
open={isOpen}
onClose={onClose}
maxWidth="lg"
fullWidth
PaperProps={{
sx: {
background: 'rgba(255, 255, 255, 0.98)',
backdropFilter: 'blur(10px)',
borderRadius: 3,
minHeight: '80vh'
}
}}
>
<DialogTitle sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
pb: 1,
borderBottom: '1px solid rgba(0,0,0,0.1)'
}}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<TagIcon sx={{ color: 'primary.main' }} />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
SEO Metadata Generator
</Typography>
{metadataResult && (
<Chip
label={`${metadataResult.optimization_score || 0}% Optimized`}
color={metadataResult.optimization_score && metadataResult.optimization_score >= 80 ? 'success' :
metadataResult.optimization_score && metadataResult.optimization_score >= 60 ? 'warning' : 'error'}
size="small"
/>
)}
</Box>
<IconButton onClick={onClose} size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
{!metadataResult && !isGenerating && (
<Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="h6" sx={{ mb: 2 }}>
Generate Comprehensive SEO Metadata
</Typography>
<Typography variant="body2" sx={{ mb: 3, color: 'text.secondary' }}>
Create optimized titles, descriptions, Open Graph tags, Twitter cards, and structured data for your blog post.
</Typography>
<Button
variant="contained"
size="large"
onClick={generateMetadata}
startIcon={<RefreshIcon />}
sx={{ px: 4 }}
>
Generate SEO Metadata
</Button>
</Box>
)}
{isGenerating && (
<Box sx={{ p: 4, textAlign: 'center' }}>
<CircularProgress size={60} sx={{ mb: 2 }} />
<Typography variant="h6" sx={{ mb: 1 }}>
Generating SEO Metadata...
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Creating optimized titles, descriptions, and social media tags
</Typography>
</Box>
)}
{error && (
<Box sx={{ p: 3 }}>
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
<Button
variant="outlined"
onClick={generateMetadata}
startIcon={<RefreshIcon />}
>
Try Again
</Button>
</Box>
)}
{metadataResult && (
<Box>
{/* Tabs */}
<Box sx={{ borderBottom: 1, borderColor: 'divider', px: 3 }}>
<Tabs
value={tabValue}
onChange={handleTabChange}
variant="scrollable"
scrollButtons="auto"
sx={{ minHeight: 48 }}
>
{['core', 'social', 'structured', 'preview'].map((tab) => (
<Tab
key={tab}
value={tab}
label={getTabLabel(tab)}
icon={getTabIcon(tab)}
iconPosition="start"
sx={{ minHeight: 48, textTransform: 'none' }}
/>
))}
</Tabs>
</Box>
{/* Tab Content */}
<Box sx={{ p: 3 }}>
{tabValue === 'core' && (
<CoreMetadataTab
metadata={editableMetadata || metadataResult}
onMetadataEdit={handleMetadataEdit}
onCopyToClipboard={handleCopyToClipboard}
copiedItems={copiedItems}
/>
)}
{tabValue === 'social' && (
<SocialMediaTab
metadata={editableMetadata || metadataResult}
onMetadataEdit={handleMetadataEdit}
onCopyToClipboard={handleCopyToClipboard}
copiedItems={copiedItems}
/>
)}
{tabValue === 'structured' && (
<StructuredDataTab
metadata={editableMetadata || metadataResult}
onMetadataEdit={handleMetadataEdit}
onCopyToClipboard={handleCopyToClipboard}
copiedItems={copiedItems}
/>
)}
{tabValue === 'preview' && (
<PreviewCard
metadata={editableMetadata || metadataResult}
blogTitle={blogTitle}
/>
)}
</Box>
</Box>
)}
</DialogContent>
{metadataResult && (
<DialogActions sx={{ p: 3, borderTop: '1px solid rgba(0,0,0,0.1)' }}>
<Button onClick={onClose} color="inherit">
Cancel
</Button>
<Button
variant="contained"
onClick={handleApplyMetadata}
startIcon={<CheckIcon />}
sx={{ px: 3 }}
>
Apply Metadata
</Button>
</DialogActions>
)}
</Dialog>
);
};

View File

@@ -114,9 +114,9 @@ const BlogEditor: React.FC<BlogEditorProps> = ({
<h1
className="flex-1 text-2xl md:text-4xl font-bold font-serif text-gray-900 leading-tight cursor-pointer hover:bg-gray-50 p-2 rounded-md transition-colors duration-200"
style={{
wordBreak: 'break-word',
overflowWrap: 'break-word',
whiteSpace: 'normal',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
lineHeight: '1.3'
}}
onClick={() => {