""" Blog service s-expression page components. Renders home, blog index (posts/pages), new post/page, post detail, post admin, post data, post entries, post edit, post settings, settings home, cache, snippets, menu items, and tag groups pages. Called from route handlers in place of ``render_template()``. """ from __future__ import annotations from typing import Any from markupsafe import escape from shared.sexp.jinja_bridge import sexp from shared.sexp.helpers import ( call_url, get_asset_url, root_header_html, search_mobile_html, search_desktop_html, full_page, oob_page, ) # --------------------------------------------------------------------------- # OOB header helper # --------------------------------------------------------------------------- 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 ( f'
' f'
{row_html}' f'
' ) # --------------------------------------------------------------------------- # Blog header (root-header-child -> blog-header-child) # --------------------------------------------------------------------------- def _blog_header_html(ctx: dict, *, oob: bool = False) -> str: """Blog header row — empty child of root.""" return sexp( '(~menu-row :id "blog-row" :level 1' ' :link-label-html llh' ' :child-id "blog-header-child" :oob oob)', llh="
", oob=oob, ) # --------------------------------------------------------------------------- # Post header helpers # --------------------------------------------------------------------------- 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_parts = [] if feature_image: label_parts.append( f'' ) label_parts.append(f"{escape(title)}") label_html = "".join(label_parts) 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( f'' f'' f'{page_cart_count}' ) # Container nav fragments (calendars, markets) container_nav = ctx.get("container_nav_html", "") if container_nav: nav_parts.append( f'
{container_nav}
' ) # Admin link from quart import url_for as qurl, g rights = ctx.get("rights") or {} has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False) if has_admin: hx_select = ctx.get("hx_select_search", "#main-panel") 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) nav_parts.append( f'' ) nav_html = "".join(nav_parts) link_href = call_url(ctx, "blog_url", f"/{slug}/") return sexp( '(~menu-row :id "post-row" :level 1' ' :link-href lh :link-label-html llh' ' :nav-html nh :child-id "post-header-child" :oob oob)', lh=link_href, llh=label_html, nh=nav_html, oob=oob, ) # --------------------------------------------------------------------------- # Post admin header # --------------------------------------------------------------------------- def _post_admin_header_html(ctx: dict, *, oob: bool = False) -> str: """Post admin header row with admin icon and nav links.""" from quart import url_for as qurl post = ctx.get("post") or {} slug = post.get("slug", "") hx_select = ctx.get("hx_select_search", "#main-panel") 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) label_html = ' admin' nav_html = _post_admin_nav_html(ctx) return sexp( '(~menu-row :id "post-admin-row" :level 2' ' :link-href lh :link-label-html llh' ' :nav-html nh :child-id "post-admin-header-child" :oob oob)', lh=admin_href, llh=label_html, nh=nav_html, oob=oob, ) def _post_admin_nav_html(ctx: dict) -> str: """Post admin desktop nav: calendars, markets, payments, entries, data, edit, settings.""" from quart import url_for as qurl post = ctx.get("post") or {} slug = post.get("slug", "") hx_select = ctx.get("hx_select_search", "#main-panel") 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", "") parts = [] # External links to events service events_url_fn = ctx.get("events_url") if callable(events_url_fn): for path, label in [ (f"/{slug}/calendars/", "calendars"), (f"/{slug}/markets/", "markets"), (f"/{slug}/payments/", "payments"), ]: href = events_url_fn(path) parts.append( f'' ) # HTMX links for endpoint, label in [ ("blog.post.admin.entries", "entries"), ("blog.post.admin.data", "data"), ("blog.post.admin.edit", "edit"), ("blog.post.admin.settings", "settings"), ]: href = qurl(endpoint, slug=slug) parts.append(sexp( '(~nav-link :href h :label l :select-colours sc)', h=href, l=label, sc=select_colours, )) return "".join(parts) # --------------------------------------------------------------------------- # Settings header (root-header-child -> root-settings-header-child) # --------------------------------------------------------------------------- def _settings_header_html(ctx: dict, *, oob: bool = False) -> str: """Settings header row with admin icon and nav links.""" from quart import url_for as qurl hx_select = ctx.get("hx_select_search", "#main-panel") settings_href = qurl("settings.home") label_html = ' admin' nav_html = _settings_nav_html(ctx) return sexp( '(~menu-row :id "root-settings-row" :level 1' ' :link-href lh :link-label-html llh' ' :nav-html nh :child-id "root-settings-header-child" :oob oob)', lh=settings_href, llh=label_html, nh=nav_html, oob=oob, ) def _settings_nav_html(ctx: dict) -> str: """Settings desktop nav: menu items, snippets, tag groups, cache.""" from quart import url_for as qurl select_colours = ctx.get("select_colours", "") parts = [] for endpoint, icon, label in [ ("menu_items.list_menu_items", "bars", "Menu Items"), ("snippets.list_snippets", "puzzle-piece", "Snippets"), ("blog.tag_groups_admin.index", "tags", "Tag Groups"), ("settings.cache", "refresh", "Cache"), ]: href = qurl(endpoint) parts.append(sexp( '(~nav-link :href h :icon ic :label l :select-colours sc)', h=href, ic=f"fa fa-{icon}", l=label, sc=select_colours, )) return "".join(parts) # --------------------------------------------------------------------------- # Sub-settings headers (root-settings-header-child -> X-header-child) # --------------------------------------------------------------------------- def _sub_settings_header_html(row_id: str, child_id: str, href: str, icon: str, label: str, ctx: dict, *, oob: bool = False, nav_html: str = "") -> str: """Generic sub-settings header row (menu_items, snippets, tag_groups, cache).""" select_colours = ctx.get("select_colours", "") label_html = f' {escape(label)}' return sexp( '(~menu-row :id rid :level 2' ' :link-href lh :link-label-html llh' ' :nav-html nh :child-id cid :oob oob)', rid=row_id, lh=href, llh=label_html, nh=nav_html, cid=child_id, oob=oob, ) def _post_sub_admin_header_html(row_id: str, child_id: str, href: str, icon: str, label: str, ctx: dict, *, oob: bool = False, nav_html: str = "") -> str: """Generic post sub-admin header row (data, edit, entries, settings).""" label_html = f'
{escape(label)}
' return sexp( '(~menu-row :id rid :level 3' ' :link-href lh :link-label-html llh' ' :nav-html nh :child-id cid :oob oob)', rid=row_id, lh=href, llh=label_html, nh=nav_html, cid=child_id, oob=oob, ) # --------------------------------------------------------------------------- # Blog index main panel helpers # --------------------------------------------------------------------------- def _blog_cards_html(ctx: dict) -> str: """Render blog post cards (list or tile).""" posts = ctx.get("posts") or [] view = ctx.get("view") parts = [] for p in posts: if view == "tile": parts.append(_blog_card_tile_html(p, ctx)) else: parts.append(_blog_card_html(p, ctx)) parts.append(_blog_sentinel_html(ctx)) return "".join(parts) def _blog_card_html(post: dict, ctx: dict) -> str: """Single blog post card (list view).""" from quart import url_for as qurl, g slug = post.get("slug", "") href = call_url(ctx, "blog_url", f"/{slug}/") hx_select = ctx.get("hx_select_search", "#main-panel") user = getattr(g, "user", None) parts = ['
'] # Like button if user: liked = post.get("is_liked", False) like_url = call_url(ctx, "blog_url", f"/{slug}/like/toggle/") parts.append( f'
' f'
' ) parts.append( f'' ) # Header parts.append(f'

