77from fastapi .responses import HTMLResponse , Response
88from sqlalchemy .ext .asyncio import AsyncSession
99
10- from api .cache import cache_key , get_cache , set_cache
10+ from api .cache import cache_key , get_cache , get_or_set_cache , set_cache
1111from api .dependencies import optional_db
12+ from core .config import settings
1213from core .database import SpecRepository
14+ from core .database .connection import get_db_context
1315
1416
1517router = APIRouter (tags = ["seo" ])
@@ -20,6 +22,42 @@ def _lastmod(dt: datetime | None) -> str:
2022 return f"<lastmod>{ dt .strftime ('%Y-%m-%d' )} </lastmod>" if dt else ""
2123
2224
25+ def _build_sitemap_xml (specs : list ) -> str :
26+ """Build sitemap XML string from specs."""
27+ xml_lines = [
28+ '<?xml version="1.0" encoding="UTF-8"?>' ,
29+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' ,
30+ " <url><loc>https://pyplots.ai/</loc></url>" ,
31+ " <url><loc>https://pyplots.ai/catalog</loc></url>" ,
32+ " <url><loc>https://pyplots.ai/mcp</loc></url>" ,
33+ " <url><loc>https://pyplots.ai/legal</loc></url>" ,
34+ ]
35+
36+ for spec in specs :
37+ if spec .impls :
38+ spec_id = html .escape (spec .id )
39+ xml_lines .append (f" <url><loc>https://pyplots.ai/{ spec_id } </loc>{ _lastmod (spec .updated )} </url>" )
40+ for impl in spec .impls :
41+ library_id = html .escape (impl .library_id )
42+ xml_lines .append (
43+ f" <url><loc>https://pyplots.ai/{ spec_id } /{ library_id } </loc>{ _lastmod (impl .updated )} </url>"
44+ )
45+
46+ xml_lines .append ("</urlset>" )
47+ return "\n " .join (xml_lines )
48+
49+
50+ _STATIC_SITEMAP = _build_sitemap_xml ([])
51+
52+
53+ async def _refresh_sitemap () -> str :
54+ """Standalone factory for background sitemap refresh (creates own DB session)."""
55+ async with get_db_context () as db :
56+ repo = SpecRepository (db )
57+ specs = await repo .get_all ()
58+ return _build_sitemap_xml (specs )
59+
60+
2361# Minimal HTML template for social media bots (meta tags are what matters)
2462BOT_HTML_TEMPLATE = """<!DOCTYPE html>
2563<html lang="en">
@@ -66,41 +104,17 @@ async def get_sitemap(db: AsyncSession | None = Depends(optional_db)):
66104
67105 Includes root, catalog page, and all specs with implementations.
68106 """
69- key = cache_key ("sitemap_xml" )
70- cached = get_cache (key )
71- if cached :
72- return Response (content = cached , media_type = "application/xml" )
73-
74- # Build XML lines
75- xml_lines = [
76- '<?xml version="1.0" encoding="UTF-8"?>' ,
77- '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' ,
78- " <url><loc>https://pyplots.ai/</loc></url>" ,
79- " <url><loc>https://pyplots.ai/catalog</loc></url>" ,
80- " <url><loc>https://pyplots.ai/mcp</loc></url>" ,
81- " <url><loc>https://pyplots.ai/legal</loc></url>" ,
82- ]
107+ if db is None :
108+ return Response (content = _STATIC_SITEMAP , media_type = "application/xml" )
83109
84- # Add spec URLs (overview + all implementations)
85- if db is not None :
110+ async def _fetch () -> str :
86111 repo = SpecRepository (db )
87112 specs = await repo .get_all ()
88- for spec in specs :
89- if spec .impls : # Only include specs with implementations
90- spec_id = html .escape (spec .id )
91- # Overview page
92- xml_lines .append (f" <url><loc>https://pyplots.ai/{ spec_id } </loc>{ _lastmod (spec .updated )} </url>" )
93- # Individual implementation pages
94- for impl in spec .impls :
95- library_id = html .escape (impl .library_id )
96- xml_lines .append (
97- f" <url><loc>https://pyplots.ai/{ spec_id } /{ library_id } </loc>{ _lastmod (impl .updated )} </url>"
98- )
99-
100- xml_lines .append ("</urlset>" )
101- xml = "\n " .join (xml_lines )
113+ return _build_sitemap_xml (specs )
102114
103- set_cache (key , xml )
115+ xml = await get_or_set_cache (
116+ cache_key ("sitemap_xml" ), _fetch , refresh_after = settings .cache_refresh_after , refresh_factory = _refresh_sitemap
117+ )
104118 return Response (content = xml , media_type = "application/xml" )
105119
106120
0 commit comments