Files
kmtnflower/backend/app/api/sitemap.py
T
kfj001 4b123025e4
CI Pipeline / backend-test (push) Successful in 10m0s
CI Pipeline / frontend-build (push) Successful in 32s
CI Pipeline / backend-lint (push) Successful in 10s
CI Pipeline / frontend-test (push) Successful in 28s
CI Pipeline / backend-test (pull_request) Successful in 11m42s
CI Pipeline / frontend-test (pull_request) Successful in 31s
CI Pipeline / frontend-build (pull_request) Successful in 32s
CI Pipeline / backend-lint (pull_request) Successful in 12s
CI Pipeline / quality-gate (push) Successful in 3s
CI Pipeline / quality-gate (pull_request) Successful in 3s
Updates the generation means of robots.txt and sitemap.xml to be based off siteconfig's website param.
2026-08-04 20:33:25 -07:00

94 lines
3.2 KiB
Python

"""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
(stored in StationConfig.website), sitemap.xml and robots.txt are
generated on-the-fly from the database — 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
the station config 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 sqlalchemy import select
from app.auth import get_current_admin_user
from app.config import settings
from app.database import async_session
from app.models import StationConfig
from app.sitemap_export import build_robots_txt, build_sitemap_xml
from app.user_models import User
router = APIRouter()
def _build_base_url(website: str) -> str:
"""Build the canonical base URL from the station website field.
The website column stores the bare domain (e.g. 'kmountainflower.org'),
so we prefix with https:// to produce a valid base URL.
"""
domain = website.strip().removesuffix("/")
# The website field may or may not include a scheme; if it already
# has one, use it as-is, otherwise default to https.
if "://" in domain:
return domain
return f"https://{domain}"
async def _get_base_url() -> str:
"""Fetch the base URL from the station config in the database."""
async with async_session() as session:
result = await session.execute(
select(StationConfig).where(StationConfig.key == "default")
)
config = result.scalar_one_or_none()
if config is None:
# Fall back to env config if DB is empty (e.g. before seeding)
return settings.WEBSITE_URL
return _build_base_url(config.website)
@router.get("/sitemap.xml")
async def get_sitemap() -> Response:
"""Return a dynamically-generated sitemap.xml. Public endpoint.
Uses StationConfig.website so each tenant gets the correct domain.
"""
base_url = await _get_base_url()
xml = build_sitemap_xml(base_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 StationConfig.website so the Sitemap directive points to the
correct tenant domain.
"""
base_url = await _get_base_url()
txt = build_robots_txt(base_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.
"""
base_url = await _get_base_url()
return {
"status": "ok",
"note": "sitemap and robots.txt are dynamically generated per-request from StationConfig.website",
"base_url": base_url,
}