All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
- Server sends sexp source text, client (sexp.js) renders everything - SexpExpr marker class for nested sexp composition in serialize() - sexp_page() HTML shell with data-mount="body" for full page loads - sexp_response() returns text/sexp for OOB/partial responses - ~app-body layout component replaces ~app-layout (no raw!) - ~rich-text is the only component using raw! (for CMS HTML content) - Fragment endpoints return text/sexp, auto-wrapped in SexpExpr - All _*_html() helpers converted to _*_sexp() returning sexp source - Head auto-hoist: sexp.js moves meta/title/link/script[ld+json] from rendered body to document.head automatically - Unknown components render warning box instead of crashing page - Component kwargs preserve AST for lazy rendering (fixes <> in kwargs) - Fix unterminated paren in events/sexp/tickets.sexpr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
"""Blog app fragment endpoints.
|
|
|
|
Exposes sexp fragments at ``/internal/fragments/<type>`` for consumption
|
|
by other coop apps via the fragment client.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from quart import Blueprint, Response, g, render_template, request
|
|
|
|
from shared.infrastructure.fragments import FRAGMENT_HEADER
|
|
from shared.services.navigation import get_navigation_tree
|
|
|
|
|
|
def register():
|
|
bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments")
|
|
|
|
_handlers: dict[str, object] = {}
|
|
|
|
@bp.before_request
|
|
async def _require_fragment_header():
|
|
if not request.headers.get(FRAGMENT_HEADER):
|
|
return Response("", status=403)
|
|
|
|
@bp.get("/<fragment_type>")
|
|
async def get_fragment(fragment_type: str):
|
|
handler = _handlers.get(fragment_type)
|
|
if handler is None:
|
|
return Response("", status=200, content_type="text/sexp")
|
|
result = await handler()
|
|
# nav-tree still returns HTML (Jinja template) for now
|
|
ct = "text/html" if fragment_type == "nav-tree" else "text/sexp"
|
|
return Response(result, status=200, content_type=ct)
|
|
|
|
# --- nav-tree fragment (still Jinja for now — complex template) ---
|
|
async def _nav_tree_handler():
|
|
app_name = request.args.get("app_name", "")
|
|
path = request.args.get("path", "/")
|
|
first_seg = path.strip("/").split("/")[0]
|
|
menu_items = list(await get_navigation_tree(g.s))
|
|
|
|
class _NavItem:
|
|
__slots__ = ("slug", "label", "feature_image")
|
|
def __init__(self, slug, label, feature_image=None):
|
|
self.slug = slug
|
|
self.label = label
|
|
self.feature_image = feature_image
|
|
|
|
menu_items.append(_NavItem("artdag", "art-dag"))
|
|
|
|
return await render_template(
|
|
"fragments/nav_tree.html",
|
|
menu_items=menu_items,
|
|
frag_app_name=app_name,
|
|
frag_first_seg=first_seg,
|
|
)
|
|
|
|
_handlers["nav-tree"] = _nav_tree_handler
|
|
|
|
# --- link-card fragment — returns sexp source ---
|
|
def _blog_link_card_sexp(post, link: str) -> str:
|
|
from shared.sexp.helpers import sexp_call
|
|
published = post.published_at.strftime("%d %b %Y") if post.published_at else None
|
|
return sexp_call("link-card",
|
|
link=link,
|
|
title=post.title,
|
|
image=post.feature_image,
|
|
icon="fas fa-file-alt",
|
|
subtitle=post.custom_excerpt or post.excerpt,
|
|
detail=published,
|
|
data_app="blog")
|
|
|
|
async def _link_card_handler():
|
|
from shared.services.registry import services
|
|
from shared.infrastructure.urls import blog_url
|
|
|
|
slug = request.args.get("slug", "")
|
|
keys_raw = request.args.get("keys", "")
|
|
|
|
# Batch mode
|
|
if keys_raw:
|
|
slugs = [k.strip() for k in keys_raw.split(",") if k.strip()]
|
|
parts = []
|
|
for s in slugs:
|
|
parts.append(f"<!-- fragment:{s} -->")
|
|
post = await services.blog.get_post_by_slug(g.s, s)
|
|
if post:
|
|
parts.append(_blog_link_card_sexp(post, blog_url(f"/{post.slug}")))
|
|
return "\n".join(parts)
|
|
|
|
# Single mode
|
|
if not slug:
|
|
return ""
|
|
post = await services.blog.get_post_by_slug(g.s, slug)
|
|
if not post:
|
|
return ""
|
|
return _blog_link_card_sexp(post, blog_url(f"/{post.slug}"))
|
|
|
|
_handlers["link-card"] = _link_card_handler
|
|
|
|
bp._fragment_handlers = _handlers
|
|
|
|
return bp
|