{escape(post.get("title", ""))}

') status = post.get("status", "published") if status == "draft": parts.append('
') parts.append('Draft') if post.get("publish_requested"): parts.append('Publish requested') parts.append('
') updated = post.get("updated_at") if updated: ts = updated.strftime("%-d %b %Y at %H:%M") if hasattr(updated, "strftime") else str(updated) parts.append(f'

Updated: {ts}

') else: pub = post.get("published_at") if pub: ts = pub.strftime("%-d %b %Y at %H:%M") if hasattr(pub, "strftime") else str(pub) parts.append(f'

Published: {ts}

') parts.append('
') # Feature image fi = post.get("feature_image") if fi: parts.append(f'
') # Excerpt excerpt = post.get("custom_excerpt") or post.get("excerpt", "") if excerpt: parts.append(f'

{escape(excerpt)}

') parts.append('
') # Card widgets (fragments) card_widgets = ctx.get("card_widgets_html") or {} widget = card_widgets.get(str(post.get("id", "")), "") if widget: parts.append(widget) # Tags + authors bar parts.append(_at_bar_html(post, ctx)) parts.append('
') return "".join(parts) def _blog_card_tile_html(post: dict, ctx: dict) -> str: """Single blog post card (tile view).""" slug = post.get("slug", "") href = call_url(ctx, "blog_url", f"/{slug}/") hx_select = ctx.get("hx_select_search", "#main-panel") parts = ['
'] parts.append( f'' ) fi = post.get("feature_image") if fi: parts.append(f'
') parts.append('
') parts.append(f'

{escape(post.get("title", ""))}

') status = post.get("status", "published") if status == "draft": parts.append('
') parts.append('Draft') if post.get("publish_requested"): parts.append('Publish requested') parts.append('
') updated = post.get("updated_at") if updated: ts = updated.strftime("%-d %b %Y at %H:%M") if hasattr(updated, "strftime") else str(updated) parts.append(f'

Updated: {ts}

') else: pub = post.get("published_at") if pub: ts = pub.strftime("%-d %b %Y at %H:%M") if hasattr(pub, "strftime") else str(pub) parts.append(f'

Published: {ts}

') excerpt = post.get("custom_excerpt") or post.get("excerpt", "") if excerpt: parts.append(f'

{escape(excerpt)}

