mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 23:51:13 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
0
django-backend/apps/core/__init__.py
Normal file
0
django-backend/apps/core/__init__.py
Normal file
11
django-backend/apps/core/apps.py
Normal file
11
django-backend/apps/core/apps.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Core app configuration.
|
||||
"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.core'
|
||||
verbose_name = 'Core'
|
||||
194
django-backend/apps/core/migrations/0001_initial.py
Normal file
194
django-backend/apps/core/migrations/0001_initial.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# Generated by Django 4.2.8 on 2025-11-08 16:35
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import django_lifecycle.mixins
|
||||
import model_utils.fields
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Country",
|
||||
fields=[
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255, unique=True)),
|
||||
(
|
||||
"code",
|
||||
models.CharField(
|
||||
help_text="ISO 3166-1 alpha-2 country code",
|
||||
max_length=2,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"code3",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="ISO 3166-1 alpha-3 country code",
|
||||
max_length=3,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "countries",
|
||||
"db_table": "countries",
|
||||
"ordering": ["name"],
|
||||
},
|
||||
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Subdivision",
|
||||
fields=[
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
(
|
||||
"code",
|
||||
models.CharField(
|
||||
help_text="ISO 3166-2 subdivision code (without country prefix)",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"subdivision_type",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Type of subdivision (state, province, region, etc.)",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
(
|
||||
"country",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="subdivisions",
|
||||
to="core.country",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "subdivisions",
|
||||
"ordering": ["country", "name"],
|
||||
"unique_together": {("country", "code")},
|
||||
},
|
||||
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Locality",
|
||||
fields=[
|
||||
(
|
||||
"created",
|
||||
model_utils.fields.AutoCreatedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="created",
|
||||
),
|
||||
),
|
||||
(
|
||||
"modified",
|
||||
model_utils.fields.AutoLastModifiedField(
|
||||
default=django.utils.timezone.now,
|
||||
editable=False,
|
||||
verbose_name="modified",
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
(
|
||||
"latitude",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=6, max_digits=9, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"longitude",
|
||||
models.DecimalField(
|
||||
blank=True, decimal_places=6, max_digits=9, null=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"subdivision",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="localities",
|
||||
to="core.subdivision",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "localities",
|
||||
"db_table": "localities",
|
||||
"ordering": ["subdivision", "name"],
|
||||
"indexes": [
|
||||
models.Index(
|
||||
fields=["subdivision", "name"],
|
||||
name="localities_subdivi_675d5a_idx",
|
||||
)
|
||||
],
|
||||
},
|
||||
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
|
||||
),
|
||||
]
|
||||
0
django-backend/apps/core/migrations/__init__.py
Normal file
0
django-backend/apps/core/migrations/__init__.py
Normal file
240
django-backend/apps/core/models.py
Normal file
240
django-backend/apps/core/models.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Core base models and utilities for ThrillWiki.
|
||||
These abstract models provide common functionality for all entities.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from django.db import models
|
||||
from model_utils.models import TimeStampedModel
|
||||
from django_lifecycle import LifecycleModel, hook, AFTER_CREATE, AFTER_UPDATE
|
||||
from dirtyfields import DirtyFieldsMixin
|
||||
|
||||
|
||||
class BaseModel(LifecycleModel, TimeStampedModel):
|
||||
"""
|
||||
Abstract base model for all entities.
|
||||
|
||||
Provides:
|
||||
- UUID primary key
|
||||
- created_at and updated_at timestamps (from TimeStampedModel)
|
||||
- Lifecycle hooks for versioning
|
||||
"""
|
||||
id = models.UUIDField(
|
||||
primary_key=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}({self.id})"
|
||||
|
||||
|
||||
class VersionedModel(DirtyFieldsMixin, BaseModel):
|
||||
"""
|
||||
Abstract base model for entities that track field changes.
|
||||
|
||||
Uses DirtyFieldsMixin to track which fields changed.
|
||||
History tracking is now handled automatically by pghistory decorators.
|
||||
|
||||
Note: This class is kept for backwards compatibility and the DirtyFieldsMixin
|
||||
functionality, but no longer triggers custom versioning.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
# Location Models
|
||||
|
||||
class Country(BaseModel):
|
||||
"""
|
||||
Country reference data (ISO 3166-1).
|
||||
|
||||
Examples: United States, Canada, United Kingdom, etc.
|
||||
"""
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
code = models.CharField(
|
||||
max_length=2,
|
||||
unique=True,
|
||||
help_text="ISO 3166-1 alpha-2 country code"
|
||||
)
|
||||
code3 = models.CharField(
|
||||
max_length=3,
|
||||
blank=True,
|
||||
help_text="ISO 3166-1 alpha-3 country code"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'countries'
|
||||
ordering = ['name']
|
||||
verbose_name_plural = 'countries'
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Subdivision(BaseModel):
|
||||
"""
|
||||
State/Province/Region reference data (ISO 3166-2).
|
||||
|
||||
Examples: California, Ontario, England, etc.
|
||||
"""
|
||||
country = models.ForeignKey(
|
||||
Country,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='subdivisions'
|
||||
)
|
||||
name = models.CharField(max_length=255)
|
||||
code = models.CharField(
|
||||
max_length=10,
|
||||
help_text="ISO 3166-2 subdivision code (without country prefix)"
|
||||
)
|
||||
subdivision_type = models.CharField(
|
||||
max_length=50,
|
||||
blank=True,
|
||||
help_text="Type of subdivision (state, province, region, etc.)"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'subdivisions'
|
||||
ordering = ['country', 'name']
|
||||
unique_together = [['country', 'code']]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}, {self.country.code}"
|
||||
|
||||
|
||||
class Locality(BaseModel):
|
||||
"""
|
||||
City/Town reference data.
|
||||
|
||||
Examples: Los Angeles, Toronto, London, etc.
|
||||
"""
|
||||
subdivision = models.ForeignKey(
|
||||
Subdivision,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='localities'
|
||||
)
|
||||
name = models.CharField(max_length=255)
|
||||
latitude = models.DecimalField(
|
||||
max_digits=9,
|
||||
decimal_places=6,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
longitude = models.DecimalField(
|
||||
max_digits=9,
|
||||
decimal_places=6,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'localities'
|
||||
ordering = ['subdivision', 'name']
|
||||
verbose_name_plural = 'localities'
|
||||
indexes = [
|
||||
models.Index(fields=['subdivision', 'name']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}, {self.subdivision.code}"
|
||||
|
||||
@property
|
||||
def full_location(self):
|
||||
"""Return full location string: City, State, Country"""
|
||||
return f"{self.name}, {self.subdivision.name}, {self.subdivision.country.name}"
|
||||
|
||||
|
||||
# Date Precision Tracking
|
||||
|
||||
class DatePrecisionMixin(models.Model):
|
||||
"""
|
||||
Mixin for models that need to track date precision.
|
||||
|
||||
Allows tracking whether a date is known to year, month, or day precision.
|
||||
This is important for historical records where exact dates may not be known.
|
||||
"""
|
||||
|
||||
DATE_PRECISION_CHOICES = [
|
||||
('year', 'Year'),
|
||||
('month', 'Month'),
|
||||
('day', 'Day'),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def add_date_precision_field(cls, field_name):
|
||||
"""
|
||||
Helper to add a precision field for a date field.
|
||||
|
||||
Usage in subclass:
|
||||
opening_date = models.DateField(null=True, blank=True)
|
||||
opening_date_precision = models.CharField(...)
|
||||
"""
|
||||
return models.CharField(
|
||||
max_length=20,
|
||||
choices=cls.DATE_PRECISION_CHOICES,
|
||||
default='day',
|
||||
help_text=f"Precision level for {field_name}"
|
||||
)
|
||||
|
||||
|
||||
# Soft Delete Mixin
|
||||
|
||||
class SoftDeleteMixin(models.Model):
|
||||
"""
|
||||
Mixin for soft-deletable models.
|
||||
|
||||
Instead of actually deleting records, mark them as deleted.
|
||||
This preserves data integrity and allows for undelete functionality.
|
||||
"""
|
||||
is_deleted = models.BooleanField(default=False, db_index=True)
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
deleted_by = models.ForeignKey(
|
||||
'users.User',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='%(class)s_deletions'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def soft_delete(self, user=None):
|
||||
"""Mark this record as deleted"""
|
||||
from django.utils import timezone
|
||||
self.is_deleted = True
|
||||
self.deleted_at = timezone.now()
|
||||
if user:
|
||||
self.deleted_by = user
|
||||
self.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by'])
|
||||
|
||||
def undelete(self):
|
||||
"""Restore a soft-deleted record"""
|
||||
self.is_deleted = False
|
||||
self.deleted_at = None
|
||||
self.deleted_by = None
|
||||
self.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by'])
|
||||
|
||||
|
||||
# Model Managers
|
||||
|
||||
class ActiveManager(models.Manager):
|
||||
"""Manager that filters out soft-deleted records by default"""
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(is_deleted=False)
|
||||
|
||||
|
||||
class AllObjectsManager(models.Manager):
|
||||
"""Manager that includes all records, even soft-deleted ones"""
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset()
|
||||
119
django-backend/apps/core/sitemaps.py
Normal file
119
django-backend/apps/core/sitemaps.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Django Sitemaps for SEO
|
||||
|
||||
Generates XML sitemaps for search engine crawlers to discover and index content.
|
||||
"""
|
||||
|
||||
from django.contrib.sitemaps import Sitemap
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.entities.models import Park, Ride, Company, RideModel
|
||||
|
||||
|
||||
class ParkSitemap(Sitemap):
|
||||
"""Sitemap for theme parks."""
|
||||
|
||||
changefreq = "weekly"
|
||||
priority = 0.9
|
||||
|
||||
def items(self):
|
||||
"""Return all active parks."""
|
||||
return Park.objects.filter(is_active=True).order_by('-updated')
|
||||
|
||||
def lastmod(self, obj):
|
||||
"""Return last modification date."""
|
||||
return obj.updated
|
||||
|
||||
def location(self, obj):
|
||||
"""Return URL for park."""
|
||||
return f'/parks/{obj.slug}/'
|
||||
|
||||
|
||||
class RideSitemap(Sitemap):
|
||||
"""Sitemap for rides."""
|
||||
|
||||
changefreq = "weekly"
|
||||
priority = 0.8
|
||||
|
||||
def items(self):
|
||||
"""Return all active rides."""
|
||||
return Ride.objects.filter(
|
||||
is_active=True
|
||||
).select_related('park').order_by('-updated')
|
||||
|
||||
def lastmod(self, obj):
|
||||
"""Return last modification date."""
|
||||
return obj.updated
|
||||
|
||||
def location(self, obj):
|
||||
"""Return URL for ride."""
|
||||
return f'/parks/{obj.park.slug}/rides/{obj.slug}/'
|
||||
|
||||
|
||||
class CompanySitemap(Sitemap):
|
||||
"""Sitemap for companies/manufacturers."""
|
||||
|
||||
changefreq = "monthly"
|
||||
priority = 0.6
|
||||
|
||||
def items(self):
|
||||
"""Return all active companies."""
|
||||
return Company.objects.filter(is_active=True).order_by('-updated')
|
||||
|
||||
def lastmod(self, obj):
|
||||
"""Return last modification date."""
|
||||
return obj.updated
|
||||
|
||||
def location(self, obj):
|
||||
"""Return URL for company."""
|
||||
return f'/manufacturers/{obj.slug}/'
|
||||
|
||||
|
||||
class RideModelSitemap(Sitemap):
|
||||
"""Sitemap for ride models."""
|
||||
|
||||
changefreq = "monthly"
|
||||
priority = 0.7
|
||||
|
||||
def items(self):
|
||||
"""Return all active ride models."""
|
||||
return RideModel.objects.filter(
|
||||
is_active=True
|
||||
).select_related('manufacturer').order_by('-updated')
|
||||
|
||||
def lastmod(self, obj):
|
||||
"""Return last modification date."""
|
||||
return obj.updated
|
||||
|
||||
def location(self, obj):
|
||||
"""Return URL for ride model."""
|
||||
return f'/models/{obj.slug}/'
|
||||
|
||||
|
||||
class StaticSitemap(Sitemap):
|
||||
"""Sitemap for static pages."""
|
||||
|
||||
changefreq = "monthly"
|
||||
priority = 0.5
|
||||
|
||||
def items(self):
|
||||
"""Return list of static pages."""
|
||||
return ['home', 'about', 'privacy', 'terms']
|
||||
|
||||
def location(self, item):
|
||||
"""Return URL for static page."""
|
||||
if item == 'home':
|
||||
return '/'
|
||||
return f'/{item}/'
|
||||
|
||||
def changefreq(self, item):
|
||||
"""Home page changes more frequently."""
|
||||
if item == 'home':
|
||||
return 'daily'
|
||||
return 'monthly'
|
||||
|
||||
def priority(self, item):
|
||||
"""Home page has higher priority."""
|
||||
if item == 'home':
|
||||
return 1.0
|
||||
return 0.5
|
||||
340
django-backend/apps/core/utils/seo.py
Normal file
340
django-backend/apps/core/utils/seo.py
Normal file
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user