From analysis_3.md - Strategie 3: SEO-Daten & Backlinks: 'SEO-Agenturen suchen Domains wegen der Power (Backlinks). Solche Domains sind für SEOs 100-500€ wert, auch wenn der Name hässlich ist.' BACKEND: - Model: DomainSEOData for caching SEO metrics - Service: seo_analyzer.py with Moz API integration - Falls back to estimation if no API keys - Detects notable links (Wikipedia, .gov, .edu, news) - Calculates SEO value estimate - API: /seo endpoints (Tycoon-only access) FRONTEND: - /command/seo page with full SEO analysis - Upgrade prompt for non-Tycoon users - Notable links display (Wikipedia, .gov, .edu, news) - Top backlinks with authority scores - Recent searches saved locally SIDEBAR: - Added 'SEO Juice' nav item with 'Tycoon' badge DOCS: - Updated DATABASE_MIGRATIONS.md with domain_seo_data table - Added SEO API endpoints documentation - Added Moz API environment variables info
117 lines
4.1 KiB
Python
117 lines
4.1 KiB
Python
"""
|
|
SEO Data models for the "SEO Juice Detector" feature.
|
|
|
|
This implements "Strategie 3: SEO-Daten & Backlinks" from analysis_3.md:
|
|
"SEO-Agenturen suchen Domains nicht wegen dem Namen, sondern wegen der Power (Backlinks).
|
|
Wenn eine Domain droppt, prüfst du nicht nur den Namen, sondern ob Backlinks existieren."
|
|
|
|
This is a TYCOON-ONLY feature ($29/month).
|
|
|
|
DATABASE TABLE TO CREATE:
|
|
- domain_seo_data - Cached SEO metrics for domains
|
|
|
|
Run migrations: alembic upgrade head
|
|
"""
|
|
from datetime import datetime
|
|
from typing import Optional, List
|
|
from sqlalchemy import String, DateTime, Float, Integer, Text, ForeignKey, Boolean, JSON
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class DomainSEOData(Base):
|
|
"""
|
|
Cached SEO data for domains.
|
|
|
|
Stores backlink data, domain authority, and other SEO metrics
|
|
from Moz API or alternative sources.
|
|
|
|
From analysis_3.md:
|
|
"Domain `alte-bäckerei-münchen.de` ist frei.
|
|
Hat Links von `sueddeutsche.de` und `wikipedia.org`."
|
|
"""
|
|
|
|
__tablename__ = "domain_seo_data"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
domain: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
|
|
|
# Moz metrics
|
|
domain_authority: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 0-100
|
|
page_authority: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 0-100
|
|
spam_score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 0-100
|
|
|
|
# Backlink data
|
|
total_backlinks: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
referring_domains: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
|
|
# Top backlinks (JSON array of {domain, authority, type})
|
|
top_backlinks: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
|
|
|
# Notable backlinks (high-authority sites)
|
|
notable_backlinks: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # Comma-separated
|
|
has_wikipedia_link: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
has_gov_link: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
has_edu_link: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
has_news_link: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
|
|
# Estimated value based on SEO
|
|
seo_value_estimate: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
|
|
|
|
# Data source
|
|
data_source: Mapped[str] = mapped_column(String(50), default="moz") # moz, ahrefs, majestic, estimated
|
|
|
|
# Cache management
|
|
last_updated: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
|
|
|
# Request tracking
|
|
fetch_count: Mapped[int] = mapped_column(Integer, default=0)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<DomainSEOData {self.domain} DA:{self.domain_authority}>"
|
|
|
|
@property
|
|
def is_expired(self) -> bool:
|
|
if not self.expires_at:
|
|
return True
|
|
return datetime.utcnow() > self.expires_at
|
|
|
|
@property
|
|
def seo_score(self) -> int:
|
|
"""Calculate overall SEO score (0-100)."""
|
|
if not self.domain_authority:
|
|
return 0
|
|
|
|
score = self.domain_authority
|
|
|
|
# Boost for notable links
|
|
if self.has_wikipedia_link:
|
|
score = min(100, score + 10)
|
|
if self.has_gov_link:
|
|
score = min(100, score + 5)
|
|
if self.has_edu_link:
|
|
score = min(100, score + 5)
|
|
if self.has_news_link:
|
|
score = min(100, score + 3)
|
|
|
|
# Penalty for spam
|
|
if self.spam_score and self.spam_score > 30:
|
|
score = max(0, score - (self.spam_score // 5))
|
|
|
|
return score
|
|
|
|
@property
|
|
def value_category(self) -> str:
|
|
"""Categorize SEO value for display."""
|
|
score = self.seo_score
|
|
if score >= 60:
|
|
return "High Value"
|
|
elif score >= 40:
|
|
return "Medium Value"
|
|
elif score >= 20:
|
|
return "Low Value"
|
|
return "Minimal"
|
|
|