') parts.append('
') parts.append(_at_bar_html(post, ctx)) parts.append('
') return "".join(parts) def _at_bar_html(post: dict, ctx: dict) -> str: """Tags + authors bar below a card.""" tags = post.get("tags") or [] authors = post.get("authors") or [] if not tags and not authors: return "" all_tags = ctx.get("tags") or [] tag_slugs = {t.get("slug") or getattr(t, "slug", "") for t in all_tags} if all_tags else set() parts = ['
'] if tags: parts.append('
in
') parts.append('
') if authors: parts.append('
by
') parts.append('
') return "".join(parts) def _blog_sentinel_html(ctx: dict) -> str: """Infinite scroll sentinels for blog post list.""" page = ctx.get("page", 1) total_pages = ctx.get("total_pages", 1) if isinstance(total_pages, str): total_pages = int(total_pages) if page >= total_pages: return '
End of results
' current_local_href = ctx.get("current_local_href", "/index") qs_fn = ctx.get("qs") # Build next page URL next_url = f"{current_local_href}?page={page + 1}" parts = [] # Mobile sentinel parts.append( f'' ) # Desktop sentinel parts.append( f'' ) return "".join(parts) def _page_cards_html(ctx: dict) -> str: """Render page cards with sentinel.""" pages = ctx.get("pages") or ctx.get("posts") or [] page_num = ctx.get("page", 1) total_pages = ctx.get("total_pages", 1) if isinstance(total_pages, str): total_pages = int(total_pages) hx_select = ctx.get("hx_select_search", "#main-panel") parts = [] for pg in pages: parts.append(_page_card_html(pg, ctx)) if page_num < total_pages: current_local_href = ctx.get("current_local_href", "/index?type=pages") next_url = f"{current_local_href}&page={page_num + 1}" if "?" in current_local_href else f"{current_local_href}?page={page_num + 1}" parts.append( f'
' ) elif pages: parts.append('
End of results
') else: parts.append('
No pages found.
') return "".join(parts) def _page_card_html(page: dict, ctx: dict) -> str: """Single page card.""" slug = page.get("slug", "") href = call_url(ctx, "blog_url", f"/{slug}/") hx_select = ctx.get("hx_select_search", "#main-panel") parts = ['
'] parts.append( f'' ) parts.append(f'

{escape(page.get("title", ""))}

') # Feature badges features = page.get("features") or {} if features: parts.append('
') if features.get("calendar"): parts.append('Calendar') if features.get("market"): parts.append('Market') parts.append('
') pub = page.get("published_at") if pub: ts = pub.strftime("%-d %b %Y at %H:%M") if hasattr(pub, "strftime") else str(pub) parts.append(f'

Published: {ts}

') parts.append('
') fi = page.get("feature_image") if fi: parts.append(f'
') excerpt = page.get("custom_excerpt") or page.get("excerpt", "") if excerpt: parts.append(f'

{escape(excerpt)}

