Content Calendar, Content Gap Analysis, and Content Optimization

This commit is contained in:
ajaysi
2025-05-27 09:15:08 +05:30
parent 4049d19787
commit 889021c078
100 changed files with 18504 additions and 1251 deletions

View File

@@ -8,7 +8,7 @@ from loguru import logger
from lib.ai_writers.ai_news_article_writer import ai_news_generation
from lib.ai_writers.ai_financial_writer import write_basic_ta_report
from lib.ai_writers.ai_finance_report_generator.ai_financial_dashboard import get_dashboard
from lib.ai_writers.ai_facebook_writer.facebook_ai_writer import facebook_main_menu
from lib.ai_writers.linkedin_writer.linkedin_ai_writer import linkedin_main_menu
from lib.ai_writers.twitter_writers.twitter_dashboard import run_dashboard
@@ -198,7 +198,9 @@ def ai_finance_ta_writer():
if ticker_symbol:
with st.spinner("Generating TA Report..."):
try:
ta_report = write_basic_ta_report(ticker_symbol)
# Get dashboard instance and generate technical analysis
dashboard = get_dashboard()
ta_report = dashboard.generate_technical_analysis(ticker_symbol)
st.success(f"Successfully generated TA report for: {ticker_symbol}")
st.markdown(ta_report)
except Exception as err:

View File

@@ -3,7 +3,7 @@
import streamlit as st
from loguru import logger
from ...website_analyzer import analyze_website
from ...website_analyzer.seo_analyzer import analyze_seo
from ...website_analyzer.analyzer import WebsiteAnalyzer
import asyncio
import sys
from typing import Dict, Any
@@ -127,37 +127,19 @@ def render_website_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]:
# Call the analyze_website function
results = analyze_website(url)
# If full analysis is selected, add SEO analysis
if analyze_type == "Full Analysis with SEO":
seo_results = analyze_seo(url)
if seo_results.success:
results['data']['seo_analysis'] = {
'overall_score': seo_results.overall_score,
'meta_tags': {
'title': seo_results.meta_tags.title,
'description': seo_results.meta_tags.description,
'keywords': seo_results.meta_tags.keywords,
'has_robots': seo_results.meta_tags.has_robots,
'has_sitemap': seo_results.meta_tags.has_sitemap
},
'content': {
'word_count': seo_results.content.word_count,
'readability_score': seo_results.content.readability_score,
'content_quality_score': seo_results.content.content_quality_score,
'headings_structure': seo_results.content.headings_structure,
'keyword_density': seo_results.content.keyword_density
},
'recommendations': [
{
'priority': rec.priority,
'category': rec.category,
'issue': rec.issue,
'recommendation': rec.recommendation,
'impact': rec.impact
}
for rec in seo_results.recommendations
]
}
# Replace the old SEO analysis code with the new analyzer
analyzer = WebsiteAnalyzer()
seo_results = analyzer.analyze_website(url)
if seo_results.get('success', False):
results['data']['seo_analysis'] = seo_results['data']['analysis']['seo_info']
else:
results['data']['seo_analysis'] = {
'error': seo_results.get('error', 'Unknown error in SEO analysis'),
'overall_score': 0,
'meta_tags': {},
'content': {},
'recommendations': []
}
logger.debug(f"[render_website_setup] Analysis results received: {results.get('success', False)}")

83
lib/utils/save_to_file.py Normal file
View File

@@ -0,0 +1,83 @@
"""
Utility module for saving generated content to files.
Handles saving various types of content to the workspace directory.
"""
import os
import json
from pathlib import Path
from datetime import datetime
from typing import Union, Dict, List, Any
# Define the workspace directory
WORKSPACE_DIR = Path(__file__).parent.parent.parent / "workspace" / "alwrity_content"
def ensure_directory_exists(directory: Union[str, Path]) -> None:
"""Ensure the specified directory exists."""
os.makedirs(directory, exist_ok=True)
def save_to_file(
content: Union[str, Dict, List, Any],
filename: str,
content_type: str = "text",
subdirectory: str = None
) -> str:
"""
Save content to a file in the workspace directory.
Args:
content: The content to save (string, dict, list, or any serializable object)
filename: Name of the file to save
content_type: Type of content ('text', 'json', 'audio', 'image')
subdirectory: Optional subdirectory within the workspace
Returns:
str: Path to the saved file
"""
# Create timestamp for unique filenames
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
base_filename = f"{timestamp}_{filename}"
# Determine the target directory
target_dir = WORKSPACE_DIR
if subdirectory:
target_dir = target_dir / subdirectory
# Ensure directory exists
ensure_directory_exists(target_dir)
# Determine file extension and format content
if content_type == "json":
file_path = target_dir / f"{base_filename}.json"
with open(file_path, "w", encoding="utf-8") as f:
json.dump(content, f, indent=2, ensure_ascii=False)
elif content_type == "audio":
file_path = target_dir / f"{base_filename}.mp3"
with open(file_path, "wb") as f:
f.write(content)
elif content_type == "image":
file_path = target_dir / f"{base_filename}.png"
with open(file_path, "wb") as f:
f.write(content)
else: # text
file_path = target_dir / f"{base_filename}.txt"
with open(file_path, "w", encoding="utf-8") as f:
f.write(str(content))
return str(file_path)
def save_audio(audio_bytes: bytes, filename: str, subdirectory: str = "audio") -> str:
"""Save audio content to a file."""
return save_to_file(audio_bytes, filename, "audio", subdirectory)
def save_image(image_bytes: bytes, filename: str, subdirectory: str = "images") -> str:
"""Save image content to a file."""
return save_to_file(image_bytes, filename, "image", subdirectory)
def save_json(data: Union[Dict, List], filename: str, subdirectory: str = "json") -> str:
"""Save JSON content to a file."""
return save_to_file(data, filename, "json", subdirectory)
def save_text(text: str, filename: str, subdirectory: str = "text") -> str:
"""Save text content to a file."""
return save_to_file(text, filename, "text", subdirectory)

View File

