Migrate all apps to defpage declarative page routes
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m41s

Replace Python GET page handlers with declarative defpage definitions in .sx
files across all 8 apps (sx docs, orders, account, market, cart, federation,
events, blog). Each app now has sxc/pages/ with setup functions, layout
registrations, page helpers, and .sx defpage declarations.

Core infrastructure: add g I/O primitive, PageDef support for auth/layout/
data/content/filter/aside/menu slots, post_author auth level, and custom
layout registration. Remove ~1400 lines of render_*_page/render_*_oob
boilerplate. Update all endpoint references in routes, sx_components, and
templates to defpage_* naming.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 14:52:34 +00:00
parent 5b4cacaf19
commit c243d17eeb
108 changed files with 3598 additions and 2851 deletions

View File

@@ -102,26 +102,136 @@ def root_header_sx(ctx: dict, *, oob: bool = False) -> str:
)
def mobile_nav_sx(ctx: dict) -> str:
"""Build mobile navigation panel from context fragments (nav_tree, auth_menu)."""
def mobile_menu_sx(*sections: str) -> str:
"""Assemble mobile menu from pre-built sections (deepest first)."""
parts = [s for s in sections if s]
return "(<> " + " ".join(parts) + ")" if parts else ""
def mobile_root_nav_sx(ctx: dict) -> str:
"""Root-level mobile nav via ~mobile-root-nav component."""
nav_tree = ctx.get("nav_tree") or ""
auth_menu = ctx.get("auth_menu") or ""
if not nav_tree and not auth_menu:
return ""
return sx_call("mobile-root-nav",
nav_tree=_as_sx(nav_tree),
auth_menu=_as_sx(auth_menu),
)
# ---------------------------------------------------------------------------
# Shared nav-item builders — used by BOTH desktop headers and mobile menus
# ---------------------------------------------------------------------------
def _post_nav_items_sx(ctx: dict) -> str:
"""Build post-level nav items (container_nav + admin cog). Shared by
``post_header_sx`` (desktop) and ``post_mobile_nav_sx`` (mobile)."""
post = ctx.get("post") or {}
slug = post.get("slug", "")
if not slug:
return ""
parts: list[str] = []
if nav_tree:
nav_tree_sx = _as_sx(nav_tree)
page_cart_count = ctx.get("page_cart_count", 0)
if page_cart_count and page_cart_count > 0:
cart_href = call_url(ctx, "cart_url", f"/{slug}/")
parts.append(sx_call("page-cart-badge", href=cart_href,
count=str(page_cart_count)))
container_nav = str(ctx.get("container_nav") or "").strip()
# Skip empty fragment wrappers like "(<> )"
if container_nav and container_nav.replace("(<>", "").replace(")", "").strip():
parts.append(
f'(div :class "flex flex-col gap-2 p-3 text-sm" {nav_tree_sx})'
)
if auth_menu:
auth_sx = _as_sx(auth_menu)
parts.append(
f'(div :class "p-3 border-t border-stone-200" {auth_sx})'
f'(div :id "entries-calendars-nav-wrapper"'
f' :class "flex flex-col sm:flex-row sm:items-center gap-2'
f' border-r border-stone-200 mr-2 sm:max-w-2xl"'
f' {container_nav})'
)
# Admin cog
admin_nav = ctx.get("post_admin_nav")
if not admin_nav:
rights = ctx.get("rights") or {}
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
if has_admin and slug:
from quart import request
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
is_admin_page = ctx.get("is_admin_section") or "/admin" in request.path
sel_cls = "!bg-stone-500 !text-white" if is_admin_page else ""
base_cls = ("justify-center cursor-pointer flex flex-row"
" items-center gap-2 rounded bg-stone-200 text-black p-3")
admin_nav = (
f'(div :class "relative nav-group"'
f' (a :href "{admin_href}"'
f' :class "{base_cls} {sel_cls}"'
f' (i :class "fa fa-cog" :aria-hidden "true")))'
)
if admin_nav:
parts.append(admin_nav)
return "(<> " + " ".join(parts) + ")" if parts else ""
def _post_admin_nav_items_sx(ctx: dict, slug: str,
selected: str = "") -> str:
"""Build post-admin nav items (calendars, markets, etc.). Shared by
``post_admin_header_sx`` (desktop) and mobile menu."""
select_colours = ctx.get("select_colours", "")
parts: list[str] = []
items = [
("events_url", f"/{slug}/admin/", "calendars"),
("market_url", f"/{slug}/admin/", "markets"),
("cart_url", f"/{slug}/admin/payments/", "payments"),
("blog_url", f"/{slug}/admin/entries/", "entries"),
("blog_url", f"/{slug}/admin/data/", "data"),
("blog_url", f"/{slug}/admin/preview/", "preview"),
("blog_url", f"/{slug}/admin/edit/", "edit"),
("blog_url", f"/{slug}/admin/settings/", "settings"),
]
for url_key, path, label in items:
url_fn = ctx.get(url_key)
if not callable(url_fn):
continue
href = url_fn(path)
is_sel = label == selected
parts.append(sx_call("nav-link", href=href, label=label,
select_colours=select_colours,
is_selected=is_sel or None))
return "(<> " + " ".join(parts) + ")" if parts else ""
# ---------------------------------------------------------------------------
# Mobile menu section builders — wrap shared nav items for hamburger panel
# ---------------------------------------------------------------------------
def post_mobile_nav_sx(ctx: dict) -> str:
"""Post-level mobile menu section."""
nav = _post_nav_items_sx(ctx)
if not nav:
return ""
post = ctx.get("post") or {}
slug = post.get("slug", "")
title = (post.get("title") or slug)[:40]
return sx_call("mobile-menu-section",
label=title,
href=call_url(ctx, "blog_url", f"/{slug}/"),
level=1,
items=SxExpr(nav),
)
def post_admin_mobile_nav_sx(ctx: dict, slug: str,
selected: str = "") -> str:
"""Post-admin mobile menu section."""
nav = _post_admin_nav_items_sx(ctx, slug, selected)
if not nav:
return ""
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
return sx_call("mobile-menu-section",
label="admin", href=admin_href, level=2,
items=SxExpr(nav),
)
def search_mobile_sx(ctx: dict) -> str:
"""Build mobile search input as sx call string."""
return sx_call("search-mobile",
@@ -154,43 +264,7 @@ def post_header_sx(ctx: dict, *, oob: bool = False) -> str:
feature_image = post.get("feature_image")
label_sx = sx_call("post-label", feature_image=feature_image, title=title)
nav_parts: list[str] = []
page_cart_count = ctx.get("page_cart_count", 0)
if page_cart_count and page_cart_count > 0:
cart_href = call_url(ctx, "cart_url", f"/{slug}/")
nav_parts.append(sx_call("page-cart-badge", href=cart_href, count=str(page_cart_count)))
container_nav = ctx.get("container_nav")
if container_nav:
nav_parts.append(
f'(div :id "entries-calendars-nav-wrapper"'
f' :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"'
f' {container_nav})'
)
# Admin cog
admin_nav = ctx.get("post_admin_nav")
if not admin_nav:
rights = ctx.get("rights") or {}
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
if has_admin and slug:
from quart import request
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
is_admin_page = ctx.get("is_admin_section") or "/admin" in request.path
sel_cls = "!bg-stone-500 !text-white" if is_admin_page else ""
base_cls = ("justify-center cursor-pointer flex flex-row"
" items-center gap-2 rounded bg-stone-200 text-black p-3")
admin_nav = (
f'(div :class "relative nav-group"'
f' (a :href "{admin_href}"'
f' :class "{base_cls} {sel_cls}"'
f' (i :class "fa fa-cog" :aria-hidden "true")))'
)
if admin_nav:
nav_parts.append(admin_nav)
nav_sx = "(<> " + " ".join(nav_parts) + ")" if nav_parts else None
nav_sx = _post_nav_items_sx(ctx) or None
link_href = call_url(ctx, "blog_url", f"/{slug}/")
return sx_call("menu-row-sx",
@@ -212,39 +286,7 @@ def post_admin_header_sx(ctx: dict, slug: str, *, oob: bool = False,
label_parts.append(f'(span :class "text-white" "{escape(selected)}")')
label_sx = "(<> " + " ".join(label_parts) + ")"
# Nav items
select_colours = ctx.get("select_colours", "")
base_cls = ("justify-center cursor-pointer flex flex-row items-center"
" gap-2 rounded bg-stone-200 text-black p-3")
selected_cls = ("justify-center cursor-pointer flex flex-row items-center"
" gap-2 rounded !bg-stone-500 !text-white p-3")
nav_parts: list[str] = []
items = [
("events_url", f"/{slug}/admin/", "calendars"),
("market_url", f"/{slug}/admin/", "markets"),
("cart_url", f"/{slug}/admin/payments/", "payments"),
("blog_url", f"/{slug}/admin/entries/", "entries"),
("blog_url", f"/{slug}/admin/data/", "data"),
("blog_url", f"/{slug}/admin/preview/", "preview"),
("blog_url", f"/{slug}/admin/edit/", "edit"),
("blog_url", f"/{slug}/admin/settings/", "settings"),
]
for url_key, path, label in items:
url_fn = ctx.get(url_key)
if not callable(url_fn):
continue
href = url_fn(path)
is_sel = label == selected
cls = selected_cls if is_sel else base_cls
aria = "true" if is_sel else None
nav_parts.append(
f'(div :class "relative nav-group"'
f' (a :href "{escape(href)}"'
+ (f' :aria-selected "true"' if aria else "")
+ f' :class "{cls} {escape(select_colours)}"'
+ f' "{escape(label)}"))'
)
nav_sx = "(<> " + " ".join(nav_parts) + ")" if nav_parts else None
nav_sx = _post_admin_nav_items_sx(ctx, slug, selected) or None
if not admin_href:
blog_fn = ctx.get("blog_url")
@@ -301,7 +343,7 @@ def full_page_sx(ctx: dict, *, header_rows: str,
"""
# Auto-generate mobile nav from context when no menu provided
if not menu:
menu = mobile_nav_sx(ctx)
menu = mobile_root_nav_sx(ctx)
body_sx = sx_call("app-body",
header_rows=SxExpr(f"(<> {header_rows})") if header_rows else None,
filter=SxExpr(filter) if filter else None,