Compare commits

..
2 Commits
Author SHA1 Message Date
kfj001 2716765223 Merge pull request 'Updates the generation means of robots.txt and sitemap.xml to be based off siteconfig's website param.' (#5) from bugfix/fix_API_robots_txt into main
CI Pipeline / backend-lint (push) Successful in 9s
CI Pipeline / quality-gate (push) Successful in 3s
CI Pipeline / backend-test (push) Successful in 7m40s
CI Pipeline / frontend-test (push) Successful in 28s
CI Pipeline / frontend-build (push) Successful in 29s
Reviewed-on: kfj001/kmtnflower#5
2026-08-04 22:15:22 -07:00
kfj001 4b123025e4 Updates the generation means of robots.txt and sitemap.xml to be based off siteconfig's website param.
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
2026-08-04 20:33:25 -07:00
2 changed files with 69 additions and 10 deletions

No files matched your search

+42 -9
View File
@@ -1,35 +1,66 @@
"""Sitemap & robots.txt API: tenant-aware SEO files served by the backend. """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 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 (stored in StationConfig.website), sitemap.xml and robots.txt are
on-the-fly from the station config — no static files needed. generated on-the-fly from the database — no static files needed.
This matches the reviewer feedback: This matches the reviewer feedback:
'This is a whitelabel app - this file may need to be dynaimcally generated...' '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 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 typing import Any, Dict
from fastapi import APIRouter, Depends, Response, status as http_status from fastapi import APIRouter, Depends, Response, status as http_status
from sqlalchemy import select
from app.auth import get_current_admin_user from app.auth import get_current_admin_user
from app.config import settings 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.sitemap_export import build_robots_txt, build_sitemap_xml
from app.user_models import User from app.user_models import User
router = APIRouter() 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") @router.get("/sitemap.xml")
async def get_sitemap() -> Response: async def get_sitemap() -> Response:
"""Return a dynamically-generated sitemap.xml. Public endpoint. """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") return Response(content=xml, media_type="application/xml")
@@ -37,10 +68,11 @@ async def get_sitemap() -> Response:
async def get_robots() -> Response: async def get_robots() -> Response:
"""Return a dynamically-generated robots.txt. Public endpoint. """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. 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") 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), In the dynamic model this is a no-op (files are generated per-request),
but kept for API consistency with the theme endpoint. but kept for API consistency with the theme endpoint.
""" """
base_url = await _get_base_url()
return { return {
"status": "ok", "status": "ok",
"note": "sitemap and robots.txt are dynamically generated per-request using KMTN_WEBSITE_URL", "note": "sitemap and robots.txt are dynamically generated per-request from StationConfig.website",
"base_url": settings.WEBSITE_URL, "base_url": base_url,
} }
+27 -1
View File
@@ -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 import pytest
from httpx import AsyncClient from httpx import AsyncClient
from starlette.testclient import TestClient from starlette.testclient import TestClient
from app.api.sitemap import _build_base_url
from app.main import app 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: class TestSitemapEndpoints:
"""Tests for sitemap API routes.""" """Tests for sitemap API routes."""