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