GraphSX URL routing: s-expression URLs for sx-docs
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m50s

Replace path-based URLs with nested s-expression URLs across the sx app.
URLs like /language/docs/introduction become /(language.(doc.introduction)),
making the URL simultaneously a query, render instruction, and address.

- Add sx_router.py: catch-all route evaluator with dot→space conversion,
  auto-quoting slugs, two-phase eval, streaming detection, 301 redirects
- Add page-functions.sx: section + page functions for URL dispatch
- Rewrite nav-data.sx: ~200 hrefs to SX expression format, tree-descent
  nav matching via has-descendant-href? (replaces prefix heuristics)
- Convert ~120 old-style hrefs across 26 .sx content files
- Add SX Protocol proposal (etc/plans/sx-protocol)
- Wire catch-all route in app.py with before_request redirect handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 09:51:04 +00:00
parent 0cc2f178a9
commit da1ca6009a
32 changed files with 1763 additions and 348 deletions

View File

@@ -114,10 +114,37 @@ def create_app() -> "Quart":
from shared.sx.pages import auto_mount_pages
auto_mount_pages(app, "sx")
# --- GraphSX catch-all route: SX expression URLs ---
from sxc.pages.sx_router import eval_sx_url, redirect_old_url
@app.before_request
async def sx_url_redirect():
"""Redirect old-style paths to SX expression URLs (301)."""
from quart import request, redirect as q_redirect
path = request.path
# Skip non-page paths
if path.startswith(("/static/", "/internal/", "/auth/", "/sx/")):
return None
# Skip SX expression URLs (already in new format)
if path.startswith("/(") or path.startswith("/~"):
return None
# Skip API/handler paths
if "/api/" in path:
return None
new_url = redirect_old_url(path)
if new_url:
qs = request.query_string.decode()
if qs:
new_url += "?" + qs
return q_redirect(new_url, 301)
@app.before_request
async def trailing_slash_redirect():
from quart import request, redirect
path = request.path
# Skip SX expression URLs — they don't use trailing slashes
if "(" in path or path.startswith("/~"):
return None
if (path != "/"
and not path.endswith("/")
and request.method == "GET"
@@ -128,6 +155,15 @@ def create_app() -> "Quart":
target = path + "/" + ("?" + qs if qs else "")
return redirect(target, 301)
@app.get("/<path:expr>")
async def sx_eval_route(expr):
"""Catch-all: evaluate SX expression URLs."""
result = await eval_sx_url(f"/{expr}")
if result is None:
from quart import abort
abort(404)
return result
@app.errorhandler(404)
async def sx_not_found(e):
from quart import request, make_response