diff --git a/blog/sexp/header.sexpr b/blog/sexp/header.sexpr
index 3ab3654..6debebe 100644
--- a/blog/sexp/header.sexpr
+++ b/blog/sexp/header.sexpr
@@ -1,22 +1,8 @@
;; Blog header components
-(defcomp ~blog-oob-header (&key parent-id child-id row-html)
- (div :id parent-id :hx-swap-oob "outerHTML" :class "w-full"
- (div :class "w-full" (raw! row-html)
- (div :id child-id))))
-
(defcomp ~blog-header-label ()
(div))
-(defcomp ~blog-post-label (&key feature-image title)
- (<> (when feature-image (img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
- (span title)))
-
-(defcomp ~blog-post-cart-link (&key href count)
- (a :href href :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition"
- (i :class "fa fa-shopping-cart" :aria-hidden "true")
- (span count)))
-
(defcomp ~blog-container-nav (&key container-nav-html)
(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
:id "entries-calendars-nav-wrapper" (raw! container-nav-html)))
diff --git a/blog/sexp/sexp_components.py b/blog/sexp/sexp_components.py
index 8c3a3c2..7b85bb9 100644
--- a/blog/sexp/sexp_components.py
+++ b/blog/sexp/sexp_components.py
@@ -15,6 +15,8 @@ from markupsafe import escape
from shared.sexp.jinja_bridge import render, load_service_components
from shared.sexp.helpers import (
call_url, get_asset_url, root_header_html,
+ post_header_html as _shared_post_header_html,
+ oob_header_html,
search_mobile_html, search_desktop_html,
full_page, oob_page,
)
@@ -24,14 +26,10 @@ load_service_components(os.path.dirname(os.path.dirname(__file__)))
# ---------------------------------------------------------------------------
-# OOB header helper
+# OOB header helper — delegates to shared
# ---------------------------------------------------------------------------
-def _oob_header_html(parent_id: str, child_id: str, row_html: str) -> str:
- """Wrap a header row in OOB div with child placeholder."""
- return render("blog-oob-header",
- parent_id=parent_id, child_id=child_id, row_html=row_html,
- )
+_oob_header_html = oob_header_html
# ---------------------------------------------------------------------------
@@ -48,59 +46,40 @@ def _blog_header_html(ctx: dict, *, oob: bool = False) -> str:
# ---------------------------------------------------------------------------
-# Post header helpers
+# Post header helpers — thin wrapper over shared post_header_html
# ---------------------------------------------------------------------------
def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
- """Build the post-level header row."""
- post = ctx.get("post") or {}
- slug = post.get("slug", "")
- title = (post.get("title") or "")[:160]
- feature_image = post.get("feature_image")
+ """Build the post-level header row (blog-specific: container-nav wrapping + admin cog)."""
+ overrides: dict = {}
- label_html = render("blog-post-label",
- feature_image=feature_image, title=title,
- )
-
- nav_parts = []
- 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(render("blog-post-cart-link",
- href=cart_href, count=str(page_cart_count),
- ))
-
- # Container nav fragments (calendars, markets)
+ # Blog wraps container_nav_html in border styling
container_nav = ctx.get("container_nav_html", "")
if container_nav:
- nav_parts.append(render("blog-container-nav",
+ overrides["container_nav_html"] = render("blog-container-nav",
container_nav_html=container_nav,
- ))
+ )
- # Admin link
- from quart import url_for as qurl, g, request
+ # Admin cog link
+ from quart import url_for as qurl, request
+ post = ctx.get("post") or {}
+ slug = post.get("slug", "")
rights = ctx.get("rights") or {}
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
- if has_admin:
+ if has_admin and slug:
select_colours = ctx.get("select_colours", "")
styles = ctx.get("styles") or {}
nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
admin_href = qurl("blog.post.admin.admin", slug=slug)
is_admin_page = "/admin" in request.path
- nav_parts.append(render("nav-link",
+ overrides["post_admin_nav_html"] = render("nav-link",
href=admin_href, hx_select="#main-panel", icon="fa fa-cog",
aclass=f"{nav_btn} {select_colours}",
select_colours=select_colours, is_selected=is_admin_page,
- ))
+ )
- nav_html = "".join(nav_parts)
- link_href = call_url(ctx, "blog_url", f"/{slug}/")
-
- return render("menu-row",
- id="post-row", level=1,
- link_href=link_href, link_label_html=label_html,
- nav_html=nav_html, child_id="post-header-child", oob=oob,
- )
+ effective_ctx = {**ctx, **overrides} if overrides else ctx
+ return _shared_post_header_html(effective_ctx, oob=oob)
# ---------------------------------------------------------------------------
diff --git a/events/sexp/header.sexpr b/events/sexp/header.sexpr
index 84d6990..69ba3dd 100644
--- a/events/sexp/header.sexpr
+++ b/events/sexp/header.sexpr
@@ -1,20 +1,5 @@
;; Events header components
-(defcomp ~events-oob-header (&key parent-id child-id row-html)
- (div :id parent-id :hx-swap-oob "outerHTML" :class "w-full"
- (div :class "w-full"
- (raw! row-html)
- (div :id child-id))))
-
-(defcomp ~events-post-label (&key feature-image title)
- (<> (when feature-image (img :src feature-image :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
- (span title)))
-
-(defcomp ~events-post-cart-link (&key href count)
- (a :href href :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition"
- (i :class "fa fa-shopping-cart" :aria-hidden "true")
- (span count)))
-
(defcomp ~events-calendars-label ()
(<> (i :class "fa fa-calendar" :aria-hidden "true") (div "Calendars")))
@@ -39,5 +24,3 @@
(div :id (str "entry-title-" entry-id) :class "flex gap-1 items-center"
(raw! title-html) (raw! times-html)))
-(defcomp ~events-header-child (&key inner-html)
- (div :id "root-header-child" :class "w-full" (raw! inner-html)))
diff --git a/events/sexp/sexp_components.py b/events/sexp/sexp_components.py
index d0039d5..093e9d2 100644
--- a/events/sexp/sexp_components.py
+++ b/events/sexp/sexp_components.py
@@ -14,6 +14,8 @@ from markupsafe import escape
from shared.sexp.jinja_bridge import render, load_service_components
from shared.sexp.helpers import (
call_url, get_asset_url, root_header_html,
+ post_header_html as _shared_post_header_html,
+ oob_header_html,
search_mobile_html, search_desktop_html,
full_page, oob_page,
)
@@ -23,46 +25,21 @@ load_service_components(os.path.dirname(os.path.dirname(__file__)))
# ---------------------------------------------------------------------------
-# OOB header helper (same pattern as market)
+# OOB header helper — delegates to shared
# ---------------------------------------------------------------------------
-def _oob_header_html(parent_id: str, child_id: str, row_html: str) -> str:
- """Wrap a header row in OOB div with child placeholder."""
- return render("events-oob-header",
- parent_id=parent_id, child_id=child_id, row_html=row_html)
+_oob_header_html = oob_header_html
# ---------------------------------------------------------------------------
-# Post header helpers (mirrors events/templates/_types/post/header/_header.html)
+# Post header helpers — thin wrapper over shared post_header_html
# ---------------------------------------------------------------------------
def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
- """Build the post-level header row."""
- post = ctx.get("post") or {}
- slug = post.get("slug", "")
- title = (post.get("title") or "")[:160]
- feature_image = post.get("feature_image")
-
- label_html = render("events-post-label",
- feature_image=feature_image, title=title)
-
- nav_parts = []
- 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(render("events-post-cart-link",
- href=cart_href, count=str(page_cart_count)))
-
- # Post nav: calendar links + admin
- nav_parts.append(_post_nav_html(ctx))
-
- nav_html = "".join(nav_parts)
- link_href = call_url(ctx, "blog_url", f"/{slug}/")
-
- return render("menu-row", id="post-row", level=1,
- link_href=link_href, link_label_html=label_html,
- nav_html=nav_html, child_id="post-header-child", oob=oob,
- external=True)
+ """Build the post-level header row (events-specific: calendar links + admin cog)."""
+ admin_nav = _post_nav_html(ctx)
+ effective_ctx = {**ctx, "post_admin_nav_html": admin_nav} if admin_nav else ctx
+ return _shared_post_header_html(effective_ctx, oob=oob)
def _post_nav_html(ctx: dict) -> str:
@@ -1235,7 +1212,7 @@ async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets
)
hdr = root_header_html(ctx)
- hdr += render("events-header-child",
+ hdr += render("header-child",
inner_html=_post_header_html(ctx))
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1289,7 +1266,7 @@ async def render_calendars_page(ctx: dict) -> str:
content = _calendars_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _calendars_header_html(ctx)
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1311,7 +1288,7 @@ async def render_calendar_page(ctx: dict) -> str:
content = _calendar_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _calendar_header_html(ctx)
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1334,7 +1311,7 @@ async def render_day_page(ctx: dict) -> str:
hdr = root_header_html(ctx)
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx))
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1358,7 +1335,7 @@ async def render_day_admin_page(ctx: dict) -> str:
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx)
+ _day_admin_header_html(ctx))
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1381,7 +1358,7 @@ async def render_calendar_admin_page(ctx: dict) -> str:
hdr = root_header_html(ctx)
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1407,7 +1384,7 @@ async def render_slots_page(ctx: dict) -> str:
hdr = root_header_html(ctx)
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1476,7 +1453,7 @@ async def render_markets_page(ctx: dict) -> str:
content = _markets_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _markets_header_html(ctx)
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1860,7 +1837,7 @@ async def render_entry_page(ctx: dict) -> str:
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx)
+ _entry_header_html(ctx))
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
nav_html = _entry_nav_html(ctx)
return full_page(ctx, header_rows_html=hdr, content_html=content, menu_html=nav_html)
@@ -2873,7 +2850,7 @@ async def render_entry_admin_page(ctx: dict) -> str:
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx)
+ _entry_header_html(ctx) + _entry_admin_header_html(ctx))
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
nav_html = render("events-admin-placeholder-nav")
return full_page(ctx, header_rows_html=hdr, content_html=content, menu_html=nav_html)
@@ -2932,7 +2909,7 @@ async def render_slot_page(ctx: dict) -> str:
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx)
+ _slot_header_html(ctx))
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -3140,7 +3117,7 @@ async def render_ticket_types_page(ctx: dict) -> str:
+ _calendar_header_html(ctx) + _day_header_html(ctx)
+ _entry_header_html(ctx) + _entry_admin_header_html(ctx)
+ _ticket_types_header_html(ctx))
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
nav_html = render("events-admin-placeholder-nav")
return full_page(ctx, header_rows_html=hdr, content_html=content, menu_html=nav_html)
@@ -3214,7 +3191,7 @@ async def render_ticket_type_page(ctx: dict) -> str:
+ _calendar_header_html(ctx) + _day_header_html(ctx)
+ _entry_header_html(ctx) + _entry_admin_header_html(ctx)
+ _ticket_types_header_html(ctx) + _ticket_type_header_html(ctx))
- hdr += render("events-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
nav_html = render("events-admin-placeholder-nav")
return full_page(ctx, header_rows_html=hdr, content_html=content, menu_html=nav_html)
diff --git a/market/sexp/headers.sexpr b/market/sexp/headers.sexpr
index 1bbfe7c..57e7633 100644
--- a/market/sexp/headers.sexpr
+++ b/market/sexp/headers.sexpr
@@ -1,16 +1,5 @@
;; Market header components
-(defcomp ~market-post-label-image (&key src)
- (img :src src :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))
-
-(defcomp ~market-post-label-title (&key title)
- (span title))
-
-(defcomp ~market-post-cart-badge (&key href count)
- (a :href href :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition"
- (i :class "fa fa-shopping-cart" :aria-hidden "true")
- (span count)))
-
(defcomp ~market-shop-label (&key title top-slug sub-div-html)
(div :class "font-bold text-xl flex-shrink-0 flex gap-2 items-center"
(div (i :class "fa fa-shop") " " title)
@@ -29,10 +18,3 @@
:class "px-2 py-1 text-stone-500 hover:text-stone-700"
(i :class "fa fa-cog" :aria-hidden "true")))
-(defcomp ~market-oob-header (&key parent-id child-id row-html)
- (div :id parent-id :hx-swap-oob "outerHTML" :class "w-full"
- (div :class "w-full" (raw! row-html)
- (div :id child-id))))
-
-(defcomp ~market-header-child (&key inner-html)
- (div :id "root-header-child" :class "w-full" (raw! inner-html)))
diff --git a/market/sexp/sexp_components.py b/market/sexp/sexp_components.py
index fcf6616..3131aa7 100644
--- a/market/sexp/sexp_components.py
+++ b/market/sexp/sexp_components.py
@@ -14,6 +14,8 @@ from markupsafe import escape
from shared.sexp.jinja_bridge import render, load_service_components
from shared.sexp.helpers import (
call_url, get_asset_url, root_header_html,
+ post_header_html as _post_header_html,
+ oob_header_html as _oob_header_html,
search_mobile_html, search_desktop_html,
full_page, oob_page,
)
@@ -66,42 +68,9 @@ def _card_price_html(p: dict) -> str:
# ---------------------------------------------------------------------------
-# Header helpers
+# Header helpers — _post_header_html and _oob_header_html imported from shared
# ---------------------------------------------------------------------------
-def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
- """Build the post-level header row (feature image + title + page cart count)."""
- post = ctx.get("post") or {}
- slug = post.get("slug", "")
- title = (post.get("title") or "")[:160]
- feature_image = post.get("feature_image")
-
- label_html = ""
- if feature_image:
- label_html += render("market-post-label-image", src=feature_image)
- label_html += render("market-post-label-title", title=title)
-
- nav_html = ""
- 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_html += render("market-post-cart-badge", href=cart_href, count=str(page_cart_count))
-
- # Container nav
- container_nav = ctx.get("container_nav_html", "")
- if container_nav:
- nav_html += container_nav
-
- link_href = call_url(ctx, "blog_url", f"/{slug}/")
-
- return render(
- "menu-row",
- id="post-row", level=1,
- link_href=link_href, link_label_html=label_html,
- nav_html=nav_html, child_id="post-header-child", oob=oob,
- external=True,
- )
-
def _market_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build the market-level header row (shop icon + market title + category slugs + nav)."""
@@ -1153,16 +1122,9 @@ def _market_cards_html(markets: list, page_info: dict, page: int, has_more: bool
# ---------------------------------------------------------------------------
-# OOB header helpers
+# OOB header helpers — _oob_header_html imported from shared
# ---------------------------------------------------------------------------
-def _oob_header_html(parent_id: str, child_id: str, row_html: str) -> str:
- """Wrap a header row in OOB div with child placeholder."""
- return render(
- "market-oob-header",
- parent_id=parent_id, child_id=child_id, row_html=row_html,
- )
-
# ===========================================================================
# PUBLIC API
@@ -1258,7 +1220,7 @@ async def render_page_markets_page(ctx: dict, markets: list, has_more: bool,
content += render("market-bottom-spacer")
hdr = root_header_html(ctx)
- hdr += render("market-header-child", inner_html=_post_header_html(ctx))
+ hdr += render("header-child", inner_html=_post_header_html(ctx))
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1309,7 +1271,7 @@ async def render_market_home_page(ctx: dict) -> str:
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _market_header_html(ctx)
- hdr += render("market-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
menu = _mobile_nav_panel_html(ctx)
return full_page(ctx, header_rows_html=hdr, content_html=content, menu_html=menu)
@@ -1354,7 +1316,7 @@ async def render_browse_page(ctx: dict) -> str:
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _market_header_html(ctx)
- hdr += render("market-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
menu = _mobile_nav_panel_html(ctx)
filter_html = _mobile_filter_summary_html(ctx)
aside_html = _desktop_filter_html(ctx)
@@ -1395,7 +1357,7 @@ async def render_product_page(ctx: dict, d: dict) -> str:
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _market_header_html(ctx) + _product_header_html(ctx, d)
- hdr += render("market-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content, meta_html=meta)
@@ -1421,7 +1383,7 @@ async def render_product_admin_page(ctx: dict, d: dict) -> str:
hdr = root_header_html(ctx)
child = (_post_header_html(ctx) + _market_header_html(ctx)
+ _product_header_html(ctx, d) + _product_admin_header_html(ctx, d))
- hdr += render("market-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1459,7 +1421,7 @@ async def render_market_admin_page(ctx: dict) -> str:
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _market_header_html(ctx) + _market_admin_header_html(ctx)
- hdr += render("market-header-child", inner_html=child)
+ hdr += render("header-child", inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
diff --git a/shared/browser/app/errors.py b/shared/browser/app/errors.py
index 7cb2c3a..4a0d751 100644
--- a/shared/browser/app/errors.py
+++ b/shared/browser/app/errors.py
@@ -83,8 +83,7 @@ async def _rich_error_page(errnum: str, message: str, image: str | None = None)
return None
try:
- from shared.sexp.jinja_bridge import render
- from shared.sexp.helpers import full_page, call_url
+ from shared.sexp.helpers import full_page
# Build a minimal context — avoid get_template_context() which
# calls cross-service fragment fetches that may fail.
@@ -150,44 +149,22 @@ async def _rich_error_page(errnum: str, message: str, image: str | None = None)
pass
# Root header (site nav bar)
- from shared.sexp.helpers import root_header_html
+ from shared.sexp.helpers import (
+ root_header_html, post_header_html,
+ header_child_html, error_content_html,
+ )
hdr = root_header_html(ctx)
# Post breadcrumb if we resolved a post
post = (post_data or {}).get("post") or ctx.get("post") or {}
if post.get("slug"):
- title = (post.get("title") or "")[:160]
- feature_image = post.get("feature_image")
- label_html = ""
- if feature_image:
- label_html += (
- f''
- )
- label_html += f"{escape(title)}"
- post_row = render(
- "menu-row",
- id="post-row", level=1,
- link_href=call_url(ctx, "blog_url", f"/{post['slug']}/"),
- link_label_html=label_html,
- child_id="post-header-child",
- external=True,
- )
- hdr += (
- f'