@@ -9,7 +9,347 @@ from lib.ai_seo_tools.google_pagespeed_insights import google_pagespeed_insights
from lib.ai_seo_tools.on_page_seo_analyzer import analyze_onpage_seo
from lib.ai_seo_tools.weburl_seo_checker import url_seo_checker
from lib.ai_marketing_tools.ai_backlinker.backlinking_ui_streamlit import backlinking_ui
from lib.ai_seo_tools.content_gap_analysis.ui import ContentGapAnalysisUI
from lib.ai_seo_tools.content_calendar.ui.dashboard import ContentCalendarDashboard
def render_content_gap_analysis():
"""Render the content gap analysis workflow interface."""
from lib.ai_seo_tools.content_gap_analysis.ui import ContentGapAnalysisUI
# Initialize and run the Content Gap Analysis UI
ui = ContentGapAnalysisUI()
ui.run()
def render_content_calendar():
"""Render the content calendar dashboard."""
import logging
import sys
from datetime import datetime
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('content_calendar.log', mode='a')
]
)
logger = logging.getLogger('content_calendar')
try:
logger.info("Initializing Content Calendar Dashboard")
dashboard = ContentCalendarDashboard()
logger.info("Rendering Content Calendar Dashboard")
dashboard.render()
logger.info("Content Calendar Dashboard rendered successfully")
except Exception as e:
logger.error(f"Error rendering content calendar: {str(e)}", exc_info=True)
st.error(f"An error occurred while loading the content calendar: {str(e)}")
def render_seo_tools_dashboard():
"""Render a modern dashboard for SEO tools with improved UI and navigation."""
selected_section = st.session_state.get('seo_dashboard_section', 'combinations')
# Define card gradients at the top so it's available in all sections
card_gradients = {
"Content Optimization Suite": "linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)",
"Technical SEO Audit": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
"Image Optimization Suite": "linear-gradient(135deg, #f7971e 0%, #ffd200 100%)",
"Social Media Optimization": "linear-gradient(135deg, #f953c6 0%, #b91d73 100%)",
"Content Gap Analysis": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
"Content Calendar": "linear-gradient(135deg, #4CAF50 0%, #2196F3 100%)",
"Structured Data Generator": "linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)",
"Blog Title Generator": "linear-gradient(135deg, #2193b0 0%, #6dd5ed 100%)",
"Meta Description Generator": "linear-gradient(135deg, #f7971e 0%, #ffd200 100%)",
"Image Alt Text Generator": "linear-gradient(135deg, #f953c6 0%, #b91d73 100%)",
"OpenGraph Tags Generator": "linear-gradient(135deg, #f857a6 0%, #ff5858 100%)",
"Image Optimizer": "linear-gradient(135deg, #43cea2 0%, #185a9d 100%)",
"PageSpeed Insights": "linear-gradient(135deg, #ff9966 0%, #ff5e62 100%)",
"On-Page SEO Analyzer": "linear-gradient(135deg, #56ab2f 0%, #a8e063 100%)",
"URL SEO Checker": "linear-gradient(135deg, #3a7bd5 0%, #00d2ff 100%)",
"AI Backlinking Tool": "linear-gradient(135deg, #e96443 0%, #904e95 100%)"
}
# Navigation bar only (no dashboard title/description)
nav_cols = st.columns([1,1,1,1])
nav_labels = ["Tool Combos", "Advanced", "Individual", "About"]
nav_keys = ["combinations", "advanced", "individual", "about"]
for i, label in enumerate(nav_labels):
if nav_cols[i].button(label, key=f"nav_{label}"):
st.session_state['seo_dashboard_section'] = nav_keys[i]
selected_section = nav_keys[i]
st.markdown("<hr style='margin:1.5rem 0;'>", unsafe_allow_html=True)
# Define tool combinations for cross-tool analysis
tool_combinations = {
"Content Optimization Suite": {
"icon": "📊",
"description": "Comprehensive content optimization combining title generation, meta descriptions, and structured data.",
"tools": ["Blog Title Generator", "Meta Description Generator", "Structured Data Generator"],
"path": "content_optimization",
"color": "#4CAF50"
},
"Technical SEO Audit": {
"icon": "🔧",
"description": "Complete technical SEO analysis including page speed, on-page SEO, and URL structure.",
"tools": ["PageSpeed Insights", "On-Page SEO Analyzer", "URL SEO Checker"],
"path": "technical_audit",
"color": "#2196F3"
},
"Image Optimization Suite": {
"icon": "🖼️",
"description": "End-to-end image optimization with alt text generation and performance optimization.",
"tools": ["Image Alt Text Generator", "Image Optimizer"],
"path": "image_optimization",
"color": "#FF9800"
},
"Social Media Optimization": {
"icon": "📱",
"description": "Enhance social media presence with OpenGraph tags and backlink analysis.",
"tools": ["OpenGraph Tags Generator", "AI Backlinking Tool"],
"path": "social_optimization",
"color": "#9C27B0"
}
}
# Define individual SEO tools
seo_tools = {
"Structured Data Generator": {
"icon": "📋",
"description": "Generate structured data (Rich Snippets) to enhance your search results with additional information.",
"color": "#4CAF50",
"path": "structured_data",
"status": "active"
},
"Blog Title Generator": {
"icon": "✏️",
"description": "Create SEO-optimized blog titles that attract clicks and improve search rankings.",
"color": "#2196F3",
"path": "blog_title",
"status": "active"
},
"Meta Description Generator": {
"icon": "📝",
"description": "Generate compelling meta descriptions that improve click-through rates from search results.",
"color": "#FF9800",
"path": "meta_description",
"status": "active"
},
"Image Alt Text Generator": {
"icon": "🖼️",
"description": "Create descriptive alt text for images to improve accessibility and image SEO.",
"color": "#9C27B0",
"path": "alt_text",
"status": "active"
},
"OpenGraph Tags Generator": {
"icon": "📱",
"description": "Generate OpenGraph tags for better social media sharing and visibility.",
"color": "#F44336",
"path": "opengraph",
"status": "active"
},
"Image Optimizer": {
"icon": "📉",
"description": "Optimize and resize images for better website performance and SEO.",
"color": "#607D8B",
"path": "image_optimizer",
"status": "active"
},
"PageSpeed Insights": {
"icon": "",
"description": "Analyze your website's performance using Google PageSpeed Insights.",
"color": "#795548",
"path": "pagespeed",
"status": "active"
},
"On-Page SEO Analyzer": {
"icon": "🔍",
"description": "Analyze and optimize your webpage's SEO elements and content.",
"color": "#009688",
"path": "onpage_seo",
"status": "active"
},
"URL SEO Checker": {
"icon": "🌐",
"description": "Check the SEO health of specific URLs and get improvement suggestions.",
"color": "#3F51B5",
"path": "url_checker",
"status": "active"
},
"AI Backlinking Tool": {
"icon": "🔗",
"description": "Discover and analyze backlink opportunities using AI-powered insights.",
"color": "#E91E63",
"path": "backlinking",
"status": "active"
}
}
# --- Tool Combinations Section ---
if selected_section == 'combinations':
combo_cols = st.columns(2)
for idx, (combo_name, details) in enumerate(tool_combinations.items()):
gradient = card_gradients.get(combo_name, "linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)")
with combo_cols[idx % 2]:
st.markdown(f"""
<div class="seo-card" style="background: {gradient}; position: relative; overflow: hidden;">
<div class="seo-card-overlay"></div>
<div class="seo-icon">{details['icon']}</div>
<div class="seo-title">{combo_name}</div>
<div class="seo-description">{details['description']}</div>
<div>
{''.join([f'<span class="tool-badge">{tool}</span>' for tool in details['tools']])}
</div>
</div>
""", unsafe_allow_html=True)
if st.button(f"Launch {combo_name}", key=f"combo_{combo_name}", use_container_width=True):
st.query_params["tool"] = details["path"]
st.rerun()
# --- Advanced Features Section ---
elif selected_section == 'advanced':
adv_cols = st.columns(2)
adv_features = [
{
"name": "Content Gap Analysis",
"icon": "🎯",
"description": "Identify content opportunities and optimize your content strategy with AI-powered insights.",
"badges": ["Website Analysis", "Competitor Research", "Keyword Opportunities", "AI Recommendations"],
"gradient": card_gradients["Content Gap Analysis"],
"button": "Start Content Gap Analysis",
"key": "content_gap_analysis",
"path": "content_gap_analysis"
},
{
"name": "Content Calendar",
"icon": "📅",
"description": "Plan, schedule, and manage your content strategy with our AI-powered content calendar.",
"badges": ["Content Planning", "Scheduling", "Performance Tracking", "AI Insights"],
"gradient": card_gradients["Content Calendar"],
"button": "Open Content Calendar",
"key": "content_calendar",
"path": "content_calendar"
}
]
for idx, feature in enumerate(adv_features):
with adv_cols[idx % 2]:
st.markdown(f"""
<div class="seo-card" style="background: {feature['gradient']}; position: relative; overflow: hidden;">
<div class="seo-card-overlay"></div>
<div class="seo-icon">{feature['icon']}</div>
<div class="seo-title">{feature['name']}</div>
<div class="seo-description">{feature['description']}</div>
<div>
{''.join([f'<span class="tool-badge">{badge}</span>' for badge in feature['badges']])}
</div>
</div>
""", unsafe_allow_html=True)
if st.button(feature['button'], key=feature['key'], use_container_width=True):
st.query_params["tool"] = feature["path"]
st.rerun()
# --- Individual Tools Section ---
elif selected_section == 'individual':
cols = st.columns(3)
for idx, (tool_name, details) in enumerate(seo_tools.items()):
gradient = card_gradients.get(tool_name, "linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%)")
with cols[idx % 3]:
st.markdown(f"""
<div class="seo-card" style="background: {gradient}; position: relative; overflow: hidden;">
<div class="seo-card-overlay"></div>
<div class="seo-icon">{details['icon']}</div>
<div class="seo-title">{tool_name}</div>
<div class="seo-description">{details['description']}</div>
</div>
""", unsafe_allow_html=True)
if st.button(f"Use {tool_name}", key=f"btn_{tool_name}", use_container_width=True):
st.query_params["tool"] = details["path"]
st.rerun()
# --- About Section ---
elif selected_section == 'about':
st.markdown("""
<div style='text-align: center; margin: 2rem 0;'>
<h2>About This Dashboard</h2>
<p style='color: #666;'>This dashboard brings together powerful AI-driven SEO tools and workflows to help you optimize your website and content strategy. Use the navigation above to explore combinations, advanced features, or individual tools.</p>
</div>
""", unsafe_allow_html=True)
st.markdown("""
<style>
.seo-card {
border-radius: 14px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 16px rgba(44, 62, 80, 0.10), 0 1.5px 4px rgba(44,62,80,0.06);
transition: transform 0.2s cubic-bezier(.4,2,.6,1), box-shadow 0.2s;
height: 100%;
border: 1.5px solid #e3e8ee;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.seo-card-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(255,255,255,0.72);
z-index: 1;
pointer-events: none;
border-radius: 14px;
box-shadow: 0 2px 8px rgba(44,62,80,0.08);
}
.seo-card:hover {
transform: translateY(-6px) scale(1.025);
box-shadow: 0 8px 32px rgba(44, 62, 80, 0.18), 0 2px 8px rgba(44,62,80,0.10);
border-color: #4CAF50;
}
.seo-icon {
font-size: 2.7rem;
margin-bottom: 18px;
z-index: 2;
position: relative;
text-shadow: 0 2px 8px rgba(44,62,80,0.10);
}
.seo-title {
font-size: 1.25rem;
font-weight: 800;
margin-bottom: 12px;
color: #222b45;
z-index: 2;
position: relative;
text-shadow: 0 2px 8px rgba(44,62,80,0.10);
letter-spacing: 0.01em;
}
.seo-description {
color: #34495e;
font-size: 1.08rem;
margin-bottom: 15px;
z-index: 2;
position: relative;
text-align: center;
line-height: 1.5;
text-shadow: 0 1px 4px rgba(44,62,80,0.08);
}
.tool-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.9rem;
margin-right: 8px;
margin-bottom: 8px;
background: rgba(255, 255, 255, 0.95);
color: #2196F3;
font-weight: 600;
border: 1px solid #e3e8ee;
}
</style>
""", unsafe_allow_html=True)
def ai_seo_tools():
"""
@@ -17,71 +357,83 @@ def ai_seo_tools():
such as generating structured data, optimizing images, checking page speed,
and analyzing on-page SEO.
"""
st.markdown(
"""
Welcome to your one-stop solution for AI-driven SEO optimization. Select a tool from the options below
to improve your websites SEO with cutting-edge AI technology.
"""
)
# List of SEO tools with unique emojis for each option
options = [
"📝 Generate Structured Data - Rich Snippet",
"✏️ Generate SEO Optimized Blog Titles",
"📝 Generate Meta Description for SEO",
"🖼️ Generate Image Alt Text",
"📄 Generate OpenGraph Tags",
"📉 Optimize/Resize Image",
"⚡ Run Google PageSpeed Insights",
"🔍 Analyze On-Page SEO",
"🌐 URL SEO Checker",
"🔗 AI Backlinking Tool"
]
# Check if a specific tool is selected
selected_tool = st.query_params.get("tool")
# User selection of SEO tools using radio buttons
choice = st.radio(
"**👇 Select an AI SEO Tool:**",
options,
index=0,
format_func=lambda x: x
)
if selected_tool:
# Map tool paths to their respective functions
tool_functions = {
# Individual tools
"structured_data": ai_structured_data,
"blog_title": ai_title_generator,
"meta_description": metadesc_generator_main,
"alt_text": alt_text_gen,
"opengraph": og_tag_generator,
"image_optimizer": main_img_optimizer,
"pagespeed": google_pagespeed_insights,
"onpage_seo": analyze_onpage_seo,
"url_checker": url_seo_checker,
"backlinking": backlinking_ui,
# Tool combinations
"content_optimization": lambda: run_tool_combination([
ai_title_generator,
metadesc_generator_main,
ai_structured_data
], "Content Optimization Suite"),
"technical_audit": lambda: run_tool_combination([
google_pagespeed_insights,
analyze_onpage_seo,
url_seo_checker
], "Technical SEO Audit"),
"image_optimization": lambda: run_tool_combination([
alt_text_gen,
main_img_optimizer
], "Image Optimization Suite"),
"social_optimization": lambda: run_tool_combination([
og_tag_generator,
backlinking_ui
], "Social Media Optimization"),
# Add Content Gap Analysis and Content Calendar
"content_gap_analysis": render_content_gap_analysis,
"content_calendar": render_content_calendar
}
if selected_tool in tool_functions:
# Clear any existing content
st.empty()
# Execute the selected tool's function
tool_functions[selected_tool]()
else:
st.error(f"Invalid tool selected: {selected_tool}")
render_seo_tools_dashboard()
else:
# Show the dashboard if no tool is selected
render_seo_tools_dashboard()
def run_tool_combination(tools, combination_name):
"""Run a combination of tools and provide cross-tool analysis."""
st.markdown(f"# {combination_name}")
st.markdown("Running comprehensive analysis...")
# Call the respective functions based on the user selection
if choice == "📝 Generate Structured Data - Rich Snippet":
# Generate Structured Data for Rich Snippets
ai_structured_data()
elif choice == "📝 Generate Meta Description for SEO":
# Generate SEO-optimized meta descriptions
metadesc_generator_main()
elif choice == "✏️ Generate SEO Optimized Blog Titles":
# Generate SEO-friendly blog titles
ai_title_generator()
elif choice == "🖼️ Generate Image Alt Text":
# Generate alternative text for images
alt_text_gen()
elif choice == "📄 Generate OpenGraph Tags":
# Generate OpenGraph tags for social media sharing
og_tag_generator()
elif choice == "📉 Optimize/Resize Image":
# Optimize images by resizing or compressing them
main_img_optimizer()
elif choice == "⚡ Run Google PageSpeed Insights":
# Run Google PageSpeed Insights for performance analysis
google_pagespeed_insights()
elif choice == "🔍 Analyze On-Page SEO":
# Analyze on-page SEO elements
analyze_onpage_seo()
elif choice == "🌐 URL SEO Checker":
# Check SEO health of a specific URL
url_seo_checker()
elif choice == "🔗 AI Backlinking Tool":
# Run AI Backlinking tool for link-building opportunities
backlinking_ui()
# Create tabs for each tool in the combination
tabs = st.tabs([f"Step {i+1}" for i in range(len(tools))])
# Run each tool in its own tab
for i, (tab, tool) in enumerate(zip(tabs, tools)):
with tab:
st.markdown(f"### Step {i+1}")
tool()
# Add cross-tool analysis section
st.markdown("## 📊 Cross-Tool Analysis")
st.markdown("Analyzing results across all tools...")
# Add recommendations based on combined results
st.markdown("## 💡 Recommendations")
st.markdown("Based on the combined analysis, here are the key recommendations:")
# Add a button to export the complete analysis
if st.button("📥 Export Complete Analysis", use_container_width=True):
st.info("Analysis export functionality coming soon!")