') parts.append('
') return "".join(parts) def _view_toggle_html(ctx: dict) -> str: """View toggle bar (list/tile) for desktop.""" view = ctx.get("view") current_local_href = ctx.get("current_local_href", "/index") hx_select = ctx.get("hx_select_search", "#main-panel") list_cls = "bg-stone-200 text-stone-800" if view != "tile" else "text-stone-400 hover:text-stone-600" tile_cls = "bg-stone-200 text-stone-800" if view == "tile" else "text-stone-400 hover:text-stone-600" list_href = f"{current_local_href}" tile_href = f"{current_local_href}{'&' if '?' in current_local_href else '?'}view=tile" list_svg = '' tile_svg = '' return ( f'' ) def _content_type_tabs_html(ctx: dict) -> str: """Posts/Pages tabs.""" from quart import url_for as qurl content_type = ctx.get("content_type", "posts") hx_select = ctx.get("hx_select_search", "#main-panel") posts_href = call_url(ctx, "blog_url", "/index") pages_href = f"{posts_href}?type=pages" posts_cls = "bg-stone-700 text-white" if content_type != "pages" else "bg-stone-100 text-stone-600 hover:bg-stone-200" pages_cls = "bg-stone-700 text-white" if content_type == "pages" else "bg-stone-100 text-stone-600 hover:bg-stone-200" return ( f'
' f'Posts' f'Pages' f'
' ) def _blog_main_panel_html(ctx: dict) -> str: """Blog index main panel with tabs, toggle, and cards.""" content_type = ctx.get("content_type", "posts") view = ctx.get("view") parts = [_content_type_tabs_html(ctx)] if content_type == "pages": parts.append('
') parts.append(_page_cards_html(ctx)) parts.append('
') else: parts.append(_view_toggle_html(ctx)) if view == "tile": parts.append('
') else: parts.append('
') parts.append(_blog_cards_html(ctx)) parts.append('
') return "".join(parts) # --------------------------------------------------------------------------- # Desktop aside (filter sidebar) # --------------------------------------------------------------------------- def _blog_aside_html(ctx: dict) -> str: """Desktop aside with search, action buttons, and filters.""" parts = [] parts.append(search_desktop_html(ctx)) parts.append(_action_buttons_html(ctx)) parts.append(f'
') parts.append(_tag_groups_filter_html(ctx)) parts.append(_authors_filter_html(ctx)) parts.append('
') parts.append('
') return "".join(parts) def _blog_filter_html(ctx: dict) -> str: """Mobile filter (details/summary).""" current_local_href = ctx.get("current_local_href", "/index") search = ctx.get("search", "") search_count = ctx.get("search_count", "") hx_select = ctx.get("hx_select", "#main-panel") # Mobile filter summary tags summary_parts = [] summary_parts.append(_tag_groups_filter_summary_html(ctx)) summary_parts.append(_authors_filter_summary_html(ctx)) summary_html = "".join(summary_parts) filter_content = search_mobile_html(ctx) + summary_html action_buttons = _action_buttons_html(ctx) filter_details = _tag_groups_filter_html(ctx) + _authors_filter_html(ctx) return sexp( '(~mobile-filter :filter-summary-html fsh :action-buttons-html abh' ' :filter-details-html fdh)', fsh=filter_content, abh=action_buttons, fdh=filter_details, ) def _action_buttons_html(ctx: dict) -> str: """New Post/Page + Drafts toggle buttons.""" from quart import g rights = ctx.get("rights") or {} has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False) user = getattr(g, "user", None) hx_select = ctx.get("hx_select_search", "#main-panel") drafts = ctx.get("drafts") draft_count = ctx.get("draft_count", 0) current_local_href = ctx.get("current_local_href", "/index") parts = ['
'] if has_admin: new_href = call_url(ctx, "blog_url", "/new/") parts.append( f' New Post' ) new_page_href = call_url(ctx, "blog_url", "/new-page/") parts.append( f' New Page' ) if user and (draft_count or drafts): if drafts: off_href = f"{current_local_href}" parts.append( f' Drafts' f' {draft_count}' ) else: on_href = f"{current_local_href}{'&' if '?' in current_local_href else '?'}drafts=1" parts.append( f' Drafts' f' {draft_count}' ) parts.append('
') return "".join(parts) def _tag_groups_filter_html(ctx: dict) -> str: """Tag group filter bar for desktop/mobile.""" tag_groups = ctx.get("tag_groups") or [] selected_groups = ctx.get("selected_groups") or () selected_tags = ctx.get("selected_tags") or () hx_select = ctx.get("hx_select_search", "#main-panel") parts = ['') return "".join(parts) def _authors_filter_html(ctx: dict) -> str: """Author filter bar for desktop/mobile.""" authors = ctx.get("authors") or [] selected_authors = ctx.get("selected_authors") or () hx_select = ctx.get("hx_select_search", "#main-panel") parts = ['') return "".join(parts) def _tag_groups_filter_summary_html(ctx: dict) -> str: """Mobile filter summary for tag groups.""" selected_groups = ctx.get("selected_groups") or () tag_groups = ctx.get("tag_groups") or [] if not selected_groups: return "" names = [] for g in tag_groups: g_slug = getattr(g, "slug", "") if hasattr(g, "slug") else g.get("slug", "") g_name = getattr(g, "name", "") if hasattr(g, "name") else g.get("name", "") if g_slug in selected_groups: names.append(g_name) if not names: return "" return f'{escape(", ".join(names))}' def _authors_filter_summary_html(ctx: dict) -> str: """Mobile filter summary for authors.""" selected_authors = ctx.get("selected_authors") or () authors = ctx.get("authors") or [] if not selected_authors: return "" names = [] for a in authors: a_slug = getattr(a, "slug", "") if hasattr(a, "slug") else a.get("slug", "") a_name = getattr(a, "name", "") if hasattr(a, "name") else a.get("name", "") if a_slug in selected_authors: names.append(a_name) if not names: return "" return f'{escape(", ".join(names))}' # --------------------------------------------------------------------------- # Post detail main panel # --------------------------------------------------------------------------- def _post_main_panel_html(ctx: dict) -> str: """Post/page article content.""" from quart import g, url_for as qurl post = ctx.get("post") or {} slug = post.get("slug", "") user = getattr(g, "user", None) rights = ctx.get("rights") or {} is_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False) hx_select = ctx.get("hx_select_search", "#main-panel") parts = ['
'] # Draft indicator if post.get("status") == "draft": parts.append('
') parts.append('Draft') if post.get("publish_requested"): parts.append('Publish requested') if is_admin or (user and post.get("user_id") == getattr(user, "id", None)): edit_href = qurl("blog.post.admin.edit", slug=slug) parts.append( f'' f' Edit' ) parts.append('
') # Blog post chrome (not for pages) if not post.get("is_page"): if user: liked = post.get("is_liked", False) like_url = call_url(ctx, "blog_url", f"/{slug}/like/toggle/") parts.append( f'
' f'
' ) if post.get("custom_excerpt"): parts.append(f'
{post["custom_excerpt"]}
') # Desktop at_bar parts.append(f'') # Feature image fi = post.get("feature_image") if fi: parts.append(f'
') # Post HTML content html_content = post.get("html", "") if html_content: parts.append(f'
{html_content}
') parts.append('
') return "".join(parts) def _post_meta_html(ctx: dict) -> str: """Post SEO meta tags (Open Graph, Twitter, JSON-LD).""" post = ctx.get("post") or {} base_title = ctx.get("base_title", "") is_public = post.get("visibility") == "public" is_published = post.get("status") == "published" email_only = post.get("email_only", False) robots = "index,follow" if (is_public and is_published and not email_only) else "noindex,nofollow" # Description desc = (post.get("meta_description") or post.get("og_description") or post.get("twitter_description") or post.get("custom_excerpt") or post.get("excerpt") or "") if not desc and post.get("html"): import re desc = re.sub(r'<[^>]+>', '', post["html"]) desc = desc.replace("\n", " ").replace("\r", " ").strip()[:160] # Image image = (post.get("og_image") or post.get("twitter_image") or post.get("feature_image") or "") # Canonical from quart import request as req canonical = post.get("canonical_url") or (req.url if req else "") og_title = post.get("og_title") or base_title tw_title = post.get("twitter_title") or base_title is_article = not post.get("is_page") parts = [f''] parts.append(f'{escape(base_title)}') parts.append(f'') if canonical: parts.append(f'') parts.append(f'') parts.append(f'') parts.append(f'') if canonical: parts.append(f'') if image: parts.append(f'') parts.append(f'') parts.append(f'') parts.append(f'') if image: parts.append(f'') return "".join(parts) # --------------------------------------------------------------------------- # Home page (Ghost "home" page) # --------------------------------------------------------------------------- def _home_main_panel_html(ctx: dict) -> str: """Home page content — renders the Ghost page HTML.""" post = ctx.get("post") or {} html = post.get("html", "") return f'
{html}
' # --------------------------------------------------------------------------- # Post admin - empty main panel # --------------------------------------------------------------------------- def _post_admin_main_panel_html(ctx: dict) -> str: return '
' # --------------------------------------------------------------------------- # Settings main panels # --------------------------------------------------------------------------- def _settings_main_panel_html(ctx: dict) -> str: return '
' def _cache_main_panel_html(ctx: dict) -> str: from quart import url_for as qurl csrf = ctx.get("csrf_token", "") clear_url = qurl("settings.cache_clear") return ( f'
' f'
' f'
' f'' f'' f'
' f'
' f'
' ) # --------------------------------------------------------------------------- # Snippets main panel # --------------------------------------------------------------------------- def _snippets_main_panel_html(ctx: dict) -> str: return ( f'
' f'
' f'

