Skip to content

Commit 86e46cc

Browse files
committed
rebasing fixing
1 parent 003d598 commit 86e46cc

8 files changed

Lines changed: 366 additions & 17 deletions

File tree

reflex/app.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
get_type_hints,
3232
)
3333

34-
from fastapi import FastAPI, HTTPException, Request
34+
from fastapi import FastAPI, HTTPException, Request, Response
3535
from fastapi import UploadFile as FastAPIUploadFile
3636
from fastapi.middleware import cors
3737
from fastapi.responses import JSONResponse, StreamingResponse
@@ -110,6 +110,12 @@
110110
)
111111
from reflex.utils.exec import get_compile_context, is_prod_mode, is_testing_env
112112
from reflex.utils.imports import ImportVar
113+
from reflex.sitemap import (
114+
generate_static_sitemap,
115+
generate_links_for_sitemap,
116+
check_sitemap_file_exists,
117+
read_sitemap_file,
118+
)
113119

114120
if TYPE_CHECKING:
115121
from reflex.vars import Var
@@ -410,6 +416,8 @@ class App(MiddlewareMixin, LifespanMixin):
410416
# Put the toast provider in the app wrap.
411417
toaster: Component | None = dataclasses.field(default_factory=toast.provider)
412418

419+
sitemap_properties: Dict[str, Dict] = dataclasses.field(default_factory=dict)
420+
413421
@property
414422
def api(self) -> FastAPI | None:
415423
"""Get the backend api.
@@ -462,6 +470,10 @@ def __post_init__(self):
462470
# Set up the admin dash.
463471
self._setup_admin_dash()
464472

473+
# sitemap generation
474+
links = generate_links_for_sitemap(self)
475+
generate_static_sitemap(links)
476+
465477
if sys.platform == "win32" and not is_prod_mode():
466478
# Hack to fix Windows hot reload issue.
467479
from reflex.utils.compat import windows_hot_reload_lifespan_hack
@@ -586,6 +598,24 @@ def _add_default_endpoints(self):
586598

587599
self.api.get(str(constants.Endpoint.PING))(ping)
588600
self.api.get(str(constants.Endpoint.HEALTH))(health)
601+
self.api.get(str(constants.Endpoint.SITEMAP))(self.serve_sitemap)
602+
603+
async def serve_sitemap(self) -> Response:
604+
"""Asynchronously serve the sitemap as an XML response.
605+
606+
This function checks if a sitemap.xml file exists in the root directory of the app. If so, this file is served
607+
as a Response. Otherwise, a new sitemap is generated and saved to sitemap.xml before being served.
608+
609+
Returns:
610+
Response: An HTTP response with the XML sitemap content and the media type set to "application/xml".
611+
"""
612+
if not check_sitemap_file_exists():
613+
links_sitemaps = generate_links_for_sitemap(self)
614+
generate_static_sitemap(links_sitemaps)
615+
616+
sitemaps = read_sitemap_file()
617+
618+
return Response(content=sitemaps, media_type="application/xml")
589619

590620
def _add_optional_endpoints(self):
591621
"""Add optional api endpoints (_upload)."""
@@ -663,6 +693,8 @@ def add_page(
663693
image: str = constants.DefaultPage.IMAGE,
664694
on_load: EventType[()] | None = None,
665695
meta: list[dict[str, str]] = constants.DefaultPage.META_LIST,
696+
sitemap_priority: float = constants.DefaultPage.SITEMAP_PRIORITY,
697+
sitemap_changefreq: str = constants.DefaultPage.SITEMAP_CHANGEFREQ,
666698
):
667699
"""Add a page to the app.
668700
@@ -677,6 +709,9 @@ def add_page(
677709
image: The image to display on the page.
678710
on_load: The event handler(s) that will be called each time the page load.
679711
meta: The metadata of the page.
712+
sitemap_priority: The priority of the page in the sitemap. If None, the priority is calculated based on the
713+
depth of the route.
714+
sitemap_changefreq: The change frequency of the page in the sitemap. Default to 'weekly'
680715
681716
Raises:
682717
PageValueError: When the component is not set for a non-404 page.
@@ -736,6 +771,11 @@ def add_page(
736771
on_load if isinstance(on_load, list) else [on_load]
737772
)
738773

774+
self.sitemap_properties[route] = {
775+
"priority": sitemap_priority,
776+
"changefreq": sitemap_changefreq,
777+
}
778+
739779
self._unevaluated_pages[route] = UnevaluatedPage(
740780
component=component,
741781
route=route,
@@ -771,6 +811,14 @@ def _compile_page(self, route: str, save_page: bool = True):
771811
if save_page:
772812
self._pages[route] = component
773813

814+
def get_pages(self) -> dict[str, Component]:
815+
"""Get the pages of the app.
816+
817+
Returns:
818+
The pages of the app.
819+
"""
820+
return self._pages
821+
774822
def get_load_events(self, route: str) -> list[IndividualEventType[()]]:
775823
"""Get the load events for a route.
776824

reflex/config.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434

3535
from reflex import constants
3636
from reflex.base import Base
37-
from reflex.constants.base import LogLevel
3837
from reflex.utils import console
3938
from reflex.utils.exceptions import ConfigError, EnvironmentVarValueError
4039
from reflex.utils.types import (
@@ -878,13 +877,6 @@ def __init__(self, *args, **kwargs):
878877
"""
879878
super().__init__(*args, **kwargs)
880879

881-
# Set the log level for this process
882-
env_loglevel = os.environ.get("LOGLEVEL")
883-
if env_loglevel is not None:
884-
env_loglevel = LogLevel(env_loglevel)
885-
if env_loglevel or self.loglevel != LogLevel.DEFAULT:
886-
console.set_log_level(env_loglevel or self.loglevel)
887-
888880
# Update the config from environment variables.
889881
env_kwargs = self.update_from_env()
890882
for key, env_value in env_kwargs.items():
@@ -895,6 +887,9 @@ def __init__(self, *args, **kwargs):
895887
self._non_default_attributes.update(kwargs)
896888
self._replace_defaults(**kwargs)
897889

890+
# Set the log level for this process
891+
console.set_log_level(self.loglevel)
892+
898893
if (
899894
self.state_manager_mode == constants.StateManagerMode.REDIS
900895
and not self.redis_url

reflex/constants/event.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class Endpoint(Enum):
1313
AUTH_CODESPACE = "auth-codespace"
1414
HEALTH = "_health"
1515
ALL_ROUTES = "_all_routes"
16+
SITEMAP = "sitemap.xml"
1617

1718
def __str__(self) -> str:
1819
"""Get the string representation of the endpoint.

reflex/constants/route.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ class DefaultPage(SimpleNamespace):
6161
IMAGE = "favicon.ico"
6262
# The default meta list to show for Reflex apps.
6363
META_LIST = []
64+
# The default changefrequency for sitemap generation.
65+
SITEMAP_CHANGEFREQ = "weekly"
66+
# The default priority for sitemap generation.
67+
SITEMAP_PRIORITY = 10.0
6468

6569

6670
# 404 variables

reflex/sitemap.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
from pathlib import Path
2+
from typing import List, Dict, Any
3+
from xml.dom import minidom
4+
from xml.etree.ElementTree import Element, SubElement, tostring
5+
6+
from reflex import constants
7+
from reflex.utils import prerequisites
8+
from reflex.config import get_config
9+
10+
11+
# _static folder in the .web directory containing the sitemap.xml file.
12+
_sitemap_folder_path: Path = (
13+
Path.cwd() / prerequisites.get_web_dir() / constants.Dirs.STATIC
14+
)
15+
16+
# sitemap file path
17+
_sitemap_file_path: Path = _sitemap_folder_path / "sitemap.xml"
18+
19+
20+
def check_sitemap_file_exists() -> bool:
21+
"""Check if the sitemap file exists.
22+
23+
Returns:
24+
bool: True if the sitemap file exists in the .web/_static folder.
25+
"""
26+
return _sitemap_folder_path.exists() & _sitemap_file_path.exists()
27+
28+
29+
def read_sitemap_file() -> str:
30+
"""Read the sitemap file.
31+
32+
Returns:
33+
str: The contents of the sitemap file.
34+
"""
35+
with open(_sitemap_file_path, "r") as f:
36+
return f.read()
37+
38+
39+
def generate_xml(links: List[Dict[str, Any]]) -> str:
40+
"""Generate an XML sitemap from a list of links.
41+
42+
Args:
43+
links (List[Dict[str, Any]]): A list of dictionaries where each dictionary contains
44+
'loc' (URL of the page), 'changefreq' (frequency of changes), and 'priority' (priority of the page).
45+
46+
Returns:
47+
str: A pretty-printed XML string representing the sitemap.
48+
"""
49+
urlset = Element("urlset", xmlns="https://www.sitemaps.org/schemas/sitemap/0.9")
50+
for link in links:
51+
url = SubElement(urlset, "url")
52+
loc = SubElement(url, "loc")
53+
loc.text = link["loc"]
54+
changefreq = SubElement(url, "changefreq")
55+
changefreq.text = link["changefreq"]
56+
priority = SubElement(url, "priority")
57+
priority.text = str(link["priority"])
58+
rough_string = tostring(urlset, "utf-8")
59+
reparsed = minidom.parseString(rough_string)
60+
return reparsed.toprettyxml(indent=" ")
61+
62+
63+
def generate_links_for_sitemap(app_instance) -> List[dict[str, str]]:
64+
"""Generate a list of links for which sitemaps are generated.
65+
66+
This function loops through the registered routes in the app and generates a list of
67+
links with their respective sitemap properties such as location (URL), change frequency,
68+
and priority. Dynamic routes and the 404 page are excluded from the sitemap.
69+
70+
Args:
71+
app_instance: The instance of the App class from app.py.
72+
73+
Returns:
74+
List: A list of dictionaries where each dictionary contains the 'loc' (URL of the page), 'priority' and
75+
'changefreq' of each route.
76+
"""
77+
links = []
78+
79+
# find link of pages that are not dynamicaly created.
80+
for route, component in app_instance.get_pages().items():
81+
# Ignore dynamic routes and 404
82+
if ("[" in route and "]" in route) or route == "404":
83+
continue
84+
85+
# Handle the index route
86+
if route == "index":
87+
route = "/"
88+
89+
if not route.startswith("/"):
90+
route = f"/{route}"
91+
92+
sitemap_changefreq = constants.DefaultPage.SITEMAP_CHANGEFREQ # default value
93+
sitemap_priority = constants.DefaultPage.SITEMAP_PRIORITY # default value
94+
95+
# extract sitemap properties from the app's property, route exist in the compiled pages.
96+
if route in app_instance.site_map_properties:
97+
sitemap_priority = app_instance.site_map_properties[route]["priority"]
98+
sitemap_changefreq = app_instance.site_map_properties[route]["changefreq"]
99+
100+
if (
101+
sitemap_priority == constants.DefaultPage.SITEMAP_PRIORITY
102+
): # indicates that user didn't set priority
103+
depth = route.count("/")
104+
sitemap_priority = max(0.5, 1.0 - (depth * 0.1))
105+
106+
deploy_url = get_config().deploy_url # pick domain url from the config file.
107+
108+
links.append(
109+
{
110+
"loc": f"{deploy_url}{route}",
111+
"changefreq": sitemap_changefreq,
112+
"priority": sitemap_priority,
113+
}
114+
)
115+
return links
116+
117+
118+
def generate_static_sitemap(links: List[dict[str, str]]) -> None:
119+
"""Generates the sitemaps for the pages stored in _pages. Store it in sitemap.xml.
120+
121+
This method is called from two methods:
122+
1. Everytime the web app is deployed onto the server.
123+
2. When the user (or crawler) requests for the sitemap.xml file.
124+
125+
Args:
126+
links: The list of urls for which the sitemap is to be generated.
127+
"""
128+
sitemap = generate_xml(links)
129+
Path(_sitemap_folder_path).mkdir(parents=True, exist_ok=True)
130+
131+
# this method is only called when old sitemap.xml is not retrieved. So we can safely replace an already existing xml
132+
# file.
133+
with open(_sitemap_file_path, "w") as f:
134+
f.write(sitemap)
135+
136+
137+
def remove_sitemap_file() -> None:
138+
"""Remove the sitemap file, if a regeneration is needed.
139+
140+
Generally for testing the generation of sitemap.xml file, we need to remove the automatically generated file
141+
when the app is initialized.
142+
"""
143+
if _sitemap_file_path.exists():
144+
_sitemap_file_path.unlink()

reflex/utils/build.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,6 @@ def build(
203203
"Collecting build traces",
204204
]
205205

206-
# Generate a sitemap if a deploy URL is provided.
207-
if deploy_url is not None:
208-
generate_sitemap_config(deploy_url, export=for_export)
209-
command = "export-sitemap"
210-
211-
checkpoints.extend(["Loading next-sitemap", "Generation completed"])
212-
213206
# Start the subprocess with the progress bar.
214207
process = processes.new_process(
215208
[prerequisites.get_package_manager(), "run", command],

tests/units/test_app.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Generator, Type
1313
from unittest.mock import AsyncMock
1414

15+
import fastapi
1516
import pytest
1617
import sqlmodel
1718
from fastapi import FastAPI, UploadFile
@@ -48,7 +49,7 @@
4849
_substate_key,
4950
)
5051
from reflex.style import Style
51-
from reflex.utils import exceptions, format
52+
from reflex.utils import exceptions, format, prerequisites
5253
from reflex.vars.base import computed_var
5354

5455
from .conftest import chdir
@@ -351,6 +352,20 @@ def test_add_duplicate_page_route_error(app, first_page, second_page, route):
351352
app.add_page(second_page, route="/" + route.strip("/") if route else None)
352353

353354

355+
def test_add_page_with_sitemap_properties(app):
356+
"""Test if the sitemap properties of the app instance is set properly or not."""
357+
# check with given values.
358+
app.add_page(
359+
page1, route="/page1", sitemap_priority=0.9, sitemap_changefreq="daily"
360+
)
361+
assert app.sitemap_properties["page1"] == {"priority": 0.9, "changefreq": "daily"}
362+
363+
# check default values added.
364+
app.add_page(page2, route="/page2")
365+
print(app.sitemap_properties)
366+
assert app.sitemap_properties["page2"] == {"priority": 10.0, "changefreq": "weekly"}
367+
368+
354369
def test_initialize_with_admin_dashboard(test_model):
355370
"""Test setting the admin dashboard of an app.
356371

0 commit comments

Comments
 (0)