View File

@@ -1,3 +1,8 @@
"""
UI setup module for ALwrity application.
Provides consistent navigation and layout structure.
"""
import os
import streamlit as st
from lib.utils.file_processor import load_image
@@ -15,6 +20,99 @@ from lib.ai_writers.insta_ai_writer import insta_writer
from lib.ai_writers.youtube_writers.youtube_ai_writer import youtube_main_menu
from lib.ai_writers.ai_writer_dashboard import get_ai_writers, list_ai_writers
def render_social_tools_dashboard():
"""Render a modern dashboard for social media tools."""
st.markdown("""
<style>
.social-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
height: 100%;
}
.social-card:hover {
transform: translateY(-5px);
}
.social-icon {
font-size: 2.5rem;
margin-bottom: 15px;
}
.social-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 10px;
}
.social-description {
color: #666;
font-size: 0.9rem;
margin-bottom: 15px;
}
.social-button {
width: 100%;
padding: 8px 16px;
border-radius: 5px;
border: none;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
</style>
""", unsafe_allow_html=True)
# Define social tools with their details and paths
social_tools = {
"Facebook": {
"icon": "📘",
"description": "Create engaging Facebook posts and manage your content strategy",
"color": "#4267B2",
"path": "facebook"
},
"LinkedIn": {
"icon": "💼",
"description": "Generate professional LinkedIn content and optimize your profile",
"color": "#0077B5",
"path": "linkedin"
},
"Twitter": {
"icon": "🐦",
"description": "Craft viral tweets and manage your Twitter presence",
"color": "#1DA1F2",
"path": "twitter"
},
"Instagram": {
"icon": "📸",
"description": "Create Instagram captions and plan your visual content",
"color": "#E1306C",
"path": "instagram"
},
"YouTube": {
"icon": "🎥",
"description": "Generate video scripts and optimize your YouTube content",
"color": "#FF0000",
"path": "youtube"
}
}
# Create a grid of cards
cols = st.columns(3)
for idx, (platform, details) in enumerate(social_tools.items()):
with cols[idx % 3]:
st.markdown(f"""
<div class="social-card">
<div class="social-icon">{details['icon']}</div>
<div class="social-title">{platform}</div>
<div class="social-description">{details['description']}</div>
</div>
""", unsafe_allow_html=True)
if st.button(f"Open {platform}", key=f"btn_{platform}",
help=f"Launch {platform} tools",
use_container_width=True):
# Set query parameters to redirect to the specific tool
st.query_params["tool"] = details["path"]
st.rerun()
def setup_ui():
"""Set up the UI with custom styling."""
@@ -314,29 +412,18 @@ def setup_alwrity_ui():
"AI Writers": ("📝", get_ai_writers),
"Content Planning": ("📅", content_planning_tools),
"AI SEO Tools": ("🔍", ai_seo_tools),
"AI Social Tools": ("📱", None), # Set to None as we'll handle this separately
"AI Social Tools": ("📱", render_social_tools_dashboard),
"ALwrity Settings": ("⚙️", render_settings_page),
"Agents Teams(TBD)": ("🤝", lambda: st.subheader("Agents Teams - Coming Soon!")),
"Ask Alwrity(TBD)": ("💬", lambda: (
st.subheader("Chat with your Data, Chat with any Data.. COMING SOON !"),
st.markdown("Create a collection by uploading files (PDF, MD, CSV, etc), or crawl a data source (Websites, more sources coming soon."),
st.markdown("One can ask/chat, summarize and do semantic search over the uploaded data")
)),
"ALwrity Settings": ("⚙️", render_settings_page)
))
}
logger.info(f"Defined {len(nav_items)} navigation items")
# Define sub-menu items for AI Social Tools
social_tools_submenu = {
"Facebook": ("📘", lambda: facebook_main_menu()),
"LinkedIn": ("💼", lambda: linkedin_main_menu()),
"Twitter": ("🐦", lambda: run_dashboard()),
"Instagram": ("📸", lambda: insta_writer()),
"YouTube": ("🎥", lambda: youtube_main_menu())
}
logger.info(f"Defined {len(social_tools_submenu)} social tools submenu items")
# Create sidebar navigation
st.sidebar.markdown("### ALwrity Options")
st.sidebar.markdown('<div class="sidebar-nav">', unsafe_allow_html=True)
@@ -345,53 +432,12 @@ def setup_alwrity_ui():
for name, (icon, func) in nav_items.items():
button_class = "nav-button active" if st.session_state.active_tab == name else "nav-button"
if name == "AI Social Tools":
# For AI Social Tools, we'll create a button that toggles the sub-menu
if st.sidebar.button(f"{icon} {name}", key=f"nav_{name}",
help=f"Navigate to {name}", use_container_width=True):
st.session_state.active_tab = name
# Reset sub-tab when main tab changes
st.session_state.active_sub_tab = None
logger.info(f"Selected main tab: {name}")
# If AI Social Tools is active, show the sub-menu
if st.session_state.active_tab == "AI Social Tools":
st.sidebar.markdown('<div class="sub-menu">', unsafe_allow_html=True)
# Create sub-menu buttons
for sub_name, (sub_icon, sub_func) in social_tools_submenu.items():
# Create the button with a custom key that includes the platform name
button_key = f"sub_{sub_name}"
# Determine if this button is active
is_active = st.session_state.active_sub_tab == sub_name
# Create a container with the platform-specific class
platform_class = f"{sub_name.lower()}-button"
if is_active:
platform_class += " active"
# Add the platform-specific class to the button container
st.sidebar.markdown(f'<div class="{platform_class}">', unsafe_allow_html=True)
# Create the button
if st.sidebar.button(f"{sub_icon} {sub_name}", key=button_key,
help=f"Navigate to {sub_name}", use_container_width=True):
st.session_state.active_sub_tab = sub_name
logger.info(f"Selected social tool: {sub_name}")
# Close the div
st.sidebar.markdown('</div>', unsafe_allow_html=True)
st.sidebar.markdown('</div>', unsafe_allow_html=True)
else:
# For other navigation items, create regular buttons
if st.sidebar.button(f"{icon} {name}", key=f"nav_{name}",
help=f"Navigate to {name}", use_container_width=True):
st.session_state.active_tab = name
# Reset sub-tab when main tab changes
st.session_state.active_sub_tab = None
logger.info(f"Selected main tab: {name}")
if st.sidebar.button(f"{icon} {name}", key=f"nav_{name}",
help=f"Navigate to {name}", use_container_width=True):
st.session_state.active_tab = name
# Reset sub-tab when main tab changes
st.session_state.active_sub_tab = None
logger.info(f"Selected main tab: {name}")
st.sidebar.markdown('</div>', unsafe_allow_html=True)
@@ -402,10 +448,36 @@ def setup_alwrity_ui():
st.sidebar.image(icon_path, use_container_width=False)
st.sidebar.markdown('</div>', unsafe_allow_html=True)
# Display content based on active tab
# Display content based on active tab and tool selection
if st.session_state.active_tab == "AI Social Tools":
if not st.session_state.active_sub_tab:
# Only show title and info when no sub-tab is selected
# Check if a specific tool is selected
selected_tool = st.query_params.get("tool")
if selected_tool:
# Add a back button at the top
if st.button("← Back to Social Tools Dashboard", key=f"back_to_dashboard_{selected_tool}"):
# Clear the tool query parameter
st.query_params.clear()
st.rerun()
# Map tool paths to their respective functions
tool_functions = {
"facebook": facebook_main_menu,
"linkedin": linkedin_main_menu,
"twitter": run_dashboard,
"instagram": insta_writer,
"youtube": youtube_main_menu
}
if selected_tool in tool_functions:
# Clear any existing content
st.empty()
# Execute the selected tool's function
tool_functions[selected_tool]()
else:
st.error(f"Invalid tool selected: {selected_tool}")
render_social_tools_dashboard()
else:
# Show the dashboard if no tool is selected
st.markdown("""
<style>
.main .block-container {
@@ -414,40 +486,14 @@ def setup_alwrity_ui():
</style>
""", unsafe_allow_html=True)
st.title(f"{nav_items[st.session_state.active_tab][0]} {st.session_state.active_tab}")
st.info("Please select a social media platform from the sidebar.")
else:
# When a platform is selected, show no title and minimize spacing
st.markdown("""
<style>
.main .block-container {
padding-top: 0 !important;
padding-bottom: 0;
}
/* Remove all margins and padding from content area */
.element-container {
margin: 0 !important;
padding: 0 !important;
}
/* Hide any automatic headers */
.main .block-container > div:first-child {
margin-top: 0 !important;
padding-top: 0 !important;
}
</style>
""", unsafe_allow_html=True)
# Call the function directly without any title
social_tools_submenu[st.session_state.active_sub_tab][1]()
render_social_tools_dashboard()
else:
# Check if we're in the AI Writers section and handle writer selection
# Handle other tabs as before
if st.session_state.active_tab == "AI Writers":
# Get the writer parameter from the URL using st.query_params
writer = st.query_params.get("writer")
logger.info(f"Current writer from query params: {writer}")
if writer:
# Get the list of writers without rendering the dashboard
writers = list_ai_writers()
logger.info(f"Found {len(writers)} writers")
@@ -457,9 +503,7 @@ def setup_alwrity_ui():
if w["path"] == writer:
writer_found = True
logger.info(f"Found matching writer: {w['name']}, executing function")
# Clear any existing content
st.empty()
# Execute the writer function
w["function"]()
break
@@ -467,11 +511,9 @@ def setup_alwrity_ui():
logger.error(f"No writer found with path: {writer}")
st.error(f"No writer found with path: {writer}")
else:
# If no writer selected, show the dashboard
logger.info("No writer selected, showing dashboard")
get_ai_writers()
else:
# For all other tabs, show the title
st.markdown("""
<style>
.main .block-container {

View File

@@ -1,7 +1,6 @@
"""Website analyzer module for AI-powered website analysis."""
from .analyzer import analyze_website
from .seo_analyzer import analyze_seo
from .analyzer import analyze_website, WebsiteAnalyzer
from .models import SEOAnalysisResult
__all__ = ['analyze_seo', 'SEOAnalysisResult', 'analyze_website']
__all__ = ['analyze_website', 'WebsiteAnalyzer', 'SEOAnalysisResult']

View File

@@ -1,7 +1,7 @@
"""Website scraping and AI analysis module."""
"""Website and SEO analysis module."""
import asyncio
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Tuple
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import streamlit as st
@@ -21,51 +21,29 @@ import whois
import dns.resolver
from requests.exceptions import RequestException
from concurrent.futures import ThreadPoolExecutor
from .models import (
SEOAnalysisResult,
MetaTagAnalysis,
ContentAnalysis,
SEORecommendation
)
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('logs/website_analyzer.log')
]
)
# Create a logger for the website analyzer
logger = logging.getLogger(__name__)
def analyze_website(url: str) -> Dict:
"""
Analyze a website and return comprehensive results.
Args:
url (str): The URL to analyze
Returns:
Dict: Analysis results including various metrics and checks
"""
logger.info(f"Starting website analysis for URL: {url}")
try:
analyzer = WebsiteAnalyzer()
results = analyzer.analyze_website(url)
# Add success status to results
if "error" in results:
return {
"success": False,
"error": results["error"]
}
# Add success status and wrap results
return {
"success": True,
"data": results
}
except Exception as e:
logger.error(f"Error in analyze_website: {str(e)}", exc_info=True)
return {
"success": False,
"error": str(e)
}
# Create a separate logger for scraping operations
scraping_logger = logging.getLogger('website_analyzer.scraping')
scraping_logger.setLevel(logging.WARNING)
class WebsiteAnalyzer:
def __init__(self):
@@ -89,13 +67,17 @@ class WebsiteAnalyzer:
try:
# Validate URL
if not self._validate_url(url):
logger.error(f"Invalid URL format: {url}")
return {"error": "Invalid URL format"}
error_msg = f"Invalid URL format: {url}"
logger.error(error_msg)
return {
"success": False,
"error": error_msg,
"error_details": {"stage": "url_validation"}
}
# Basic URL parsing
parsed_url = urlparse(url)
domain = parsed_url.netloc
logger.debug(f"Parsed domain: {domain}")
# Initialize results dictionary
results = {
@@ -107,36 +89,105 @@ class WebsiteAnalyzer:
# Perform various analyses
with ThreadPoolExecutor(max_workers=4) as executor:
logger.info("Starting parallel analysis tasks")
# Basic website info
logger.info("Starting basic info analysis")
basic_info = executor.submit(self._get_basic_info, url).result()
if "error" in basic_info:
error_msg = f"Basic info analysis failed: {basic_info['error']}"
logger.error(error_msg)
return {
"success": False,
"error": error_msg,
"error_details": {
"stage": "basic_info",
"details": basic_info.get("error_details", {})
}
}
results["analysis"]["basic_info"] = basic_info
# SSL/TLS info
logger.info("Starting SSL analysis")
ssl_info = executor.submit(self._check_ssl, domain).result()
results["analysis"]["ssl_info"] = ssl_info
# DNS info
logger.info("Starting DNS analysis")
dns_info = executor.submit(self._check_dns, domain).result()
results["analysis"]["dns_info"] = dns_info
# WHOIS info
logger.info("Starting WHOIS analysis")
whois_info = executor.submit(self._get_whois_info, domain).result()
results["analysis"]["whois_info"] = whois_info
# Content analysis
logger.info("Starting content analysis")
content_info = executor.submit(self._analyze_content, url).result()
if "error" in content_info:
error_msg = f"Content analysis failed: {content_info['error']}"
logger.error(error_msg)
return {
"success": False,
"error": error_msg,
"error_details": {
"stage": "content_analysis",
"details": content_info.get("error_details", {})
}
}
results["analysis"]["content_info"] = content_info
# Performance metrics
logger.info("Starting performance analysis")
performance = executor.submit(self._check_performance, url).result()
if "error" in performance:
error_msg = f"Performance analysis failed: {performance['error']}"
logger.error(error_msg)
return {
"success": False,
"error": error_msg,
"error_details": {
"stage": "performance_analysis",
"details": performance.get("error_details", {})
}
}
results["analysis"]["performance"] = performance
# SEO analysis
logger.info("Starting SEO analysis")
seo_analysis = executor.submit(self._analyze_seo, url).result()
if "error" in seo_analysis:
error_msg = f"SEO analysis failed: {seo_analysis['error']}"
logger.error(error_msg)
return {
"success": False,
"error": error_msg,
"error_details": {
"stage": "seo_analysis",
"details": seo_analysis.get("error_details", {})
}
}
results["analysis"]["seo_info"] = seo_analysis
logger.info(f"Analysis completed successfully for {url}")
return results
logger.debug(f"Final results: {json.dumps(results, indent=2)}")
return {
"success": True,
"data": results
}
except Exception as e:
logger.error(f"Error during website analysis: {str(e)}", exc_info=True)
return {"error": str(e)}
error_msg = f"Error during website analysis: {str(e)}"
logger.error(error_msg, exc_info=True)
return {
"success": False,
"error": error_msg,
"error_details": {
"type": type(e).__name__,
"traceback": str(e.__traceback__)
}
}
def _validate_url(self, url: str) -> bool:
"""Validate URL format."""
@@ -149,7 +200,7 @@ class WebsiteAnalyzer:
def _get_basic_info(self, url: str) -> Dict:
"""Get basic website information."""
logger.debug(f"Getting basic info for {url}")
scraping_logger.debug(f"Getting basic info for {url}")
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
@@ -165,13 +216,31 @@ class WebsiteAnalyzer:
"robots_txt": self._get_robots_txt(url),
"sitemap": self._get_sitemap(url)
}
except requests.exceptions.RequestException as e:
error_msg = f"Request error in basic info: {str(e)}"
logger.error(error_msg, exc_info=True)
return {
"error": error_msg,
"error_details": {
"type": "RequestException",
"status_code": getattr(e.response, 'status_code', None) if hasattr(e, 'response') else None,
"url": url
}
}
except Exception as e:
logger.error(f"Error getting basic info: {str(e)}", exc_info=True)
return {"error": str(e)}
error_msg = f"Error getting basic info: {str(e)}"
logger.error(error_msg, exc_info=True)
return {
"error": error_msg,
"error_details": {
"type": type(e).__name__,
"traceback": str(e.__traceback__)
}
}
def _check_ssl(self, domain: str) -> Dict:
"""Check SSL/TLS certificate information."""
logger.debug(f"Checking SSL for {domain}")
scraping_logger.debug(f"Checking SSL for {domain}")
try:
context = ssl.create_default_context()
with socket.create_connection((domain, 443)) as sock:
@@ -190,7 +259,7 @@ class WebsiteAnalyzer:
def _check_dns(self, domain: str) -> Dict:
"""Check DNS records."""
logger.debug(f"Checking DNS for {domain}")
scraping_logger.debug(f"Checking DNS for {domain}")
try:
records = {}
for record_type in ['A', 'AAAA', 'MX', 'NS', 'TXT']:
@@ -200,7 +269,7 @@ class WebsiteAnalyzer:
except dns.resolver.NoAnswer:
records[record_type] = []
except Exception as e:
logger.warning(f"Error resolving {record_type} record: {str(e)}")
scraping_logger.warning(f"Error resolving {record_type} record: {str(e)}")
records[record_type] = []
return records
except Exception as e:
@@ -209,6 +278,7 @@ class WebsiteAnalyzer:
def _get_whois_info(self, domain: str) -> Dict:
"""Get WHOIS information for a domain."""
scraping_logger.debug(f"Getting WHOIS info for {domain}")
try:
w = whois.whois(domain)
@@ -240,7 +310,7 @@ class WebsiteAnalyzer:
def _analyze_content(self, url: str) -> Dict:
"""Analyze website content."""
logger.debug(f"Analyzing content for {url}")
scraping_logger.debug(f"Analyzing content for {url}")
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
@@ -255,6 +325,14 @@ class WebsiteAnalyzer:
# Count headings
headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
heading_counts = {
'h1': len(soup.find_all('h1')),
'h2': len(soup.find_all('h2')),
'h3': len(soup.find_all('h3')),
'h4': len(soup.find_all('h4')),
'h5': len(soup.find_all('h5')),
'h6': len(soup.find_all('h6'))
}
# Count images
images = soup.find_all('img')
@@ -262,22 +340,52 @@ class WebsiteAnalyzer:
# Count links
links = soup.find_all('a')
# Count paragraphs
paragraphs = soup.find_all('p')
return {
"word_count": word_count,
"heading_count": len(headings),
"heading_structure": heading_counts,
"image_count": len(images),
"link_count": len(links),
"paragraph_count": len(paragraphs),
"has_meta_description": bool(self._get_meta_description(soup)),
"has_robots_txt": bool(self._get_robots_txt(url)),
"has_sitemap": bool(self._get_sitemap(url))
}
except requests.exceptions.RequestException as e:
logger.error(f"Request error in content analysis: {str(e)}", exc_info=True)
return {
"word_count": 0,
"heading_count": 0,
"heading_structure": {'h1': 0, 'h2': 0, 'h3': 0, 'h4': 0, 'h5': 0, 'h6': 0},
"image_count": 0,
"link_count": 0,
"paragraph_count": 0,
"has_meta_description": False,
"has_robots_txt": False,
"has_sitemap": False,
"error": str(e)
}
except Exception as e:
logger.error(f"Content analysis error: {str(e)}", exc_info=True)
return {"error": str(e)}
return {
"word_count": 0,
"heading_count": 0,
"heading_structure": {'h1': 0, 'h2': 0, 'h3': 0, 'h4': 0, 'h5': 0, 'h6': 0},
"image_count": 0,
"link_count": 0,
"paragraph_count": 0,
"has_meta_description": False,
"has_robots_txt": False,
"has_sitemap": False,
"error": str(e)
}
def _check_performance(self, url: str) -> Dict:
"""Check website performance metrics."""
logger.debug(f"Checking performance for {url}")
scraping_logger.debug(f"Checking performance for {url}")
try:
start_time = datetime.now()
response = self.session.get(url, timeout=10)
@@ -289,11 +397,29 @@ class WebsiteAnalyzer:
"load_time": load_time,
"status_code": response.status_code,
"content_length": len(response.content),
"headers": dict(response.headers)
"headers": dict(response.headers),
"response_time": response.elapsed.total_seconds()
}
except requests.exceptions.RequestException as e:
logger.error(f"Request error in performance check: {str(e)}", exc_info=True)
return {
"load_time": 0,
"status_code": 0,
"content_length": 0,
"headers": {},
"response_time": 0,
"error": str(e)
}
except Exception as e:
logger.error(f"Performance check error: {str(e)}", exc_info=True)
return {"error": str(e)}
return {
"load_time": 0,
"status_code": 0,
"content_length": 0,
"headers": {},
"response_time": 0,
"error": str(e)
}
def _get_meta_description(self, soup: BeautifulSoup) -> Optional[str]:
"""Extract meta description from HTML."""
@@ -308,7 +434,7 @@ class WebsiteAnalyzer:
if response.status_code == 200:
return response.text
except Exception as e:
logger.warning(f"Error fetching robots.txt: {str(e)}")
scraping_logger.warning(f"Error fetching robots.txt: {str(e)}")
return None
def _get_sitemap(self, url: str) -> Optional[str]:
@@ -319,5 +445,253 @@ class WebsiteAnalyzer:
if response.status_code == 200:
return response.text
except Exception as e:
logger.warning(f"Error fetching sitemap.xml: {str(e)}")
return None
scraping_logger.warning(f"Error fetching sitemap.xml: {str(e)}")
return None
def _analyze_seo(self, url: str) -> Dict:
"""Analyze website SEO."""
try:
# Extract content
content, soup, extract_errors = self._extract_content(url)
if not content or not soup:
return {
"error": "Failed to extract content",
"error_details": {"errors": extract_errors}
}
# Analyze meta tags
meta_analysis = self._analyze_meta_tags(soup)
# Analyze content with AI
content_analysis, recommendations = self._analyze_content_with_ai(content)
# Calculate overall score
meta_score = sum([
1 if meta_analysis.title['status'] == 'good' else 0,
1 if meta_analysis.description['status'] == 'good' else 0,
1 if meta_analysis.keywords['status'] == 'good' else 0,
1 if meta_analysis.has_robots else 0,
1 if meta_analysis.has_sitemap else 0
]) * 20 # Scale to 100
overall_score = (
meta_score * 0.3 + # 30% weight for meta tags
content_analysis.readability_score * 0.3 + # 30% weight for readability
content_analysis.content_quality_score * 0.4 # 40% weight for content quality
)
return {
"overall_score": overall_score,
"meta_tags": meta_analysis.__dict__,
"content": content_analysis.__dict__,
"recommendations": [rec.__dict__ for rec in recommendations]
}
except Exception as e:
error_msg = f"Error in SEO analysis: {str(e)}"
logger.error(error_msg, exc_info=True)
return {
"error": error_msg,
"error_details": {
"type": type(e).__name__,
"traceback": str(e.__traceback__)
}
}
def _extract_content(self, url: str) -> Tuple[Optional[str], Optional[BeautifulSoup], List[str]]:
"""Extract content from URL."""
errors = []
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
return response.text, soup, errors
except requests.RequestException as e:
error_msg = f"Error fetching URL: {str(e)}"
logger.error(error_msg)
errors.append(error_msg)
return None, None, errors
def _analyze_meta_tags(self, soup: BeautifulSoup) -> MetaTagAnalysis:
"""Analyze meta tags using BeautifulSoup."""
# Title analysis
title = soup.title.string if soup.title else ""
title_analysis = {
'status': 'good' if title and 30 <= len(title) <= 60 else 'needs_improvement',
'value': title,
'recommendation': '' if title and 30 <= len(title) <= 60 else 'Title should be between 30-60 characters'
}
# Meta description analysis
meta_desc = soup.find('meta', attrs={'name': 'description'})
desc = meta_desc.get('content', '') if meta_desc else ""
desc_analysis = {
'status': 'good' if desc and 120 <= len(desc) <= 160 else 'needs_improvement',
'value': desc,
'recommendation': '' if desc and 120 <= len(desc) <= 160 else 'Description should be between 120-160 characters'
}
# Keywords analysis
meta_keywords = soup.find('meta', attrs={'name': 'keywords'})
keywords = meta_keywords.get('content', '') if meta_keywords else ""
keywords_analysis = {
'status': 'good' if keywords else 'needs_improvement',
'value': keywords,
'recommendation': '' if keywords else 'Add relevant keywords meta tag'
}
return MetaTagAnalysis(
title=title_analysis,
description=desc_analysis,
keywords=keywords_analysis,
has_robots=bool(soup.find('meta', attrs={'name': 'robots'})),
has_sitemap=bool(soup.find('link', attrs={'rel': 'sitemap'}))
)
def _analyze_content_with_ai(self, content: str) -> Tuple[ContentAnalysis, List[SEORecommendation]]:
"""Analyze content using AI."""
try:
# Prepare prompt for content analysis
prompt = f"""Analyze the following webpage content for SEO and provide a structured analysis:
Content: {content[:4000]}... # Truncate to avoid token limits
Provide analysis in the following format:
1. Word count
2. Heading structure analysis
3. Keyword density for main topics
4. Readability score (0-100)
5. Content quality score (0-100)
6. List of SEO recommendations with priority (high/medium/low), category, issue, recommendation, and impact
Format the response as JSON."""
try:
# Get AI analysis using llm_text_gen
analysis = llm_text_gen(
prompt=prompt,
system_prompt="You are an SEO expert analyzing website content.",
response_format="json_object"
)
if not analysis:
logger.error("Empty response from AI analysis")
return self._get_fallback_analysis(content)
# Create ContentAnalysis object
content_analysis = ContentAnalysis(
word_count=len(content.split()),
headings_structure=analysis.get('heading_structure', {}),
keyword_density=analysis.get('keyword_density', {}),
readability_score=analysis.get('readability_score', 0),
content_quality_score=analysis.get('content_quality_score', 0)
)
# Create recommendations
recommendations = [
SEORecommendation(
priority=rec['priority'],
category=rec['category'],
issue=rec['issue'],
recommendation=rec['recommendation'],
impact=rec['impact']
)
for rec in analysis.get('recommendations', [])
]
return content_analysis, recommendations
except Exception as e:
logger.error(f"Error in AI analysis: {str(e)}")
return self._get_fallback_analysis(content)
except Exception as e:
logger.error(f"Error in AI analysis setup: {str(e)}")
return self._get_fallback_analysis(content)
def _get_fallback_analysis(self, content: str) -> Tuple[ContentAnalysis, List[SEORecommendation]]:
"""Provide fallback analysis when AI analysis is not available."""
try:
# Basic content analysis
words = content.split()
word_count = len(words)
# Simple readability score based on word count
readability_score = min(100, max(0, word_count / 10))
# Basic content quality score
content_quality_score = min(100, max(0, word_count / 20))
# Create basic recommendations
recommendations = [
SEORecommendation(
priority="high",
category="content",
issue="AI analysis unavailable",
recommendation="Consider running the analysis again with a valid API key for more detailed insights",
impact="Limited analysis capabilities"
)
]
return ContentAnalysis(
word_count=word_count,
headings_structure={},
keyword_density={},
readability_score=readability_score,
content_quality_score=content_quality_score
), recommendations
except Exception as e:
logger.error(f"Error in fallback analysis: {str(e)}")
return ContentAnalysis(
word_count=0,
headings_structure={},
keyword_density={},
readability_score=0,
content_quality_score=0
), []
def analyze_website(url: str) -> Dict:
"""
Analyze a website and return comprehensive results.
Args:
url (str): The URL to analyze
Returns:
Dict: Analysis results including various metrics and checks
"""
logger.info(f"Starting website analysis for URL: {url}")
try:
analyzer = WebsiteAnalyzer()
results = analyzer.analyze_website(url)
# Add success status to results
if "error" in results:
error_msg = f"Error in base analysis: {results['error']}"
logger.error(error_msg)
logger.error(f"Error details: {json.dumps(results.get('error_details', {}), indent=2)}")
return {
"success": False,
"error": error_msg,
"error_details": results.get("error_details", {})
}
# Add success status and wrap results
logger.info("Analysis completed successfully")
logger.debug(f"Analysis results: {json.dumps(results, indent=2)}")
return {
"success": True,
"data": results
}
except Exception as e:
error_msg = f"Error in analyze_website: {str(e)}"
logger.error(error_msg, exc_info=True)
return {
"success": False,
"error": error_msg,
"error_details": {
"type": type(e).__name__,
"traceback": str(e.__traceback__)
}
}

View File

@@ -0,0 +1,134 @@
from typing import Dict
import json
class ContentGapAnalyzer:
def __init__(self, analyzer):
self.analyzer = analyzer
def analyze(self, url: str) -> Dict:
"""
Analyze content gaps for a given URL.
Args:
url (str): The URL to analyze
Returns:
Dict: Analysis results including content gaps and recommendations
"""
try:
# Get base analysis
logger.info(f"Starting content gap analysis for URL: {url}")
base_analysis = self.analyzer.analyze_website(url)
# Check for errors in base analysis
if not base_analysis.get("success", False):
error_msg = base_analysis.get("error", "Unknown error in website analysis")
error_details = base_analysis.get("error_details", {})
logger.error(f"Base analysis failed: {error_msg}")
logger.error(f"Error details: {json.dumps(error_details, indent=2)}")
return {
"success": False,
"error": error_msg,
"error_details": error_details,
"stage": "base_analysis"
}
# Extract required sections
analysis_data = base_analysis.get("data", {}).get("analysis", {})
required_sections = ["content_info", "basic_info", "performance"]
missing_sections = [section for section in required_sections if section not in analysis_data]
if missing_sections:
error_msg = f"Missing required analysis sections: {', '.join(missing_sections)}"
logger.error(error_msg)
logger.error(f"Available sections: {list(analysis_data.keys())}")
return {
"success": False,
"error": error_msg,
"error_details": {
"missing_sections": missing_sections,
"available_sections": list(analysis_data.keys())
},
"stage": "section_validation"
}
# Extract content metrics
try:
content_info = analysis_data["content_info"]
basic_info = analysis_data["basic_info"]
performance = analysis_data["performance"]
except KeyError as e:
error_msg = f"Error extracting analysis section: {str(e)}"
logger.error(error_msg)
return {
"success": False,
"error": error_msg,
"error_details": {
"type": "KeyError",
"missing_key": str(e),
"available_keys": list(analysis_data.keys())
},
"stage": "data_extraction"
}
# Analyze content gaps
try:
gaps = self._analyze_content_gaps(content_info, basic_info, performance)
except Exception as e:
error_msg = f"Error analyzing content gaps: {str(e)}"
logger.error(error_msg, exc_info=True)
return {
"success": False,
"error": error_msg,
"error_details": {
"type": type(e).__name__,
"traceback": str(e.__traceback__)
},
"stage": "gap_analysis"
}
# Generate recommendations
try:
recommendations = self._generate_recommendations(gaps)
except Exception as e:
error_msg = f"Error generating recommendations: {str(e)}"
logger.error(error_msg, exc_info=True)
return {
"success": False,
"error": error_msg,
"error_details": {
"type": type(e).__name__,
"traceback": str(e.__traceback__)
},
"stage": "recommendation_generation"
}
return {
"success": True,
"data": {
"content_gaps": gaps,
"recommendations": recommendations,
"metrics": {
"word_count": content_info.get("word_count", 0),
"heading_count": content_info.get("heading_count", 0),
"image_count": content_info.get("image_count", 0),
"link_count": content_info.get("link_count", 0),
"paragraph_count": content_info.get("paragraph_count", 0),
"load_time": performance.get("load_time", 0),
"response_time": performance.get("response_time", 0)
}
}
}
except Exception as e:
error_msg = f"Error in content gap analysis: {str(e)}"
logger.error(error_msg, exc_info=True)
return {
"success": False,
"error": error_msg,
"error_details": {
"type": type(e).__name__,
"traceback": str(e.__traceback__)
},
"stage": "general"
}

View File

@@ -1,233 +0,0 @@
"""SEO analyzer module with AI integration."""
import requests
from bs4 import BeautifulSoup
from datetime import datetime
from typing import Dict, List, Tuple, Optional
from urllib.parse import urlparse
import openai
from loguru import logger
import os
from dotenv import load_dotenv
from .models import (
SEOAnalysisResult,
MetaTagAnalysis,
ContentAnalysis,
SEORecommendation
)
def extract_content(url: str) -> Tuple[Optional[str], Optional[BeautifulSoup], List[str]]:
"""Extract content from URL."""
errors = []
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
return response.text, soup, errors
except requests.RequestException as e:
error_msg = f"Error fetching URL: {str(e)}"
logger.error(error_msg)
errors.append(error_msg)
return None, None, errors
def analyze_meta_tags(soup: BeautifulSoup) -> MetaTagAnalysis:
"""Analyze meta tags using BeautifulSoup."""
# Title analysis
title = soup.title.string if soup.title else ""
title_analysis = {
'status': 'good' if title and 30 <= len(title) <= 60 else 'needs_improvement',
'value': title,
'recommendation': '' if title and 30 <= len(title) <= 60 else 'Title should be between 30-60 characters'
}
# Meta description analysis
meta_desc = soup.find('meta', attrs={'name': 'description'})
desc = meta_desc.get('content', '') if meta_desc else ""
desc_analysis = {
'status': 'good' if desc and 120 <= len(desc) <= 160 else 'needs_improvement',
'value': desc,
'recommendation': '' if desc and 120 <= len(desc) <= 160 else 'Description should be between 120-160 characters'
}
# Keywords analysis
meta_keywords = soup.find('meta', attrs={'name': 'keywords'})
keywords = meta_keywords.get('content', '') if meta_keywords else ""
keywords_analysis = {
'status': 'good' if keywords else 'needs_improvement',
'value': keywords,
'recommendation': '' if keywords else 'Add relevant keywords meta tag'
}
return MetaTagAnalysis(
title=title_analysis,
description=desc_analysis,
keywords=keywords_analysis,
has_robots=bool(soup.find('meta', attrs={'name': 'robots'})),
has_sitemap=bool(soup.find('link', attrs={'rel': 'sitemap'}))
)
def analyze_content_with_ai(content: str) -> Tuple[ContentAnalysis, List[SEORecommendation]]:
"""Analyze content using AI."""
try:
# Load environment variables
load_dotenv()
# Get API key from environment
api_key = os.getenv('OPENAI_API_KEY')
if not api_key:
raise ValueError("OpenAI API key not found in environment variables")
# Initialize OpenAI client
client = openai.OpenAI(api_key=api_key)
# Prepare prompt for content analysis
prompt = f"""Analyze the following webpage content for SEO and provide a structured analysis:
Content: {content[:4000]}... # Truncate to avoid token limits
Provide analysis in the following format:
1. Word count
2. Heading structure analysis
3. Keyword density for main topics
4. Readability score (0-100)
5. Content quality score (0-100)
6. List of SEO recommendations with priority (high/medium/low), category, issue, recommendation, and impact
Format the response as JSON."""
# Get AI analysis
response = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are an SEO expert analyzing website content."},
{"role": "user", "content": prompt}
],
response_format={"type": "json_object"}
)
# Parse AI response
analysis = response.choices[0].message.content
# Create ContentAnalysis object
content_analysis = ContentAnalysis(
word_count=len(content.split()),
headings_structure=analysis.get('heading_structure', {}),
keyword_density=analysis.get('keyword_density', {}),
readability_score=analysis.get('readability_score', 0),
content_quality_score=analysis.get('content_quality_score', 0)
)
# Create recommendations
recommendations = [
SEORecommendation(
priority=rec['priority'],
category=rec['category'],
issue=rec['issue'],
recommendation=rec['recommendation'],
impact=rec['impact']
)
for rec in analysis.get('recommendations', [])
]
return content_analysis, recommendations
except Exception as e:
logger.error(f"Error in AI analysis: {str(e)}")
return ContentAnalysis(
word_count=len(content.split()),
headings_structure={},
keyword_density={},
readability_score=0,
content_quality_score=0
), []
def analyze_seo(url: str) -> SEOAnalysisResult:
"""Main function to analyze website SEO."""
errors = []
warnings = []
# Validate URL
try:
parsed_url = urlparse(url)
if not all([parsed_url.scheme, parsed_url.netloc]):
errors.append("Invalid URL format")
raise ValueError("Invalid URL format")
except Exception as e:
errors.append(f"URL parsing error: {str(e)}")
return SEOAnalysisResult(
url=url,
analyzed_at=datetime.now(),
overall_score=0,
meta_tags=None,
content=None,
recommendations=[],
errors=errors,
warnings=warnings,
success=False
)
# Extract content
content, soup, extract_errors = extract_content(url)
errors.extend(extract_errors)
if not content or not soup:
return SEOAnalysisResult(
url=url,
analyzed_at=datetime.now(),
overall_score=0,
meta_tags=None,
content=None,
recommendations=[],
errors=errors,
warnings=warnings,
success=False
)
try:
# Analyze meta tags
meta_analysis = analyze_meta_tags(soup)
# Analyze content with AI
content_analysis, recommendations = analyze_content_with_ai(content)
# Calculate overall score
meta_score = sum([
1 if meta_analysis.title['status'] == 'good' else 0,
1 if meta_analysis.description['status'] == 'good' else 0,
1 if meta_analysis.keywords['status'] == 'good' else 0,
1 if meta_analysis.has_robots else 0,
1 if meta_analysis.has_sitemap else 0
]) * 20 # Scale to 100
overall_score = (
meta_score * 0.3 + # 30% weight for meta tags
content_analysis.readability_score * 0.3 + # 30% weight for readability
content_analysis.content_quality_score * 0.4 # 40% weight for content quality
)
return SEOAnalysisResult(
url=url,
analyzed_at=datetime.now(),
overall_score=overall_score,
meta_tags=meta_analysis,
content=content_analysis,
recommendations=recommendations,
errors=errors,
warnings=warnings,
success=True
)
except Exception as e:
error_msg = f"Error in SEO analysis: {str(e)}"
logger.error(error_msg)
errors.append(error_msg)
return SEOAnalysisResult(
url=url,
analyzed_at=datetime.now(),
overall_score=0,
meta_tags=None,
content=None,
recommendations=[],
errors=errors,
warnings=warnings,
success=False
)