diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
index c81fe00..5fe1d3e 100644
--- a/.gitea/workflows/ci.yaml
+++ b/.gitea/workflows/ci.yaml
@@ -2,7 +2,7 @@ name: CI Pipeline
on:
push:
- branches: [main, develop, feature/tests-and-ci, feature/multi-admin-roles]
+ branches: ['**']
pull_request:
branches: [main]
diff --git a/backend/app/__pycache__/__init__.cpython-312 - Copy.pyc b/backend/app/__pycache__/__init__.cpython-312 - Copy.pyc
deleted file mode 100644
index f2d8ded..0000000
Binary files a/backend/app/__pycache__/__init__.cpython-312 - Copy.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/__init__.cpython-312.pyc b/backend/app/__pycache__/__init__.cpython-312.pyc
deleted file mode 100644
index 3743842..0000000
Binary files a/backend/app/__pycache__/__init__.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/auth.cpython-312.pyc b/backend/app/__pycache__/auth.cpython-312.pyc
deleted file mode 100644
index 7c01d5e..0000000
Binary files a/backend/app/__pycache__/auth.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/config.cpython-312 - Copy.pyc b/backend/app/__pycache__/config.cpython-312 - Copy.pyc
deleted file mode 100644
index c96e43d..0000000
Binary files a/backend/app/__pycache__/config.cpython-312 - Copy.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/config.cpython-312.pyc b/backend/app/__pycache__/config.cpython-312.pyc
deleted file mode 100644
index e68e6b6..0000000
Binary files a/backend/app/__pycache__/config.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/database.cpython-312 - Copy.pyc b/backend/app/__pycache__/database.cpython-312 - Copy.pyc
deleted file mode 100644
index 9a04cdf..0000000
Binary files a/backend/app/__pycache__/database.cpython-312 - Copy.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/database.cpython-312.pyc b/backend/app/__pycache__/database.cpython-312.pyc
deleted file mode 100644
index 9a04cdf..0000000
Binary files a/backend/app/__pycache__/database.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/log_parser.cpython-312.pyc b/backend/app/__pycache__/log_parser.cpython-312.pyc
deleted file mode 100644
index c66f89b..0000000
Binary files a/backend/app/__pycache__/log_parser.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/main.cpython-312 - Copy.pyc b/backend/app/__pycache__/main.cpython-312 - Copy.pyc
deleted file mode 100644
index a1fc247..0000000
Binary files a/backend/app/__pycache__/main.cpython-312 - Copy.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc
deleted file mode 100644
index 8e89514..0000000
Binary files a/backend/app/__pycache__/main.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/models.cpython-312 - Copy.pyc b/backend/app/__pycache__/models.cpython-312 - Copy.pyc
deleted file mode 100644
index 2d459cc..0000000
Binary files a/backend/app/__pycache__/models.cpython-312 - Copy.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/models.cpython-312.pyc b/backend/app/__pycache__/models.cpython-312.pyc
deleted file mode 100644
index 6fb50ac..0000000
Binary files a/backend/app/__pycache__/models.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/schemas.cpython-312.pyc b/backend/app/__pycache__/schemas.cpython-312.pyc
deleted file mode 100644
index 5a3da62..0000000
Binary files a/backend/app/__pycache__/schemas.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/theme_export.cpython-312.pyc b/backend/app/__pycache__/theme_export.cpython-312.pyc
deleted file mode 100644
index 06d0e08..0000000
Binary files a/backend/app/__pycache__/theme_export.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/user_models.cpython-312 - Copy.pyc b/backend/app/__pycache__/user_models.cpython-312 - Copy.pyc
deleted file mode 100644
index ed7c844..0000000
Binary files a/backend/app/__pycache__/user_models.cpython-312 - Copy.pyc and /dev/null differ
diff --git a/backend/app/__pycache__/user_models.cpython-312.pyc b/backend/app/__pycache__/user_models.cpython-312.pyc
deleted file mode 100644
index ed7c844..0000000
Binary files a/backend/app/__pycache__/user_models.cpython-312.pyc and /dev/null differ
diff --git a/backend/app/api/sitemap.py b/backend/app/api/sitemap.py
new file mode 100644
index 0000000..6f6f419
--- /dev/null
+++ b/backend/app/api/sitemap.py
@@ -0,0 +1,60 @@
+"""Sitemap & robots.txt API: tenant-aware SEO files served by the backend.
+
+Because this is a white-label app where each tenant has its own domain
+(set via KMTN_WEBSITE_URL), sitemap.xml and robots.txt are generated
+on-the-fly from the station config — no static files needed.
+
+This matches the reviewer feedback:
+ 'This is a whitelabel app - this file may need to be dynaimcally generated...'
+
+Follows the same pattern as theme_export.py: content is derived from
+KMTN_WEBSITE_URL at runtime, so every tenant gets their own correct URL.
+"""
+
+from typing import Any, Dict
+
+from fastapi import APIRouter, Depends, Response, status as http_status
+
+from app.auth import get_current_admin_user
+from app.config import settings
+from app.sitemap_export import build_robots_txt, build_sitemap_xml
+from app.user_models import User
+
+router = APIRouter()
+
+
+@router.get("/sitemap.xml")
+async def get_sitemap() -> Response:
+ """Return a dynamically-generated sitemap.xml. Public endpoint.
+
+ Uses KMTN_WEBSITE_URL so each tenant gets the correct domain.
+ """
+ xml = build_sitemap_xml(settings.WEBSITE_URL)
+ return Response(content=xml, media_type="application/xml")
+
+
+@router.get("/robots.txt")
+async def get_robots() -> Response:
+ """Return a dynamically-generated robots.txt. Public endpoint.
+
+ Uses KMTN_WEBSITE_URL so the Sitemap directive points to the
+ correct tenant domain.
+ """
+ txt = build_robots_txt(settings.WEBSITE_URL)
+ return Response(content=txt, media_type="text/plain")
+
+
+@router.post("/regenerate", status_code=http_status.HTTP_200_OK)
+async def regenerate_sitemap(
+ _: User = Depends(get_current_admin_user),
+) -> Dict[str, Any]:
+ """Force regenerate the sitemap cache. Admin only.
+
+ In the dynamic model this is a no-op (files are generated per-request),
+ but kept for API consistency with the theme endpoint.
+ """
+ return {
+ "status": "ok",
+ "note": "sitemap and robots.txt are dynamically generated per-request using KMTN_WEBSITE_URL",
+ "base_url": settings.WEBSITE_URL,
+ }
diff --git a/backend/app/main.py b/backend/app/main.py
index 10ca14f..a72a6ed 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -10,7 +10,7 @@ from app.config import settings
from app.database import async_session, engine
from app.models import Base, Show, StationConfig, HistoryEntry, TeamMember, CommunityHighlight, Underwriter
from app.user_models import User
-from app.api import events, auth, station_config, history, team, community, shows, storage, underwriters, stats, mobile_builds, theme
+from app.api import events, auth, station_config, history, team, community, shows, storage, underwriters, stats, mobile_builds, theme, sitemap
def _cleanup_orphaned_sequences(sync_conn):
@@ -78,6 +78,11 @@ def _migrate_add_missing_columns(sync_conn):
("station_config", "color_black", sa.String(7), "#1a1816"),
# Multi-admin support: password_hash (nullable — existing rows stay NULL)
("users", "password_hash", sa.String(255), None),
+ # Social media columns
+ ("station_config", "social_facebook", sa.String(500), ""),
+ ("station_config", "social_twitter", sa.String(500), ""),
+ ("station_config", "social_instagram", sa.String(500), ""),
+ ("station_config", "social_youtube", sa.String(500), ""),
]
# Determine dialect
@@ -230,6 +235,7 @@ app.include_router(underwriters.router, prefix="/api/underwriters", tags=["under
app.include_router(stats.router, prefix="/api/stats", tags=["stats"])
app.include_router(mobile_builds.router, prefix="/api/mobile-builds", tags=["mobile-builds"])
app.include_router(theme.router, prefix="/api/theme", tags=["theme"])
+app.include_router(sitemap.router, prefix="/api/sitemap", tags=["sitemap"])
# Admin user management (available in all environments)
from app.api import users
diff --git a/backend/app/models.py b/backend/app/models.py
index 8954944..8b26604 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -97,6 +97,12 @@ class StationConfig(Base):
# Donation
donation_url = Column(String(500), nullable=False, default="")
+ # Social media
+ social_facebook = Column(String(500), nullable=False, default="")
+ social_twitter = Column(String(500), nullable=False, default="")
+ social_instagram = Column(String(500), nullable=False, default="")
+ social_youtube = Column(String(500), nullable=False, default="")
+
# Stream
stream_url = Column(String(500), nullable=False, default="")
stream_metadata_url = Column(String(500), nullable=False, default="")
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index 3ac4a84..bccb55b 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -78,6 +78,11 @@ class StationConfigResponse(BaseModel):
play_store_icon_url: str
play_store_url: str
app_store_embed_html: str
+ # Social media
+ social_facebook: str
+ social_twitter: str
+ social_instagram: str
+ social_youtube: str
# Page content
hero_subtitle: str
about_intro: str
@@ -129,6 +134,11 @@ class StationConfigUpdate(BaseModel):
play_store_icon_url: Optional[str] = None
play_store_url: Optional[str] = None
app_store_embed_html: Optional[str] = None
+ # Social media
+ social_facebook: Optional[str] = None
+ social_twitter: Optional[str] = None
+ social_instagram: Optional[str] = None
+ social_youtube: Optional[str] = None
# Page content
hero_subtitle: Optional[str] = None
about_intro: Optional[str] = None
diff --git a/backend/app/sitemap_export.py b/backend/app/sitemap_export.py
new file mode 100644
index 0000000..13a7ec9
--- /dev/null
+++ b/backend/app/sitemap_export.py
@@ -0,0 +1,52 @@
+"""Sitemap & robots.txt generation: tenant-aware SEO content.
+
+Because this is a white-label app where each tenant has its own domain
+(set via KMTN_WEBSITE_URL), the sitemap and robots.txt are generated
+at runtime — no static files are needed. The FastAPI /api/sitemap/*
+endpoints call these functions; Nginx proxies /sitemap.xml and
+/robots.txt to those endpoints so search-engine crawlers hit the right
+content at the root URL.
+"""
+
+# Public routes included in the sitemap with their change frequency and priority.
+SEOMAP_ROUTES = [
+ {"path": "/", "changefreq": "daily", "priority": 1.0},
+ {"path": "/about", "changefreq": "weekly", "priority": 0.8},
+ {"path": "/schedule", "changefreq": "weekly", "priority": 0.8},
+ {"path": "/events", "changefreq": "daily", "priority": 0.8},
+ {"path": "/contact", "changefreq": "monthly", "priority": 0.5},
+]
+
+
+def build_sitemap_xml(base_url: str) -> str:
+ """Build a valid sitemap.xml string for the given base URL."""
+ url = base_url.rstrip("/")
+ entries = []
+ for route in SEOMAP_ROUTES:
+ entries.append(
+ f"
{{ config().about_donation_text }}
diff --git a/src/app/admin/admin-station-form.component.html b/src/app/admin/admin-station-form.component.html index 469d777..b6bc1d5 100644 --- a/src/app/admin/admin-station-form.component.html +++ b/src/app/admin/admin-station-form.component.html @@ -184,6 +184,31 @@