Send all responses as sexp wire format with client-side rendering

- Server sends sexp source text, client (sexp.js) renders everything
- SexpExpr marker class for nested sexp composition in serialize()
- sexp_page() HTML shell with data-mount="body" for full page loads
- sexp_response() returns text/sexp for OOB/partial responses
- ~app-body layout component replaces ~app-layout (no raw!)
- ~rich-text is the only component using raw! (for CMS HTML content)
- Fragment endpoints return text/sexp, auto-wrapped in SexpExpr
- All _*_html() helpers converted to _*_sexp() returning sexp source
- Head auto-hoist: sexp.js moves meta/title/link/script[ld+json]
  from rendered body to document.head automatically
- Unknown components render warning box instead of crashing page
- Component kwargs preserve AST for lazy rendering (fixes <> in kwargs)
- Fix unterminated paren in events/sexp/tickets.sexpr

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 09:45:07 +00:00
parent 0d48fd22ee
commit 22802bd36b
270 changed files with 7153 additions and 5382 deletions

View File

@@ -23,6 +23,7 @@ from .services import (
from shared.browser.app.redis_cacher import cache_page
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response
def register():
browse_bp = Blueprint("browse", __name__)
@@ -49,10 +50,10 @@ def register():
ctx.update(p_data)
if not is_htmx_request():
html = await render_market_home_page(ctx)
return await make_response(html)
else:
html = await render_market_home_oob(ctx)
return await make_response(html)
sexp_src = await render_market_home_oob(ctx)
return sexp_response(sexp_src)
@browse_bp.get("/all/")
@cache_page(tag="browse")
@@ -80,13 +81,15 @@ def register():
tctx.update(full_context)
if not is_htmx_request():
html = await render_browse_page(tctx)
resp = await make_response(html)
elif product_info["page"] > 1:
tctx.update(product_info)
html = await render_browse_cards(tctx)
sexp_src = await render_browse_cards(tctx)
resp = sexp_response(sexp_src)
else:
html = await render_browse_oob(tctx)
sexp_src = await render_browse_oob(tctx)
resp = sexp_response(sexp_src)
resp = await make_response(html)
resp.headers["Hx-Push-Url"] = _current_url_without_page()
return _vary(resp)
@@ -119,13 +122,15 @@ def register():
tctx.update(full_context)
if not is_htmx_request():
html = await render_browse_page(tctx)
resp = await make_response(html)
elif product_info["page"] > 1:
tctx.update(product_info)
html = await render_browse_cards(tctx)
sexp_src = await render_browse_cards(tctx)
resp = sexp_response(sexp_src)
else:
html = await render_browse_oob(tctx)
sexp_src = await render_browse_oob(tctx)
resp = sexp_response(sexp_src)
resp = await make_response(html)
resp.headers["Hx-Push-Url"] = _current_url_without_page()
return _vary(resp)
@@ -158,16 +163,18 @@ def register():
tctx.update(full_context)
if not is_htmx_request():
html = await render_browse_page(tctx)
resp = await make_response(html)
elif product_info["page"] > 1:
tctx.update(product_info)
html = await render_browse_cards(tctx)
sexp_src = await render_browse_cards(tctx)
resp = sexp_response(sexp_src)
else:
html = await render_browse_oob(tctx)
sexp_src = await render_browse_oob(tctx)
resp = sexp_response(sexp_src)
resp = await make_response(html)
resp.headers["Hx-Push-Url"] = _current_url_without_page()
return _vary(resp)
return browse_bp

View File

@@ -18,7 +18,10 @@ from ...market.filters.qs import decode
def _hx_fragment_request() -> bool:
return request.headers.get("HX-Request", "").lower() == "true"
return (
request.headers.get("SX-Request", "").lower() == "true"
or request.headers.get("HX-Request", "").lower() == "true"
)
async def _productInfo(top_slug=None, sub_slug=None):
"""