Migrate all apps to defpage declarative page routes

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

@@ -99,25 +99,30 @@ def create_app() -> "Quart":
app.jinja_loader,
])
# Setup defpage routes
from sxc.pages import setup_market_pages
setup_market_pages()
from shared.sx.pages import mount_pages
# All markets: / — global view across all pages
app.register_blueprint(
register_all_markets(),
url_prefix="/",
)
all_markets_bp = register_all_markets()
mount_pages(all_markets_bp, "market", names=["all-markets-index"])
app.register_blueprint(all_markets_bp, url_prefix="/")
# Page markets: /<slug>/ — markets for a single page
app.register_blueprint(
register_page_markets(),
url_prefix="/<slug>",
)
page_markets_bp = register_page_markets()
mount_pages(page_markets_bp, "market", names=["page-markets-index"])
app.register_blueprint(page_markets_bp, url_prefix="/<slug>")
# Page admin: /<slug>/admin/ — post-level admin for markets
app.register_blueprint(
register_page_admin(),
url_prefix="/<slug>/admin",
)
page_admin_bp = register_page_admin()
mount_pages(page_admin_bp, "market", names=["page-admin"])
app.register_blueprint(page_admin_bp, url_prefix="/<slug>/admin")
# Market blueprint nested under post slug: /<page_slug>/<market_slug>/
# Defpages for market-home and market-admin are mounted inside their
# respective nested blueprints (browse and admin register functions).
app.register_blueprint(
register_market_bp(
url_prefix="/",

View File

@@ -2,70 +2,57 @@
All-markets blueprint — shows markets across ALL pages.
Mounted at / (root of market app). No slug context.
Routes:
GET / — full page with first page of markets
GET /all-markets — HTMX fragment for infinite scroll
GET / handled by defpage. GET /all-markets is pagination fragment.
"""
from __future__ import annotations
from quart import Blueprint, g, request, render_template, make_response
from quart import Blueprint, g, request
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.helpers import sx_response
from shared.infrastructure.data_client import fetch_data
from shared.contracts.dtos import PostDTO, dto_from_dict
from shared.services.registry import services
async def _load_markets(page, per_page=20):
"""Load all markets + page info for container badges."""
markets, has_more = await services.market.list_marketplaces(
g.s, page=page, per_page=per_page,
)
# Batch-load page info for container_ids
page_info = {}
if markets:
post_ids = list({
m.container_id for m in markets
if m.container_type == "page"
})
if post_ids:
raw_posts = await fetch_data("blog", "posts-by-ids",
params={"ids": ",".join(str(i) for i in post_ids)},
required=False) or []
for raw_p in raw_posts:
p = dto_from_dict(PostDTO, raw_p)
page_info[p.id] = {"title": p.title, "slug": p.slug}
return markets, has_more, page_info
def register() -> Blueprint:
bp = Blueprint("all_markets", __name__)
async def _load_markets(page, per_page=20):
"""Load all markets + page info for container badges."""
markets, has_more = await services.market.list_marketplaces(
g.s, page=page, per_page=per_page,
)
# Batch-load page info for container_ids
page_info = {}
if markets:
post_ids = list({
m.container_id for m in markets
if m.container_type == "page"
})
if post_ids:
raw_posts = await fetch_data("blog", "posts-by-ids",
params={"ids": ",".join(str(i) for i in post_ids)},
required=False) or []
for raw_p in raw_posts:
p = dto_from_dict(PostDTO, raw_p)
page_info[p.id] = {"title": p.title, "slug": p.slug}
return markets, has_more, page_info
@bp.get("/")
async def index():
@bp.before_request
async def _prepare_page_data():
"""Load all-markets data for defpage routes."""
endpoint = request.endpoint or ""
if not endpoint.endswith("defpage_all_markets_index"):
return
page = int(request.args.get("page", 1))
markets, has_more, page_info = await _load_markets(page)
ctx = dict(
markets=markets,
has_more=has_more,
page_info=page_info,
page=page,
)
from shared.sx.page import get_template_context
from sx.sx_components import render_all_markets_page, render_all_markets_oob
tctx = await get_template_context()
if is_htmx_request():
sx_src = await render_all_markets_oob(tctx, markets, has_more, page_info, page)
return sx_response(sx_src)
else:
html = await render_all_markets_page(tctx, markets, has_more, page_info, page)
return await make_response(html, 200)
g.all_markets_data = {
"markets": markets, "has_more": has_more,
"page_info": page_info, "page": page,
}
@bp.get("/all-markets")
async def markets_fragment():

View File

@@ -5,17 +5,13 @@ from quart import (
g,
Blueprint,
abort,
render_template,
render_template_string,
make_response,
current_app,
)
from shared.config import config
from .services.nav import category_context, get_nav
from .services.blacklist.category import is_category_blocked
from .services import (
_hx_fragment_request,
_productInfo,
_vary,
_current_url_without_page,
@@ -33,27 +29,9 @@ def register():
register_product(),
)
@browse_bp.get("/")
@cache_page(tag="browse")
async def home():
"""
Market landing page.
Uses the post data hydrated by the app-level before_request (g.post_data).
"""
p_data = getattr(g, "post_data", None) or {}
# Determine which template to use based on request type
from shared.sx.page import get_template_context
from sx.sx_components import render_market_home_page, render_market_home_oob
ctx = await get_template_context()
ctx.update(p_data)
if not is_htmx_request():
html = await render_market_home_page(ctx)
return await make_response(html)
else:
sx_src = await render_market_home_oob(ctx)
return sx_response(sx_src)
# Mount defpage for market home (GET /)
from shared.sx.pages import mount_pages
mount_pages(browse_bp, "market", names=["market-home"])
@browse_bp.get("/all/")
@cache_page(tag="browse")

View File

@@ -1,31 +1,13 @@
from __future__ import annotations
from quart import (
render_template, make_response, Blueprint
)
from shared.browser.app.authz import require_admin
from quart import Blueprint
def register():
bp = Blueprint("admin", __name__, url_prefix='/admin')
# ---------- Pages ----------
@bp.get("/")
@require_admin
async def admin():
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.page import get_template_context
from sx.sx_components import render_market_admin_page, render_market_admin_oob
# Mount defpage for market admin (GET /)
from shared.sx.pages import mount_pages
mount_pages(bp, "market", names=["market-admin"])
tctx = await get_template_context()
if not is_htmx_request():
html = await render_market_admin_page(tctx)
return await make_response(html)
else:
from shared.sx.helpers import sx_response
sx_src = await render_market_admin_oob(tctx)
return sx_response(sx_src)
return bp

View File

@@ -6,7 +6,6 @@ import unicodedata
from quart import make_response, request, g, Blueprint
from shared.browser.app.authz import require_admin
from shared.browser.app.utils.htmx import is_htmx_request
from shared.services.registry import services
from shared.sx.helpers import sx_response
@@ -27,19 +26,16 @@ def _slugify(value: str, max_len: int = 255) -> str:
def register():
bp = Blueprint("page_admin", __name__)
@bp.get("/")
@require_admin
async def admin(**kwargs):
@bp.before_request
async def _prepare_page_data():
"""Pre-render page admin content for defpage (async helper)."""
endpoint = request.endpoint or ""
if request.method != "GET" or not endpoint.endswith("defpage_page_admin"):
return
from shared.sx.page import get_template_context
from sx.sx_components import render_page_admin_page, render_page_admin_oob
tctx = await get_template_context()
if not is_htmx_request():
html = await render_page_admin_page(tctx)
return await make_response(html)
else:
sx_src = await render_page_admin_oob(tctx)
return sx_response(sx_src)
from sx.sx_components import _markets_admin_panel_sx
ctx = await get_template_context()
g.page_admin_content = await _markets_admin_panel_sx(ctx)
@bp.post("/new/")
@require_admin

View File

@@ -2,55 +2,40 @@
Page-markets blueprint — shows markets for a single page.
Mounted at /<slug> (page-scoped). Requires g.post_data from hydrate_post.
Routes:
GET /<slug>/ — full page scoped to this page
GET /<slug>/page-markets — HTMX fragment for infinite scroll
GET / handled by defpage. GET /page-markets is pagination fragment.
"""
from __future__ import annotations
from quart import Blueprint, g, request, render_template, make_response
from quart import Blueprint, g, request
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.helpers import sx_response
from shared.services.registry import services
async def _load_markets(post_id, page, per_page=20):
"""Load markets for this page's container."""
markets, has_more = await services.market.list_marketplaces(
g.s, "page", post_id, page=page, per_page=per_page,
)
return markets, has_more
def register() -> Blueprint:
bp = Blueprint("page_markets", __name__)
async def _load_markets(post_id, page, per_page=20):
"""Load markets for this page's container."""
markets, has_more = await services.market.list_marketplaces(
g.s, "page", post_id, page=page, per_page=per_page,
)
return markets, has_more
@bp.get("/")
async def index():
@bp.before_request
async def _prepare_page_data():
"""Load page-markets data for defpage routes."""
endpoint = request.endpoint or ""
if not endpoint.endswith("defpage_page_markets_index"):
return
post = g.post_data["post"]
page = int(request.args.get("page", 1))
markets, has_more = await _load_markets(post["id"], page)
ctx = dict(
markets=markets,
has_more=has_more,
page_info={},
page=page,
)
from shared.sx.page import get_template_context
from sx.sx_components import render_page_markets_page, render_page_markets_oob
tctx = await get_template_context()
tctx["post"] = post
if is_htmx_request():
sx_src = await render_page_markets_oob(tctx, markets, has_more, page)
return sx_response(sx_src)
else:
html = await render_page_markets_page(tctx, markets, has_more, page)
return await make_response(html, 200)
g.page_markets_data = {
"markets": markets, "has_more": has_more,
"page": page, "post_slug": post.get("slug", ""),
}
@bp.get("/page-markets")
async def markets_fragment():

View File

@@ -111,7 +111,7 @@ def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
sub_div=SxExpr(sub_div) if sub_div else None,
)
link_href = url_for("market.browse.home")
link_href = url_for("market.browse.defpage_market_home")
# Build desktop nav from categories
categories = ctx.get("categories", {})
@@ -159,7 +159,7 @@ def _desktop_category_nav_sx(ctx: dict, categories: dict, qs: str,
admin_sx = ""
if rights and rights.get("admin"):
admin_href = prefix + url_for("market.admin.admin")
admin_href = prefix + url_for("market.admin.defpage_market_admin")
admin_sx = sx_call("market-admin-link", href=admin_href, hx_select=hx_select)
return sx_call("market-desktop-category-nav",
@@ -1203,46 +1203,6 @@ def _no_markets_sx(message: str = "No markets available") -> str:
cls="px-3 py-12 text-center text-stone-400")
async def render_all_markets_page(ctx: dict, markets: list, has_more: bool,
page_info: dict, page: int) -> str:
"""Full page: all markets listing."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
if markets:
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
content = _markets_grid(cards)
else:
content = _no_markets_sx()
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
hdr = root_header_sx(ctx)
return full_page_sx(ctx, header_rows=hdr, content=content)
async def render_all_markets_oob(ctx: dict, markets: list, has_more: bool,
page_info: dict, page: int) -> str:
"""OOB response: all markets listing."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
if markets:
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
content = _markets_grid(cards)
else:
content = _no_markets_sx()
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
oobs = root_header_sx(ctx, oob=True)
return oob_page_sx(oobs=oobs, content=content)
async def render_all_markets_cards(markets: list, has_more: bool,
page_info: dict, page: int) -> str:
"""Pagination fragment: all markets cards."""
@@ -1258,54 +1218,6 @@ async def render_all_markets_cards(markets: list, has_more: bool,
# Page markets
# ---------------------------------------------------------------------------
async def render_page_markets_page(ctx: dict, markets: list, has_more: bool,
page: int) -> str:
"""Full page: page-scoped markets listing."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
post = ctx.get("post", {})
post_slug = post.get("slug", "")
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
if markets:
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
content = _markets_grid(cards)
else:
content = _no_markets_sx("No markets for this page")
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
hdr = root_header_sx(ctx)
hdr = "(<> " + hdr + " " + header_child_sx(_post_header_sx(ctx)) + ")"
return full_page_sx(ctx, header_rows=hdr, content=content)
async def render_page_markets_oob(ctx: dict, markets: list, has_more: bool,
page: int) -> str:
"""OOB response: page-scoped markets."""
from quart import url_for
from shared.utils import route_prefix
prefix = route_prefix()
post = ctx.get("post", {})
post_slug = post.get("slug", "")
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
if markets:
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
content = _markets_grid(cards)
else:
content = _no_markets_sx("No markets for this page")
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
oobs = _oob_header_sx("post-header-child", "market-header-child", "")
oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + ")"
return oob_page_sx(oobs=oobs, content=content)
async def render_page_markets_cards(markets: list, has_more: bool,
page: int, post_slug: str) -> str:
"""Pagination fragment: page-scoped markets cards."""
@@ -1322,31 +1234,6 @@ async def render_page_markets_cards(markets: list, has_more: bool,
# Market landing page
# ---------------------------------------------------------------------------
async def render_market_home_page(ctx: dict) -> str:
"""Full page: market landing page (post content)."""
post = ctx.get("post") or {}
content = _market_landing_content_sx(post)
hdr = root_header_sx(ctx)
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + ")"
hdr = "(<> " + hdr + " " + header_child_sx(child) + ")"
menu = _mobile_nav_panel_sx(ctx)
return full_page_sx(ctx, header_rows=hdr, content=content, menu=menu)
async def render_market_home_oob(ctx: dict) -> str:
"""OOB response: market landing page."""
post = ctx.get("post") or {}
content = _market_landing_content_sx(post)
oobs = _oob_header_sx("post-header-child", "market-header-child",
_market_header_sx(ctx))
oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + " "
oobs += _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child") + ")"
menu = _mobile_nav_panel_sx(ctx)
return oob_page_sx(oobs=oobs, content=content, menu=menu)
def _market_landing_content_sx(post: dict) -> str:
"""Build market landing page content as sx."""
@@ -1485,29 +1372,6 @@ def _product_admin_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
# Market admin
# ---------------------------------------------------------------------------
async def render_market_admin_page(ctx: dict) -> str:
"""Full page: market admin."""
content = '"market admin"'
hdr = root_header_sx(ctx)
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + " "
child += _market_admin_header_sx(ctx, selected="markets") + ")"
hdr = "(<> " + hdr + " " + header_child_sx(child) + ")"
return full_page_sx(ctx, header_rows=hdr, content=content)
async def render_market_admin_oob(ctx: dict) -> str:
"""OOB response: market admin."""
content = '"market admin"'
oobs = "(<> " + _market_header_sx(ctx, oob=True) + " "
oobs += _oob_header_sx("market-header-child", "market-admin-header-child",
_market_admin_header_sx(ctx, selected="markets")) + " "
oobs += _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child",
"market-admin-row", "market-admin-header-child") + ")"
return oob_page_sx(oobs=oobs, content=content)
def _market_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str:
"""Build market admin header row — delegates to shared helper."""
@@ -1586,28 +1450,6 @@ async def render_markets_admin_list_panel(ctx: dict) -> str:
return await _markets_admin_panel_sx(ctx)
async def render_page_admin_page(ctx: dict) -> str:
"""Full page: page-level market admin."""
slug = (ctx.get("post") or {}).get("slug", "")
admin_hdr = post_admin_header_sx(ctx, slug, selected="markets")
hdr = root_header_sx(ctx)
child = "(<> " + _post_header_sx(ctx) + " " + admin_hdr + ")"
hdr = "(<> " + hdr + " " + header_child_sx(child) + ")"
content = await _markets_admin_panel_sx(ctx)
content = '(div :id "main-panel" ' + content + ')'
return full_page_sx(ctx, header_rows=hdr, content=content)
async def render_page_admin_oob(ctx: dict) -> str:
"""OOB response: page-level market admin."""
slug = (ctx.get("post") or {}).get("slug", "")
oobs = "(<> " + post_admin_header_sx(ctx, slug, oob=True, selected="markets") + " "
oobs += _clear_deeper_oob("post-row", "post-header-child",
"post-admin-row", "post-admin-header-child") + ")"
content = await _markets_admin_panel_sx(ctx)
content = '(div :id "main-panel" ' + content + ')'
return oob_page_sx(oobs=oobs, content=content)
# ---------------------------------------------------------------------------
# Public API: POST handler fragment renderers

0
market/sxc/__init__.py Normal file
View File

View File

@@ -0,0 +1,170 @@
"""Market defpage setup — registers layouts, page helpers, and loads .sx pages."""
from __future__ import annotations
from typing import Any
def setup_market_pages() -> None:
"""Register market-specific layouts, page helpers, and load page definitions."""
_register_market_layouts()
_register_market_helpers()
_load_market_page_files()
def _load_market_page_files() -> None:
import os
from shared.sx.pages import load_page_dir
load_page_dir(os.path.dirname(__file__), "market")
# ---------------------------------------------------------------------------
# Layouts
# ---------------------------------------------------------------------------
def _register_market_layouts() -> None:
from shared.sx.layouts import register_custom_layout
register_custom_layout("market", _market_full, _market_oob, _market_mobile)
register_custom_layout("market-admin", _market_admin_full, _market_admin_oob)
def _market_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx
from sx.sx_components import _post_header_sx, _market_header_sx
root_hdr = root_header_sx(ctx)
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + ")"
return "(<> " + root_hdr + " " + header_child_sx(child) + ")"
def _market_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import oob_header_sx
from sx.sx_components import _post_header_sx, _market_header_sx, _clear_deeper_oob
oobs = oob_header_sx("post-header-child", "market-header-child",
_market_header_sx(ctx))
oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + " "
oobs += _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child") + ")"
return oobs
def _market_mobile(ctx: dict, **kw: Any) -> str:
from sx.sx_components import _mobile_nav_panel_sx
return _mobile_nav_panel_sx(ctx)
def _market_admin_full(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import root_header_sx, header_child_sx
from sx.sx_components import (
_post_header_sx, _market_header_sx, _market_admin_header_sx,
)
selected = kw.get("selected", "")
root_hdr = root_header_sx(ctx)
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + " "
child += _market_admin_header_sx(ctx, selected=selected) + ")"
return "(<> " + root_hdr + " " + header_child_sx(child) + ")"
def _market_admin_oob(ctx: dict, **kw: Any) -> str:
from shared.sx.helpers import oob_header_sx
from sx.sx_components import (
_market_header_sx, _market_admin_header_sx, _clear_deeper_oob,
)
selected = kw.get("selected", "")
oobs = "(<> " + _market_header_sx(ctx, oob=True) + " "
oobs += oob_header_sx("market-header-child", "market-admin-header-child",
_market_admin_header_sx(ctx, selected=selected)) + " "
oobs += _clear_deeper_oob("post-row", "post-header-child",
"market-row", "market-header-child",
"market-admin-row", "market-admin-header-child") + ")"
return oobs
# ---------------------------------------------------------------------------
# Page helpers
# ---------------------------------------------------------------------------
def _register_market_helpers() -> None:
from shared.sx.pages import register_page_helpers
register_page_helpers("market", {
"all-markets-content": _h_all_markets_content,
"page-markets-content": _h_page_markets_content,
"page-admin-content": _h_page_admin_content,
"market-home-content": _h_market_home_content,
"market-admin-content": _h_market_admin_content,
})
def _h_all_markets_content():
from quart import g, url_for, request
from shared.utils import route_prefix
data = getattr(g, "all_markets_data", None)
if not data:
from sx.sx_components import _no_markets_sx
return _no_markets_sx()
markets = data["markets"]
has_more = data["has_more"]
page_info = data["page_info"]
page = data["page"]
prefix = route_prefix()
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
from sx.sx_components import _market_cards_sx, _markets_grid, _no_markets_sx
if markets:
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
content = _markets_grid(cards)
else:
content = _no_markets_sx()
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
def _h_page_markets_content():
from quart import g, url_for
from shared.utils import route_prefix
data = getattr(g, "page_markets_data", None)
if not data:
from sx.sx_components import _no_markets_sx
return _no_markets_sx("No markets for this page")
markets = data["markets"]
has_more = data["has_more"]
page = data["page"]
post_slug = data.get("post_slug", "")
prefix = route_prefix()
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
from sx.sx_components import _market_cards_sx, _markets_grid, _no_markets_sx
if markets:
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
show_page_badge=False, post_slug=post_slug)
content = _markets_grid(cards)
else:
content = _no_markets_sx("No markets for this page")
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
def _h_page_admin_content():
# Content pre-rendered by before_request (async _markets_admin_panel_sx)
from quart import g
content = getattr(g, "page_admin_content", "")
return '(div :id "main-panel" ' + content + ')'
def _h_market_home_content():
from quart import g
post_data = getattr(g, "post_data", {})
post = post_data.get("post", {})
from sx.sx_components import _market_landing_content_sx
return _market_landing_content_sx(post)
def _h_market_admin_content():
return '"market admin"'

View File

@@ -0,0 +1,37 @@
;; Market app defpage declarations.
;;
;; all-markets-index: / — global view across all pages
;; page-markets-index: / (on page_markets bp, mounted at /<slug>)
;; page-admin: / (on page_admin bp, mounted at /<slug>/admin)
;; market-home: / (on browse bp, mounted at /<page_slug>/<market_slug>)
;; market-admin: / (on admin bp, mounted at /<page_slug>/<market_slug>/admin)
(defpage all-markets-index
:path "/"
:auth :public
:layout :root
:content (all-markets-content))
(defpage page-markets-index
:path "/"
:auth :public
:layout :post
:content (page-markets-content))
(defpage page-admin
:path "/"
:auth :admin
:layout (:post-admin :selected "markets")
:content (page-admin-content))
(defpage market-home
:path "/"
:auth :public
:layout :market
:content (market-home-content))
(defpage market-admin
:path "/"
:auth :admin
:layout (:market-admin :selected "markets")
:content (market-admin-content))