Files
thrilltrack-explorer/django-backend/apps/core/utils/seo.py

341 lines
12 KiB
Python

"""
SEO Meta Tag Generation Utilities
Generates comprehensive meta tags for social sharing (OpenGraph, Twitter Cards),
search engines (structured data), and general SEO optimization.
"""
from typing import Dict, Optional
from django.conf import settings
class SEOTags:
"""Generate comprehensive SEO meta tags for any page."""
BASE_URL = getattr(settings, 'SITE_URL', 'https://thrillwiki.com')
DEFAULT_OG_IMAGE = f"{BASE_URL}/static/images/og-default.png"
TWITTER_HANDLE = "@thrillwiki"
SITE_NAME = "ThrillWiki"
@classmethod
def for_park(cls, park) -> Dict[str, str]:
"""
Generate meta tags for a park page.
Args:
park: Park model instance
Returns:
Dictionary of meta tags for HTML head
"""
title = f"{park.name} - Theme Park Database | ThrillWiki"
description = f"Explore {park.name} in {park.locality.name}, {park.country.name}. View rides, reviews, photos, and history on ThrillWiki."
og_image = cls._get_og_image_url('park', str(park.id))
url = f"{cls.BASE_URL}/parks/{park.slug}/"
return {
# Basic Meta
'title': title,
'description': description,
'keywords': f"{park.name}, theme park, amusement park, {park.locality.name}, {park.country.name}",
# OpenGraph (Facebook, LinkedIn, Discord)
'og:title': park.name,
'og:description': description,
'og:type': 'website',
'og:url': url,
'og:image': og_image,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
# Twitter Card
'twitter:card': 'summary_large_image',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': park.name,
'twitter:description': description,
'twitter:image': og_image,
# Additional
'canonical': url,
}
@classmethod
def for_ride(cls, ride) -> Dict[str, str]:
"""
Generate meta tags for a ride page.
Args:
ride: Ride model instance
Returns:
Dictionary of meta tags for HTML head
"""
title = f"{ride.name} at {ride.park.name} | ThrillWiki"
# Build description with available details
description_parts = [
f"{ride.name} is a {ride.ride_type.name}",
f"at {ride.park.name}",
]
if ride.opened_year:
description_parts.append(f"Built in {ride.opened_year}")
if ride.manufacturer:
description_parts.append(f"by {ride.manufacturer.name}")
description = ". ".join(description_parts) + ". Read reviews and view photos."
og_image = cls._get_og_image_url('ride', str(ride.id))
url = f"{cls.BASE_URL}/parks/{ride.park.slug}/rides/{ride.slug}/"
keywords_parts = [
ride.name,
ride.ride_type.name,
ride.park.name,
]
if ride.manufacturer:
keywords_parts.append(ride.manufacturer.name)
keywords_parts.extend(['roller coaster', 'theme park ride'])
return {
'title': title,
'description': description,
'keywords': ', '.join(keywords_parts),
# OpenGraph
'og:title': f"{ride.name} at {ride.park.name}",
'og:description': description,
'og:type': 'article',
'og:url': url,
'og:image': og_image,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
# Twitter
'twitter:card': 'summary_large_image',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': f"{ride.name} at {ride.park.name}",
'twitter:description': description,
'twitter:image': og_image,
'canonical': url,
}
@classmethod
def for_company(cls, company) -> Dict[str, str]:
"""
Generate meta tags for a manufacturer/company page.
Args:
company: Company model instance
Returns:
Dictionary of meta tags for HTML head
"""
# Get company type name safely
company_type_name = company.company_types.first().name if company.company_types.exists() else "Company"
title = f"{company.name} - {company_type_name} | ThrillWiki"
description = f"{company.name} is a {company_type_name}. View their rides, history, and contributions to the theme park industry."
url = f"{cls.BASE_URL}/manufacturers/{company.slug}/"
return {
'title': title,
'description': description,
'keywords': f"{company.name}, {company_type_name}, theme park manufacturer, ride manufacturer",
# OpenGraph
'og:title': company.name,
'og:description': description,
'og:type': 'website',
'og:url': url,
'og:image': cls.DEFAULT_OG_IMAGE,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
# Twitter
'twitter:card': 'summary',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': company.name,
'twitter:description': description,
'twitter:image': cls.DEFAULT_OG_IMAGE,
'canonical': url,
}
@classmethod
def for_ride_model(cls, model) -> Dict[str, str]:
"""
Generate meta tags for a ride model page.
Args:
model: RideModel model instance
Returns:
Dictionary of meta tags for HTML head
"""
title = f"{model.name} by {model.manufacturer.name} | ThrillWiki"
description = f"The {model.name} is a {model.ride_type.name} model manufactured by {model.manufacturer.name}. View installations and specifications."
url = f"{cls.BASE_URL}/models/{model.slug}/"
return {
'title': title,
'description': description,
'keywords': f"{model.name}, {model.manufacturer.name}, {model.ride_type.name}, ride model, theme park",
# OpenGraph
'og:title': f"{model.name} by {model.manufacturer.name}",
'og:description': description,
'og:type': 'website',
'og:url': url,
'og:image': cls.DEFAULT_OG_IMAGE,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
# Twitter
'twitter:card': 'summary',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': f"{model.name} by {model.manufacturer.name}",
'twitter:description': description,
'twitter:image': cls.DEFAULT_OG_IMAGE,
'canonical': url,
}
@classmethod
def for_home(cls) -> Dict[str, str]:
"""Generate meta tags for home page."""
title = "ThrillWiki - The Ultimate Theme Park & Roller Coaster Database"
description = "Explore thousands of theme parks and roller coasters worldwide. Read reviews, view photos, track your ride credits, and discover your next adventure."
return {
'title': title,
'description': description,
'keywords': 'theme parks, roller coasters, amusement parks, ride database, coaster enthusiasts, thrillwiki',
'og:title': title,
'og:description': description,
'og:type': 'website',
'og:url': cls.BASE_URL,
'og:image': cls.DEFAULT_OG_IMAGE,
'og:image:width': '1200',
'og:image:height': '630',
'og:site_name': cls.SITE_NAME,
'og:locale': 'en_US',
'twitter:card': 'summary_large_image',
'twitter:site': cls.TWITTER_HANDLE,
'twitter:title': title,
'twitter:description': description,
'twitter:image': cls.DEFAULT_OG_IMAGE,
'canonical': cls.BASE_URL,
}
@staticmethod
def _get_og_image_url(entity_type: str, entity_id: str) -> str:
"""
Generate dynamic OG image URL.
Args:
entity_type: Type of entity (park, ride, company, model)
entity_id: Entity ID
Returns:
URL to dynamic OG image endpoint
"""
# Use existing ssrOG endpoint
return f"{SEOTags.BASE_URL}/api/og?type={entity_type}&id={entity_id}"
@classmethod
def structured_data_for_park(cls, park) -> dict:
"""
Generate JSON-LD structured data for a park.
Args:
park: Park model instance
Returns:
Dictionary for JSON-LD script tag
"""
data = {
"@context": "https://schema.org",
"@type": "TouristAttraction",
"name": park.name,
"description": f"Theme park in {park.locality.name}, {park.country.name}",
"url": f"{cls.BASE_URL}/parks/{park.slug}/",
"image": cls._get_og_image_url('park', str(park.id)),
"address": {
"@type": "PostalAddress",
"addressLocality": park.locality.name,
"addressCountry": park.country.code,
},
}
# Add geo coordinates if available
if hasattr(park, 'latitude') and hasattr(park, 'longitude') and park.latitude and park.longitude:
data["geo"] = {
"@type": "GeoCoordinates",
"latitude": str(park.latitude),
"longitude": str(park.longitude),
}
# Add aggregate rating if available
if hasattr(park, 'review_count') and park.review_count > 0:
data["aggregateRating"] = {
"@type": "AggregateRating",
"ratingValue": str(park.average_rating),
"reviewCount": park.review_count,
}
return data
@classmethod
def structured_data_for_ride(cls, ride) -> dict:
"""
Generate JSON-LD structured data for a ride.
Args:
ride: Ride model instance
Returns:
Dictionary for JSON-LD script tag
"""
data = {
"@context": "https://schema.org",
"@type": "Product",
"name": ride.name,
"description": f"{ride.name} is a {ride.ride_type.name} at {ride.park.name}",
"url": f"{cls.BASE_URL}/parks/{ride.park.slug}/rides/{ride.slug}/",
"image": cls._get_og_image_url('ride', str(ride.id)),
}
# Add manufacturer if available
if ride.manufacturer:
data["manufacturer"] = {
"@type": "Organization",
"name": ride.manufacturer.name,
}
# Add aggregate rating if available
if hasattr(ride, 'review_count') and ride.review_count > 0:
data["aggregateRating"] = {
"@type": "AggregateRating",
"ratingValue": str(ride.average_rating),
"reviewCount": ride.review_count,
}
return data