Snippets

' f'
{_snippets_list_html(ctx)}
' ) def _snippets_list_html(ctx: dict) -> str: """Snippets list with visibility badges and delete buttons.""" from quart import url_for as qurl, g snippets = ctx.get("snippets") or [] is_admin = ctx.get("is_admin", False) csrf = ctx.get("csrf_token", "") user = getattr(g, "user", None) user_id = getattr(user, "id", None) if not snippets: return ( '
' '
' '' '

No snippets yet. Create one from the blog editor.

' ) badge_colours = { "private": "bg-stone-200 text-stone-700", "shared": "bg-blue-100 text-blue-700", "admin": "bg-amber-100 text-amber-700", } parts = ['
'] for s in snippets: s_id = getattr(s, "id", None) or s.get("id") s_name = getattr(s, "name", "") if hasattr(s, "name") else s.get("name", "") s_uid = getattr(s, "user_id", None) if hasattr(s, "user_id") else s.get("user_id") s_vis = getattr(s, "visibility", "private") if hasattr(s, "visibility") else s.get("visibility", "private") owner = "You" if s_uid == user_id else f"User #{s_uid}" badge_cls = badge_colours.get(s_vis, "bg-stone-200 text-stone-700") parts.append( f'
' f'
{escape(s_name)}
' f'
{owner}
' f'{s_vis}' ) if is_admin: patch_url = qurl("snippets.patch_visibility", snippet_id=s_id) parts.append( f'') if s_uid == user_id or is_admin: del_url = qurl("snippets.delete_snippet", snippet_id=s_id) parts.append( f'' ) parts.append('
') parts.append('
') return "".join(parts) # --------------------------------------------------------------------------- # Menu items main panel # --------------------------------------------------------------------------- def _menu_items_main_panel_html(ctx: dict) -> str: from quart import url_for as qurl new_url = qurl("menu_items.new_menu_item") return ( f'
' f'
' f'
' f'' f'
' ) def _menu_items_list_html(ctx: dict) -> str: from quart import url_for as qurl menu_items = ctx.get("menu_items") or [] csrf = ctx.get("csrf_token", "") if not menu_items: return ( '
' '
' '' '

No menu items yet. Add one to get started!

' ) parts = ['
'] for item in menu_items: i_id = getattr(item, "id", None) or item.get("id") label = getattr(item, "label", "") if hasattr(item, "label") else item.get("label", "") slug = getattr(item, "slug", "") if hasattr(item, "slug") else item.get("slug", "") fi = getattr(item, "feature_image", None) if hasattr(item, "feature_image") else item.get("feature_image") sort = getattr(item, "sort_order", 0) if hasattr(item, "sort_order") else item.get("sort_order", 0) edit_url = qurl("menu_items.edit_menu_item", item_id=i_id) del_url = qurl("menu_items.delete_menu_item_route", item_id=i_id) img = (f'{escape(label)}' if fi else '
') parts.append( f'
' f'
' f'{img}' f'
{escape(label)}
' f'
{escape(slug)}
' f'
Order: {sort}
' f'
' f'' f'
' ) parts.append('
') return "".join(parts) # --------------------------------------------------------------------------- # Tag groups main panel # --------------------------------------------------------------------------- def _tag_groups_main_panel_html(ctx: dict) -> str: from quart import url_for as qurl groups = ctx.get("groups") or [] unassigned_tags = ctx.get("unassigned_tags") or [] csrf = ctx.get("csrf_token", "") parts = ['
'] # Create form create_url = qurl("blog.tag_groups_admin.create") parts.append( f'
' f'' f'

New Group

' f'
' f'' f'' f'' f'
' f'' f'' f'
' ) # Groups list if groups: parts.append('') else: parts.append('

No tag groups yet.

') # Unassigned tags if unassigned_tags: parts.append(f'

Unassigned Tags ({len(unassigned_tags)})

