"""Sitemap & robots.txt export: generate tenant-aware SEO files.
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 from the station config — one static write per startup,
plus an optional admin-triggered regeneration.
Follows the same pattern as theme_export.py:
- Atomic write (tmp + os.replace)
- Written to STATIC_FILES_DIR so Nginx serves them
- Uses WEBSITE_URL for per-tenant domain resolution
"""
import logging
from datetime import datetime, timezone
from pathlib import Path
from app.config import settings
logger = logging.getLogger(__name__)
# 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" \n"
f" {url}{route['path']}\n"
f" {route['changefreq']}\n"
f" {route['priority']}\n"
f" "
)
return (
'\n'
'\n'
+ "\n".join(entries) + "\n"
"\n"
)
def build_robots_txt(sitemap_url: str) -> str:
"""Build a robots.txt string that references the correct sitemap URL."""
return (
"User-agent: *\n"
"Allow: /\n"
"\n"
"Disallow: /admin\n"
"Disallow: /login\n"
"\n"
f"Sitemap: {sitemap_url}/sitemap.xml\n"
)
def get_sitemap_status() -> dict:
"""Check if sitemap.xml and robots.txt exist on disk and return metadata."""
base = Path(settings.STATIC_FILES_DIR)
result = {}
for name in ("sitemap.xml", "robots.txt"):
path = base / name
if not path.exists():
result[name] = {"exists": False, "path": str(path)}
continue
stat = path.stat()
result[name] = {
"exists": True,
"path": str(path),
"size": stat.st_size,
"modified": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
}
return result
def write_sitemap_files() -> dict:
"""Write sitemap.xml and robots.txt to the static files directory.
Uses atomic write (write to .tmp, then os.replace) to prevent
Nginx from serving a partially-written file.
Returns a status dict with file paths.
"""
base_url = settings.WEBSITE_URL
static_dir = Path(settings.STATIC_FILES_DIR)
static_dir.mkdir(parents=True, exist_ok=True)
# sitemap.xml
sitemap_content = build_sitemap_xml(base_url)
sitemap_path = static_dir / "sitemap.xml"
tmp_path = sitemap_path.with_suffix(".tmp")
tmp_path.write_text(sitemap_content, encoding="utf-8")
tmp_path.replace(sitemap_path)
logger.info("Wrote sitemap.xml (%d bytes) to %s, base_url=%s", len(sitemap_content), sitemap_path, base_url)
# robots.txt
robots_content = build_robots_txt(base_url)
robots_path = static_dir / "robots.txt"
tmp_path = robots_path.with_suffix(".tmp")
tmp_path.write_text(robots_content, encoding="utf-8")
tmp_path.replace(robots_path)
logger.info("Wrote robots.txt (%d bytes) to %s", len(robots_content), robots_path)
return {
"sitemap.xml": str(sitemap_path),
"robots.txt": str(robots_path),
"base_url": base_url,
}