Updates the generation means of robots.txt and sitemap.xml to be based off siteconfig's website param. #5
No files matched your search
@@ -1,35 +1,66 @@
|
||||
"""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.
|
||||
(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
|
||||
KMTN_WEBSITE_URL at runtime, so every tenant gets their own correct URL.
|
||||
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 KMTN_WEBSITE_URL so each tenant gets the correct domain.
|
||||
Uses StationConfig.website so each tenant gets the correct domain.
|
||||
"""
|
||||
xml = build_sitemap_xml(settings.WEBSITE_URL)
|
||||
base_url = await _get_base_url()
|
||||
xml = build_sitemap_xml(base_url)
|
||||
return Response(content=xml, media_type="application/xml")
|
||||
|
||||
|
||||
@@ -37,10 +68,11 @@ async def get_sitemap() -> Response:
|
||||
async def get_robots() -> Response:
|
||||
"""Return a dynamically-generated robots.txt. Public endpoint.
|
||||
|
||||
Uses KMTN_WEBSITE_URL so the Sitemap directive points to the
|
||||
Uses StationConfig.website so the Sitemap directive points to the
|
||||
correct tenant domain.
|
||||
"""
|
||||
txt = build_robots_txt(settings.WEBSITE_URL)
|
||||
base_url = await _get_base_url()
|
||||
txt = build_robots_txt(base_url)
|
||||
return Response(content=txt, media_type="text/plain")
|
||||
|
||||
|
||||
@@ -53,8 +85,9 @@ async def regenerate_sitemap(
|
||||
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 using KMTN_WEBSITE_URL",
|
||||
"base_url": settings.WEBSITE_URL,
|
||||
"note": "sitemap and robots.txt are dynamically generated per-request from StationConfig.website",
|
||||
"base_url": base_url,
|
||||
}
|
||||
@@ -1,12 +1,38 @@
|
||||
"""Tests for sitemap API endpoints — /api/sitemap/sitemap.xml, /api/sitemap/robots.txt, /api/sitemap/regenerate."""
|
||||
"""Tests for sitemap API endpoints — /api/sitemap/sitemap.xml, /api/sitemap/robots.txt, /api/sitemap/regenerate.
|
||||
|
||||
Note: The sitemap and robots.txt endpoints now read the site URL from
|
||||
StationConfig.website in the database (falling back to KMTN_WEBSITE_URL
|
||||
if the config is missing). These tests assume a seeded database with the
|
||||
default station config (website = 'kmountainflower.org').
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from app.api.sitemap import _build_base_url
|
||||
from app.main import app
|
||||
|
||||
|
||||
class TestBuildBaseUrl:
|
||||
"""Tests for the _build_base_url helper."""
|
||||
|
||||
def test_bare_domain_gets_https(self):
|
||||
assert _build_base_url("kmountainflower.org") == "https://kmountainflower.org"
|
||||
|
||||
def test_domain_with_trailing_slash(self):
|
||||
assert _build_base_url("kmountainflower.org/") == "https://kmountainflower.org"
|
||||
|
||||
def test_domain_with_whitespace(self):
|
||||
assert _build_base_url(" mystation.org ") == "https://mystation.org"
|
||||
|
||||
def test_already_has_https_scheme(self):
|
||||
assert _build_base_url("https://kmountainflower.org") == "https://kmountainflower.org"
|
||||
|
||||
def test_already_has_http_scheme(self):
|
||||
assert _build_base_url("http://kmountainflower.org") == "http://kmountainflower.org"
|
||||
|
||||
|
||||
class TestSitemapEndpoints:
|
||||
"""Tests for sitemap API routes."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user