') parts.append('
') for tag in unassigned_tags: t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "") parts.append( f'' f'{escape(t_name)}' ) parts.append('
') parts.append('
') return "".join(parts) def _tag_groups_edit_main_panel_html(ctx: dict) -> str: from quart import url_for as qurl group = ctx.get("group") all_tags = ctx.get("all_tags") or [] assigned_tag_ids = ctx.get("assigned_tag_ids") or set() csrf = ctx.get("csrf_token", "") g_id = getattr(group, "id", None) or group.get("id") if group else None g_name = getattr(group, "name", "") if hasattr(group, "name") else (group.get("name", "") if group else "") g_colour = getattr(group, "colour", "") if hasattr(group, "colour") else (group.get("colour", "") if group else "") g_sort = getattr(group, "sort_order", 0) if hasattr(group, "sort_order") else (group.get("sort_order", 0) if group else 0) g_fi = getattr(group, "feature_image", "") if hasattr(group, "feature_image") else (group.get("feature_image", "") if group else "") save_url = qurl("blog.tag_groups_admin.save", id=g_id) del_url = qurl("blog.tag_groups_admin.delete_group", id=g_id) parts = [f'
'] # Edit form parts.append( f'
' f'' f'
' f'
' f'
' f'
' f'
' f'
' f'
' f'
' f'
' f'
' ) # Tag checkboxes parts.append( '
' '
' ) for tag in all_tags: t_id = getattr(tag, "id", None) or tag.get("id") t_name = getattr(tag, "name", "") if hasattr(tag, "name") else tag.get("name", "") t_fi = getattr(tag, "feature_image", None) if hasattr(tag, "feature_image") else tag.get("feature_image") checked = " checked" if t_id in assigned_tag_ids else "" img = f'' if t_fi else "" parts.append( f'' ) parts.append('
') parts.append( '
' '
' ) # Delete form parts.append( f'
' f'' f'
' ) parts.append('
') return "".join(parts) # --------------------------------------------------------------------------- # New post/page main panel — left as render_template (uses Koenig editor JS) # Post edit main panel — left as render_template (uses Koenig editor JS) # Post settings main panel — left as render_template (complex form macros) # Post entries main panel — left as render_template (calendar browser lazy-loads) # Post data main panel — left as render_template (uses ORM introspection macros) # --------------------------------------------------------------------------- # =========================================================================== # PUBLIC API — called from route handlers # =========================================================================== # ---- Home page ---- async def render_home_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) header_rows = root_hdr + post_hdr content = _home_main_panel_html(ctx) meta = _post_meta_html(ctx) menu_html = ctx.get("nav_html", "") or "" return full_page(ctx, header_rows_html=header_rows, content_html=content, meta_html=meta, menu_html=menu_html) async def render_home_oob(ctx: dict) -> str: root_hdr = root_header_html(ctx, oob=True) post_oob = _oob_header_html("root-header-child", "post-header-child", _post_header_html(ctx)) content = _home_main_panel_html(ctx) return oob_page(ctx, oobs_html=root_hdr + post_oob, content_html=content) # ---- Blog index ---- async def render_blog_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) blog_hdr = _blog_header_html(ctx) header_rows = root_hdr + blog_hdr content = _blog_main_panel_html(ctx) aside = _blog_aside_html(ctx) filter_html = _blog_filter_html(ctx) return full_page(ctx, header_rows_html=header_rows, content_html=content, aside_html=aside, filter_html=filter_html) async def render_blog_oob(ctx: dict) -> str: root_hdr = root_header_html(ctx, oob=True) blog_oob = _oob_header_html("root-header-child", "blog-header-child", _blog_header_html(ctx)) content = _blog_main_panel_html(ctx) aside = _blog_aside_html(ctx) filter_html = _blog_filter_html(ctx) nav_html = ctx.get("nav_html", "") or "" return oob_page(ctx, oobs_html=root_hdr + blog_oob, content_html=content, aside_html=aside, filter_html=filter_html, menu_html=nav_html) async def render_blog_cards(ctx: dict) -> str: """Pagination-only response (page > 1).""" return _blog_cards_html(ctx) async def render_blog_page_cards(ctx: dict) -> str: """Page cards pagination response.""" return _page_cards_html(ctx) # ---- New post/page ---- async def render_new_post_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) blog_hdr = _blog_header_html(ctx) header_rows = root_hdr + blog_hdr # Content comes from Jinja (editor template) content = ctx.get("editor_html", "") return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_new_post_oob(ctx: dict) -> str: root_hdr = root_header_html(ctx, oob=True) blog_oob = _blog_header_html(ctx, oob=True) content = ctx.get("editor_html", "") return oob_page(ctx, oobs_html=root_hdr + blog_oob, content_html=content) # ---- Post detail ---- async def render_post_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) header_rows = root_hdr + post_hdr content = _post_main_panel_html(ctx) meta = _post_meta_html(ctx) menu_html = ctx.get("nav_html", "") or "" return full_page(ctx, header_rows_html=header_rows, content_html=content, meta_html=meta, menu_html=menu_html) async def render_post_oob(ctx: dict) -> str: root_hdr = root_header_html(ctx, oob=True) post_oob = _oob_header_html("root-header-child", "post-header-child", _post_header_html(ctx)) content = _post_main_panel_html(ctx) menu_html = ctx.get("nav_html", "") or "" return oob_page(ctx, oobs_html=root_hdr + post_oob, content_html=content, menu_html=menu_html) # ---- Post admin ---- async def render_post_admin_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) admin_hdr = _post_admin_header_html(ctx) header_rows = root_hdr + post_hdr + admin_hdr content = _post_admin_main_panel_html(ctx) menu_html = ctx.get("nav_html", "") or "" return full_page(ctx, header_rows_html=header_rows, content_html=content, menu_html=menu_html) async def render_post_admin_oob(ctx: dict) -> str: post_hdr_oob = _post_header_html(ctx, oob=True) admin_oob = _oob_header_html("post-header-child", "post-admin-header-child", _post_admin_header_html(ctx)) content = _post_admin_main_panel_html(ctx) menu_html = ctx.get("nav_html", "") or "" return oob_page(ctx, oobs_html=post_hdr_oob + admin_oob, content_html=content, menu_html=menu_html) # ---- Post data ---- async def render_post_data_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) admin_hdr = _post_admin_header_html(ctx) from quart import url_for as qurl slug = (ctx.get("post") or {}).get("slug", "") data_hdr = _post_sub_admin_header_html( "post_data-row", "post_data-header-child", qurl("blog.post.admin.data", slug=slug), "database", "data", ctx, ) header_rows = root_hdr + post_hdr + admin_hdr + data_hdr content = ctx.get("data_html", "") return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_post_data_oob(ctx: dict) -> str: admin_hdr_oob = _post_admin_header_html(ctx, oob=True) from quart import url_for as qurl slug = (ctx.get("post") or {}).get("slug", "") data_hdr = _post_sub_admin_header_html( "post_data-row", "post_data-header-child", qurl("blog.post.admin.data", slug=slug), "database", "data", ctx, ) data_oob = _oob_header_html("post-admin-header-child", "post_data-header-child", data_hdr) content = ctx.get("data_html", "") return oob_page(ctx, oobs_html=admin_hdr_oob + data_oob, content_html=content) # ---- Post entries ---- async def render_post_entries_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) admin_hdr = _post_admin_header_html(ctx) from quart import url_for as qurl slug = (ctx.get("post") or {}).get("slug", "") entries_hdr = _post_sub_admin_header_html( "post_entries-row", "post_entries-header-child", qurl("blog.post.admin.entries", slug=slug), "clock", "entries", ctx, ) header_rows = root_hdr + post_hdr + admin_hdr + entries_hdr content = ctx.get("entries_html", "") return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_post_entries_oob(ctx: dict) -> str: admin_hdr_oob = _post_admin_header_html(ctx, oob=True) from quart import url_for as qurl slug = (ctx.get("post") or {}).get("slug", "") entries_hdr = _post_sub_admin_header_html( "post_entries-row", "post_entries-header-child", qurl("blog.post.admin.entries", slug=slug), "clock", "entries", ctx, ) entries_oob = _oob_header_html("post-admin-header-child", "post_entries-header-child", entries_hdr) content = ctx.get("entries_html", "") return oob_page(ctx, oobs_html=admin_hdr_oob + entries_oob, content_html=content) # ---- Post edit ---- async def render_post_edit_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) admin_hdr = _post_admin_header_html(ctx) from quart import url_for as qurl slug = (ctx.get("post") or {}).get("slug", "") edit_hdr = _post_sub_admin_header_html( "post_edit-row", "post_edit-header-child", qurl("blog.post.admin.edit", slug=slug), "pen-to-square", "edit", ctx, ) header_rows = root_hdr + post_hdr + admin_hdr + edit_hdr content = ctx.get("edit_html", "") body_end = ctx.get("body_end_html", "") return full_page(ctx, header_rows_html=header_rows, content_html=content, body_end_html=body_end) async def render_post_edit_oob(ctx: dict) -> str: admin_hdr_oob = _post_admin_header_html(ctx, oob=True) from quart import url_for as qurl slug = (ctx.get("post") or {}).get("slug", "") edit_hdr = _post_sub_admin_header_html( "post_edit-row", "post_edit-header-child", qurl("blog.post.admin.edit", slug=slug), "pen-to-square", "edit", ctx, ) edit_oob = _oob_header_html("post-admin-header-child", "post_edit-header-child", edit_hdr) content = ctx.get("edit_html", "") return oob_page(ctx, oobs_html=admin_hdr_oob + edit_oob, content_html=content) # ---- Post settings ---- async def render_post_settings_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) post_hdr = _post_header_html(ctx) admin_hdr = _post_admin_header_html(ctx) from quart import url_for as qurl slug = (ctx.get("post") or {}).get("slug", "") settings_hdr = _post_sub_admin_header_html( "post_settings-row", "post_settings-header-child", qurl("blog.post.admin.settings", slug=slug), "cog", "settings", ctx, ) header_rows = root_hdr + post_hdr + admin_hdr + settings_hdr content = ctx.get("settings_html", "") return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_post_settings_oob(ctx: dict) -> str: admin_hdr_oob = _post_admin_header_html(ctx, oob=True) from quart import url_for as qurl slug = (ctx.get("post") or {}).get("slug", "") settings_hdr = _post_sub_admin_header_html( "post_settings-row", "post_settings-header-child", qurl("blog.post.admin.settings", slug=slug), "cog", "settings", ctx, ) settings_oob = _oob_header_html("post-admin-header-child", "post_settings-header-child", settings_hdr) content = ctx.get("settings_html", "") return oob_page(ctx, oobs_html=admin_hdr_oob + settings_oob, content_html=content) # ---- Settings home ---- async def render_settings_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) settings_hdr = _settings_header_html(ctx) header_rows = root_hdr + settings_hdr content = _settings_main_panel_html(ctx) menu_html = _settings_nav_html(ctx) return full_page(ctx, header_rows_html=header_rows, content_html=content, menu_html=menu_html) async def render_settings_oob(ctx: dict) -> str: root_hdr = root_header_html(ctx, oob=True) settings_oob = _oob_header_html("root-header-child", "root-settings-header-child", _settings_header_html(ctx)) content = _settings_main_panel_html(ctx) menu_html = _settings_nav_html(ctx) return oob_page(ctx, oobs_html=root_hdr + settings_oob, content_html=content, menu_html=menu_html) # ---- Cache ---- async def render_cache_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) settings_hdr = _settings_header_html(ctx) from quart import url_for as qurl cache_hdr = _sub_settings_header_html( "cache-row", "cache-header-child", qurl("settings.cache"), "refresh", "Cache", ctx, ) header_rows = root_hdr + settings_hdr + cache_hdr content = _cache_main_panel_html(ctx) return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_cache_oob(ctx: dict) -> str: settings_hdr_oob = _settings_header_html(ctx, oob=True) from quart import url_for as qurl cache_hdr = _sub_settings_header_html( "cache-row", "cache-header-child", qurl("settings.cache"), "refresh", "Cache", ctx, ) cache_oob = _oob_header_html("root-settings-header-child", "cache-header-child", cache_hdr) content = _cache_main_panel_html(ctx) return oob_page(ctx, oobs_html=settings_hdr_oob + cache_oob, content_html=content) # ---- Snippets ---- async def render_snippets_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) settings_hdr = _settings_header_html(ctx) from quart import url_for as qurl snippets_hdr = _sub_settings_header_html( "snippets-row", "snippets-header-child", qurl("snippets.list_snippets"), "puzzle-piece", "Snippets", ctx, ) header_rows = root_hdr + settings_hdr + snippets_hdr content = _snippets_main_panel_html(ctx) return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_snippets_oob(ctx: dict) -> str: settings_hdr_oob = _settings_header_html(ctx, oob=True) from quart import url_for as qurl snippets_hdr = _sub_settings_header_html( "snippets-row", "snippets-header-child", qurl("snippets.list_snippets"), "puzzle-piece", "Snippets", ctx, ) snippets_oob = _oob_header_html("root-settings-header-child", "snippets-header-child", snippets_hdr) content = _snippets_main_panel_html(ctx) return oob_page(ctx, oobs_html=settings_hdr_oob + snippets_oob, content_html=content) # ---- Menu items ---- async def render_menu_items_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) settings_hdr = _settings_header_html(ctx) from quart import url_for as qurl mi_hdr = _sub_settings_header_html( "menu_items-row", "menu_items-header-child", qurl("menu_items.list_menu_items"), "bars", "Menu Items", ctx, ) header_rows = root_hdr + settings_hdr + mi_hdr content = _menu_items_main_panel_html(ctx) return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_menu_items_oob(ctx: dict) -> str: settings_hdr_oob = _settings_header_html(ctx, oob=True) from quart import url_for as qurl mi_hdr = _sub_settings_header_html( "menu_items-row", "menu_items-header-child", qurl("menu_items.list_menu_items"), "bars", "Menu Items", ctx, ) mi_oob = _oob_header_html("root-settings-header-child", "menu_items-header-child", mi_hdr) content = _menu_items_main_panel_html(ctx) return oob_page(ctx, oobs_html=settings_hdr_oob + mi_oob, content_html=content) # ---- Tag groups ---- async def render_tag_groups_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) settings_hdr = _settings_header_html(ctx) from quart import url_for as qurl tg_hdr = _sub_settings_header_html( "tag-groups-row", "tag-groups-header-child", qurl("blog.tag_groups_admin.index"), "tags", "Tag Groups", ctx, ) header_rows = root_hdr + settings_hdr + tg_hdr content = _tag_groups_main_panel_html(ctx) return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_tag_groups_oob(ctx: dict) -> str: settings_hdr_oob = _settings_header_html(ctx, oob=True) from quart import url_for as qurl tg_hdr = _sub_settings_header_html( "tag-groups-row", "tag-groups-header-child", qurl("blog.tag_groups_admin.index"), "tags", "Tag Groups", ctx, ) tg_oob = _oob_header_html("root-settings-header-child", "tag-groups-header-child", tg_hdr) content = _tag_groups_main_panel_html(ctx) return oob_page(ctx, oobs_html=settings_hdr_oob + tg_oob, content_html=content) # ---- Tag group edit ---- async def render_tag_group_edit_page(ctx: dict) -> str: root_hdr = root_header_html(ctx) settings_hdr = _settings_header_html(ctx) from quart import url_for as qurl g_id = (ctx.get("group") or {}).get("id") or getattr(ctx.get("group"), "id", None) tg_hdr = _sub_settings_header_html( "tag-groups-row", "tag-groups-header-child", qurl("blog.tag_groups_admin.edit", id=g_id), "tags", "Tag Groups", ctx, ) header_rows = root_hdr + settings_hdr + tg_hdr content = _tag_groups_edit_main_panel_html(ctx) return full_page(ctx, header_rows_html=header_rows, content_html=content) async def render_tag_group_edit_oob(ctx: dict) -> str: settings_hdr_oob = _settings_header_html(ctx, oob=True) from quart import url_for as qurl g_id = (ctx.get("group") or {}).get("id") or getattr(ctx.get("group"), "id", None) tg_hdr = _sub_settings_header_html( "tag-groups-row", "tag-groups-header-child", qurl("blog.tag_groups_admin.edit", id=g_id), "tags", "Tag Groups", ctx, ) tg_oob = _oob_header_html("root-settings-header-child", "tag-groups-header-child", tg_hdr) content = _tag_groups_edit_main_panel_html(ctx) return oob_page(ctx, oobs_html=settings_hdr_oob + tg_oob, content_html=content)