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