mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 01:51:12 -05:00
341 lines
12 KiB
Python
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
|