Skip to content

Commit 28274c5

Browse files
committed
Add sitemap
1 parent 9bd3dbc commit 28274c5

File tree

3 files changed

+90
-2
lines changed

3 files changed

+90
-2
lines changed

futuramaapi/apps/app.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import mimetypes
22
from abc import ABC, abstractmethod
33
from contextlib import asynccontextmanager
4-
from typing import TYPE_CHECKING, Self
4+
from typing import TYPE_CHECKING, Literal, Self
55

66
import sentry_sdk
77
from fastapi import FastAPI
@@ -25,6 +25,19 @@
2525
mimetypes.add_type("image/webp", ".webp")
2626

2727

28+
BOTS_FORBIDDEN_URLS: tuple[str, ...] = (
29+
"/favicon.ico",
30+
"/openapi.json",
31+
"/robots.txt",
32+
"/sitemap.xml",
33+
"/static",
34+
"/health",
35+
"/logout",
36+
"/api/",
37+
"/s/",
38+
)
39+
40+
2841
class BaseAPI(ABC):
2942
version: str = __version__
3043

@@ -70,6 +83,27 @@ def get_app(
7083
def build(self) -> None: ...
7184

7285

86+
def _is_route_public(
87+
url: BaseRoute,
88+
/,
89+
*,
90+
allowed_methods: list[Literal["GET", "POST", "HEAD", "PUT"]] | None = None,
91+
) -> bool:
92+
if url.path.startswith(BOTS_FORBIDDEN_URLS):
93+
return False
94+
95+
if allowed_methods is None:
96+
allowed_methods = ["GET"]
97+
98+
if not hasattr(url, "methods"):
99+
return True
100+
101+
if not any(x in allowed_methods for x in url.methods):
102+
return False
103+
104+
return True
105+
106+
73107
class FuturamaAPI(BaseAPI):
74108
def get_app(
75109
self,
@@ -118,6 +152,15 @@ def build(self) -> None:
118152
def urls(self) -> list[BaseRoute]:
119153
return self.app.routes
120154

155+
@property
156+
def public_urls(self) -> list[BaseRoute]:
157+
urls: list[BaseRoute] = []
158+
for route in self.app.routes:
159+
if _is_route_public(route) and route.path not in [u.path for u in urls]:
160+
urls.append(route)
161+
162+
return urls
163+
121164

122165
@asynccontextmanager
123166
async def _lifespan(_: FastAPI):

futuramaapi/routers/root/api.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from futuramaapi.routers.users.dependencies import cookie_user_from_form_data, user_from_cookies
1212
from futuramaapi.routers.users.schemas import Link, User
1313

14-
from .schemas import About, Root, UserAuth
14+
from .schemas import About, Root, SiteMap, UserAuth
1515

1616
router = APIRouter()
1717

@@ -178,3 +178,14 @@ async def user_link_redirect(
178178
counter=add(link.counter, 1),
179179
)
180180
return RedirectResponse(link.url)
181+
182+
183+
@router.get(
184+
"/sitemap.xml",
185+
include_in_schema=False,
186+
)
187+
async def get_sitemap(
188+
request: Request,
189+
) -> Response:
190+
obj: SiteMap = await SiteMap.from_request(request)
191+
return await obj.get_response()

futuramaapi/routers/root/schemas.py

+34
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from typing import ClassVar, Self
22

3+
from fastapi import Response
4+
from pydantic import HttpUrl
35
from sqlalchemy.ext.asyncio import AsyncSession
46
from starlette.requests import Request
57

8+
from futuramaapi.core import settings
69
from futuramaapi.helpers.pydantic import BaseModel, Field
710
from futuramaapi.mixins.pydantic import BaseModelTemplateMixin
811
from futuramaapi.repositories.base import FilterStatementKwargs
@@ -46,3 +49,34 @@ class UserAuth(BaseModel, BaseModelTemplateMixin):
4649
@classmethod
4750
async def from_request(cls, session: AsyncSession, request: Request, /) -> Self:
4851
return cls()
52+
53+
54+
class SiteMap(BaseModel):
55+
_header: ClassVar[str] = '<?xml version="1.0" encoding="UTF-8"?>'
56+
_media_type: ClassVar[str] = "application/xml"
57+
_url_tag: ClassVar[str] = """<url><loc>%s</loc></url>"""
58+
_url_set_tag: ClassVar[str] = """<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">%s</urlset>"""
59+
60+
base_url: HttpUrl = settings.build_url(is_static=False)
61+
urls: list[str]
62+
63+
async def get_response(self) -> Response:
64+
urls: str = ""
65+
for url in self.urls:
66+
urls += self._url_tag % settings.build_url(
67+
path=url,
68+
is_static=False,
69+
)
70+
url_set: str = self._url_set_tag % urls
71+
return Response(
72+
content=f"""{self._header}{url_set}""",
73+
media_type=self._media_type,
74+
)
75+
76+
@classmethod
77+
async def from_request(cls, request: Request) -> Self:
78+
from futuramaapi.apps.app import futurama_api
79+
80+
return cls(
81+
urls=[url.path for url in futurama_api.public_urls],
82+
)

0 commit comments

Comments
 (0)