diff --git a/backend/app/routers/dive_sites.py b/backend/app/routers/dive_sites.py
index 0ca2c55f..3d95f6a7 100644
--- a/backend/app/routers/dive_sites.py
+++ b/backend/app/routers/dive_sites.py
@@ -3680,12 +3680,12 @@ async def get_dive_site_dives(
request: Request,
dive_site_id: int,
limit: int = Query(10, ge=1, le=50),
+ sort_by: str = Query("recent", description="Sort order: 'recent' (dive date desc) or 'rating' (rating desc, then date desc)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user_optional)
):
"""
- Get top dives for a specific dive site, ordered by rating (descending).
- If no rating is available, returns the first 10 dives.
+ Get dives for a specific dive site, ordered by recent date (descending) or rating (descending).
"""
# Check if dive site exists
dive_site = db.query(DiveSite).filter(DiveSite.id == dive_site_id).first()
@@ -3710,13 +3710,18 @@ async def get_dive_site_dives(
)
)
- # Order by rating (descending) first, then by dive date (descending)
- # Dives with no rating will be ordered by dive date
- query = query.order_by(
- desc(Dive.user_rating), # Highest rating first
- desc(Dive.dive_date), # Most recent first
- desc(Dive.dive_time) # Most recent time first
- )
+ # Order by rating or recent date
+ if sort_by == "rating":
+ query = query.order_by(
+ desc(Dive.user_rating), # Highest rating first
+ desc(Dive.dive_date), # Most recent first
+ desc(Dive.dive_time) # Most recent time first
+ )
+ else:
+ query = query.order_by(
+ desc(Dive.dive_date), # Most recent first
+ desc(Dive.dive_time) # Most recent time first
+ )
# Limit results
dives = query.limit(limit).all()
diff --git a/backend/app/routers/seo.py b/backend/app/routers/seo.py
index f497844d..271bd21b 100644
--- a/backend/app/routers/seo.py
+++ b/backend/app/routers/seo.py
@@ -10,10 +10,20 @@
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
import httpx
-from sqlalchemy.orm import Session, joinedload
+from sqlalchemy.orm import Session, joinedload, selectinload
from app.database import get_db
-from app.models import Dive, DiveRoute, DiveSite, DivingCenter, DivingOrganization, User, MediaType
+from app.models import (
+ Dive,
+ DiveRoute,
+ DiveSite,
+ DivingCenter,
+ DivingOrganization,
+ User,
+ MediaType,
+ DiveSiteTag,
+ CenterDiveSite,
+)
def make_absolute_url(url: str, base_url: str) -> str:
if not url:
@@ -278,7 +288,15 @@ async def get_prerendered_page(request: Request, path: str, db: Session = Depend
site = (
db.query(DiveSite)
.filter(DiveSite.id == site_id, DiveSite.status == "approved", DiveSite.deleted_at.is_(None))
- .options(joinedload(DiveSite.difficulty), joinedload(DiveSite.ratings), joinedload(DiveSite.media))
+ .options(
+ joinedload(DiveSite.difficulty),
+ joinedload(DiveSite.ratings),
+ joinedload(DiveSite.media),
+ selectinload(DiveSite.aliases),
+ selectinload(DiveSite.tags).joinedload(DiveSiteTag.tag),
+ selectinload(DiveSite.routes),
+ selectinload(DiveSite.center_relationships).joinedload(CenterDiveSite.diving_center),
+ )
.first()
)
if not site:
diff --git a/backend/generate_static_content.py b/backend/generate_static_content.py
index c3ca9fed..8f49318c 100644
--- a/backend/generate_static_content.py
+++ b/backend/generate_static_content.py
@@ -12,9 +12,20 @@
if current_dir not in sys.path:
sys.path.append(current_dir)
-from sqlalchemy.orm import Session, joinedload
+from sqlalchemy.orm import Session, joinedload, selectinload
from app.database import SessionLocal
-from app.models import DiveSite, DiveRoute, DivingCenter, Dive, ParsedDiveTrip, User, DivingOrganization, CertificationLevel
+from app.models import (
+ DiveSite,
+ DiveRoute,
+ DivingCenter,
+ Dive,
+ ParsedDiveTrip,
+ User,
+ DivingOrganization,
+ CertificationLevel,
+ DiveSiteTag,
+ CenterDiveSite,
+)
from app.seo_geo import (
distinct_approved_countries,
distinct_approved_regions_by_country,
@@ -192,7 +203,19 @@ def generate_content(db: Session, r2_client=None):
BASE_URL = CANONICAL_BASE_URL.rstrip("/")
# Data gathering
- sites = db.query(DiveSite).filter(DiveSite.status == 'approved').all()
+ sites = (
+ db.query(DiveSite)
+ .filter(DiveSite.status == 'approved', DiveSite.deleted_at.is_(None))
+ .options(
+ joinedload(DiveSite.difficulty),
+ joinedload(DiveSite.ratings),
+ selectinload(DiveSite.aliases),
+ selectinload(DiveSite.tags).joinedload(DiveSiteTag.tag),
+ selectinload(DiveSite.routes),
+ selectinload(DiveSite.center_relationships).joinedload(CenterDiveSite.diving_center),
+ )
+ .all()
+ )
routes = db.query(DiveRoute).filter(DiveRoute.deleted_at == None).all()
centers = db.query(DivingCenter).all()
# LLM markdown: all public logs; sitemap uses the substantial subset below
@@ -222,6 +245,17 @@ def generate_content(db: Session, r2_client=None):
content_sites.append(f"- **Max Depth**: {site.max_depth}m\n")
if site.difficulty:
content_sites.append(f"- **Difficulty**: {site.difficulty.label}\n")
+ if site.ratings:
+ avg_score = sum(r.score for r in site.ratings) / len(site.ratings)
+ content_sites.append(f"- **Rating**: {avg_score:.1f}/10 ({len(site.ratings)} reviews)\n")
+ if site.aliases:
+ alias_list = [a.alias for a in site.aliases if getattr(a, "alias", None)]
+ if alias_list:
+ content_sites.append(f"- **Also Known As**: {', '.join(alias_list)}\n")
+ if site.tags:
+ tag_list = [t.tag.name for t in site.tags if getattr(t, "tag", None) and getattr(t.tag, "name", None)]
+ if tag_list:
+ content_sites.append(f"- **Tags**: {', '.join(tag_list)}\n")
# Description & Details
if site.description:
@@ -236,6 +270,27 @@ def generate_content(db: Session, r2_client=None):
if site.access_instructions:
content_sites.append(f"\n**Access**:\n{site.access_instructions}\n")
+ if site.center_relationships:
+ centers_list = [rel.diving_center for rel in site.center_relationships if getattr(rel, "diving_center", None)]
+ if centers_list:
+ content_sites.append("\n**Associated Diving Centers**:\n")
+ for center in centers_list:
+ c_slug = get_diving_center_slug(center)
+ c_url = f"{BASE_URL}/diving-centers/{center.id}/{c_slug}" if c_slug else f"{BASE_URL}/diving-centers/{center.id}"
+ c_loc = ", ".join(filter(None, [getattr(center, "city", None), getattr(center, "country", None)]))
+ extra = f" ({c_loc})" if c_loc else ""
+ content_sites.append(f"- [{center.name}]({c_url}){extra}\n")
+
+ if site.routes:
+ active_routes = [r for r in site.routes if not getattr(r, "deleted_at", None)]
+ if active_routes:
+ content_sites.append("\n**Dive Routes**:\n")
+ for route in active_routes:
+ r_slug = slugify(route.name) if getattr(route, "name", None) else ""
+ r_url = f"{BASE_URL}/dive-routes/{route.id}/{r_slug}" if r_slug else f"{BASE_URL}/dive-routes/{route.id}"
+ r_type = f" ({route.route_type.name})" if getattr(route, "route_type", None) and getattr(route.route_type, "name", None) else ""
+ content_sites.append(f"- [{route.name or f'Route #{route.id}'}]({r_url}){r_type}\n")
+
content_sites.append("\n---\n\n")
# 2. Dive Routes
diff --git a/backend/static_html.py b/backend/static_html.py
index dbe806b7..176a53d2 100644
--- a/backend/static_html.py
+++ b/backend/static_html.py
@@ -284,6 +284,7 @@ def _paragraph_block(label: str, value: str) -> str:
def render_dive_site_main(site: DiveSite, avg_rating: Optional[float], total_ratings: int) -> str:
from app.seo_geo import geo_hub_path
+ from generate_static_content import get_diving_center_slug, slugify
crumbs = [("Home", "/"), ("Dive Sites", "/dive-sites")]
if site.country:
@@ -305,6 +306,12 @@ def render_dive_site_main(site: DiveSite, avg_rating: Optional[float], total_rat
if location:
lines.append(f"
Location: {escape_text(location)}
")
+ aliases = getattr(site, "aliases", None) or []
+ if aliases:
+ alias_names = [getattr(a, "alias", str(a)) for a in aliases if getattr(a, "alias", str(a))]
+ if alias_names:
+ lines.append(f"
Also known as: {escape_text(', '.join(alias_names))}
")
+
+ routes = getattr(site, "routes", None) or []
+ active_routes = [r for r in routes if not getattr(r, "deleted_at", None)]
+ if active_routes:
+ lines.append("
Dive Routes
")
+ for route in active_routes:
+ r_slug = slugify(route.name) if getattr(route, "name", None) else ""
+ r_href = f"/dive-routes/{route.id}/{r_slug}" if r_slug else f"/dive-routes/{route.id}"
+ r_name = escape_text(route.name or f"Route #{route.id}")
+ r_type_obj = getattr(route, "route_type", None)
+ r_type = f" ({escape_text(r_type_obj.name)})" if r_type_obj and getattr(r_type_obj, "name", None) else ""
+ lines.append(f'