From de80d921e9cd628c627ce17c2e4e7bd1af157afa Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 12 Mar 2026 19:07:09 +0000 Subject: [PATCH] Prefix all SX URLs with /sx/ for WhatsApp-safe sharing All routes moved under /sx/ prefix: - / redirects to /sx/ - /sx/ serves home page - /sx/ is the catch-all for SX expression URLs - Bare /(...) and /~... redirect to /sx/(...) and /sx/~... - All ~600 hrefs, sx-get attrs, defhandler paths, redirect targets, and blueprint routes updated across 44 files Co-Authored-By: Claude Opus 4.6 --- sx/app.py | 30 +- sx/bp/pages/routes.py | 14 +- sx/sx/analyzer.sx | 2 +- sx/sx/cssx.sx | 2 +- sx/sx/docs-content.sx | 2 +- sx/sx/essays/continuations.sx | 2 +- sx/sx/essays/godel-escher-bach.sx | 4 +- sx/sx/essays/reflexive-web.sx | 8 +- sx/sx/essays/self-defining-medium.sx | 20 +- sx/sx/essays/sx-and-ai.sx | 4 +- sx/sx/essays/tail-call-optimization.sx | 2 +- sx/sx/essays/zero-tooling.sx | 2 +- sx/sx/examples-content.sx | 52 +-- sx/sx/examples.sx | 16 +- sx/sx/handlers/examples.sx | 76 ++--- sx/sx/handlers/ref-api.sx | 38 +-- sx/sx/layouts.sx | 4 +- sx/sx/nav-data.sx | 360 ++++++++++---------- sx/sx/not-found.sx | 4 +- sx/sx/page-functions.sx | 2 +- sx/sx/plans/content-addressed-components.sx | 8 +- sx/sx/plans/environment-images.sx | 6 +- sx/sx/plans/isomorphic.sx | 42 +-- sx/sx/plans/js-bootstrapper.sx | 2 +- sx/sx/plans/predictive-prefetch.sx | 2 +- sx/sx/plans/reader-macros.sx | 2 +- sx/sx/plans/runtime-slicing.sx | 14 +- sx/sx/plans/self-hosting-bootstrapper.sx | 2 +- sx/sx/plans/status.sx | 30 +- sx/sx/plans/sx-urls.sx | 22 +- sx/sx/plans/typed-sx.sx | 14 +- sx/sx/reactive-islands/event-bridge.sx | 2 +- sx/sx/reactive-islands/index.sx | 2 +- sx/sx/reactive-islands/marshes.sx | 20 +- sx/sx/reactive-islands/plan.sx | 4 +- sx/sx/routing-analyzer.sx | 4 +- sx/sx/specs-explorer.sx | 4 +- sx/sx/specs.sx | 94 ++--- sx/sx/sx-urls.sx | 38 +-- sx/sx/testing.sx | 12 +- sx/sxc/examples.sx | 96 +++--- sx/sxc/pages/docs.sx | 92 ++--- sx/sxc/pages/sx_router.py | 112 +++--- sx/sxc/reference.sx | 120 +++---- 44 files changed, 701 insertions(+), 687 deletions(-) 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 "