diff --git a/sx/app.py b/sx/app.py index d53e82b..1f55bd1 100644 --- a/sx/app.py +++ b/sx/app.py @@ -119,15 +119,23 @@ def create_app() -> "Quart": @app.before_request async def sx_url_redirect(): - """Redirect old-style paths to SX expression URLs (301).""" + """Redirect old-style paths and bare root to /sx/ URLs (301).""" from quart import request, redirect as q_redirect path = request.path + # Root → /sx/ + if path == "/": + return q_redirect("/sx/", 301) # Skip non-page paths - if path.startswith(("/static/", "/internal/", "/auth/", "/sx/")): + if path.startswith(("/static/", "/internal/", "/auth/")): return None - # Skip SX expression URLs (already in new format) + # Skip SX expression URLs (already in new format under /sx/) + if path.startswith("/sx/"): + return None + # Redirect bare /(...) to /sx/(...) if path.startswith("/(") or path.startswith("/~"): - return None + qs = request.query_string.decode() + target = f"/sx{path}" + ("?" + qs if qs else "") + return q_redirect(target, 301) new_url = redirect_old_url(path) if new_url: qs = request.query_string.decode() @@ -140,20 +148,26 @@ def create_app() -> "Quart": from quart import request, redirect path = request.path # Skip SX expression URLs — they don't use trailing slashes - if "(" in path or path.startswith("/~"): + if "(" in path or "/~" in path: return None if (path != "/" + and path != "/sx/" and not path.endswith("/") and request.method == "GET" - and not path.startswith(("/static/", "/internal/", "/auth/")) + and not path.startswith(("/static/", "/internal/", "/auth/", "/sx/")) and "." not in path.rsplit("/", 1)[-1]): qs = request.query_string.decode() target = path + "/" + ("?" + qs if qs else "") return redirect(target, 301) - @app.get("/") + @app.get("/sx/") + async def sx_home(): + """SX docs home page.""" + return await eval_sx_url("/") + + @app.get("/sx/") async def sx_eval_route(expr): - """Catch-all: evaluate SX expression URLs.""" + """Catch-all: evaluate SX expression URLs under /sx/ prefix.""" result = await eval_sx_url(f"/{expr}") if result is None: from quart import abort diff --git a/sx/bp/pages/routes.py b/sx/bp/pages/routes.py index 62775e7..260c1a7 100644 --- a/sx/bp/pages/routes.py +++ b/sx/bp/pages/routes.py @@ -23,7 +23,7 @@ def register(url_prefix: str = "/") -> Blueprint: # SSE stays in Python — fundamentally different paradigm (async generator). # ------------------------------------------------------------------ - @bp.get("/(geography.(hypermedia.(reference.(api.sse-time))))") + @bp.get("/sx/(geography.(hypermedia.(reference.(api.sse-time))))") async def ref_sse_time(): async def generate(): for _ in range(30): # stream for 60 seconds max @@ -38,7 +38,7 @@ def register(url_prefix: str = "/") -> Blueprint: _marsh_sale_idx = {"n": 0} - @bp.get("/(geography.(reactive.(api.flash-sale)))") + @bp.get("/sx/(geography.(reactive.(api.flash-sale)))") async def api_marsh_flash_sale(): from shared.sx.helpers import sx_response prices = [14.99, 9.99, 24.99, 12.49, 7.99, 29.99, 4.99, 16.50] @@ -60,7 +60,7 @@ def register(url_prefix: str = "/") -> Blueprint: _settle_counter = {"n": 0} - @bp.get("/(geography.(reactive.(api.settle-data)))") + @bp.get("/sx/(geography.(reactive.(api.settle-data)))") async def api_settle_data(): from shared.sx.helpers import sx_response _settle_counter["n"] += 1 @@ -76,7 +76,7 @@ def register(url_prefix: str = "/") -> Blueprint: # --- Demo 4: signal-bound URL endpoints --- - @bp.get("/(geography.(reactive.(api.search-products)))") + @bp.get("/sx/(geography.(reactive.(api.search-products)))") async def api_search_products(): from shared.sx.helpers import sx_response q = request.args.get("q", "") @@ -95,7 +95,7 @@ def register(url_prefix: str = "/") -> Blueprint: ) return sx_response(sx_src) - @bp.get("/(geography.(reactive.(api.search-events)))") + @bp.get("/sx/(geography.(reactive.(api.search-events)))") async def api_search_events(): from shared.sx.helpers import sx_response q = request.args.get("q", "") @@ -114,7 +114,7 @@ def register(url_prefix: str = "/") -> Blueprint: ) return sx_response(sx_src) - @bp.get("/(geography.(reactive.(api.search-posts)))") + @bp.get("/sx/(geography.(reactive.(api.search-posts)))") async def api_search_posts(): from shared.sx.helpers import sx_response q = request.args.get("q", "") @@ -135,7 +135,7 @@ def register(url_prefix: str = "/") -> Blueprint: # --- Demo 5: marsh transform endpoint --- - @bp.get("/(geography.(reactive.(api.catalog)))") + @bp.get("/sx/(geography.(reactive.(api.catalog)))") async def api_catalog(): from shared.sx.helpers import sx_response items = [ diff --git a/sx/sx/analyzer.sx b/sx/sx/analyzer.sx index e3a4595..045f791 100644 --- a/sx/sx/analyzer.sx +++ b/sx/sx/analyzer.sx @@ -12,7 +12,7 @@ "Each bar shows how many of the " (strong (str total-components)) " total components a page actually needs, computed by the " - (a :href "/(language.(spec.deps))" :class "text-violet-700 underline" "deps.sx") + (a :href "/sx/(language.(spec.deps))" :class "text-violet-700 underline" "deps.sx") " transitive closure algorithm. " "Click a page to see its component tree; expand a component to see its SX source.") diff --git a/sx/sx/cssx.sx b/sx/sx/cssx.sx index e40b264..eadca6d 100644 --- a/sx/sx/cssx.sx +++ b/sx/sx/cssx.sx @@ -373,7 +373,7 @@ "directly. They arrive as part of the rendered HTML — keyframes, custom properties, " "scoped rules, anything.") (li (strong "External stylesheets: ") "Components can reference pre-loaded CSS files " - "or lazy-load them via " (code "~suspense") " (see " (a :href "/(applications.(cssx.async))" "Async CSS") ").") + "or lazy-load them via " (code "~suspense") " (see " (a :href "/sx/(applications.(cssx.async))" "Async CSS") ").") (li (strong "Custom properties: ") "A " (code "~theme") " component sets " (code "--color-primary") " etc. via a " (code "