Wire s-expression rendering into live app — blog link-card
- Add setup_sexp_bridge() and load_shared_components() to factory.py so all services get s-expression support automatically - Create shared/sexp/components.py with ~link-card component definition (replaces 5 per-service Jinja link_card.html templates) - Replace blog's link-card fragment handler to use sexp() instead of render_template() — first real s-expression rendered page content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ from quart import Blueprint, Response, g, render_template, request
|
|||||||
|
|
||||||
from shared.infrastructure.fragments import FRAGMENT_HEADER
|
from shared.infrastructure.fragments import FRAGMENT_HEADER
|
||||||
from shared.services.navigation import get_navigation_tree
|
from shared.services.navigation import get_navigation_tree
|
||||||
|
from shared.sexp.jinja_bridge import sexp
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
@@ -57,7 +58,21 @@ def register():
|
|||||||
|
|
||||||
_handlers["nav-tree"] = _nav_tree_handler
|
_handlers["nav-tree"] = _nav_tree_handler
|
||||||
|
|
||||||
# --- link-card fragment ---
|
# --- link-card fragment (s-expression rendered) ---
|
||||||
|
def _render_blog_link_card(post, link: str) -> str:
|
||||||
|
"""Render a blog link-card via the ~link-card s-expression component."""
|
||||||
|
published = post.published_at.strftime("%d %b %Y") if post.published_at else None
|
||||||
|
return sexp(
|
||||||
|
'(~link-card :link link :title title :image image'
|
||||||
|
' :icon "fas fa-file-alt" :subtitle excerpt'
|
||||||
|
' :detail published :data-app "blog")',
|
||||||
|
link=link,
|
||||||
|
title=post.title,
|
||||||
|
image=post.feature_image,
|
||||||
|
excerpt=post.custom_excerpt or post.excerpt,
|
||||||
|
published=published,
|
||||||
|
)
|
||||||
|
|
||||||
async def _link_card_handler():
|
async def _link_card_handler():
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
from shared.infrastructure.urls import blog_url
|
from shared.infrastructure.urls import blog_url
|
||||||
@@ -73,14 +88,7 @@ def register():
|
|||||||
parts.append(f"<!-- fragment:{s} -->")
|
parts.append(f"<!-- fragment:{s} -->")
|
||||||
post = await services.blog.get_post_by_slug(g.s, s)
|
post = await services.blog.get_post_by_slug(g.s, s)
|
||||||
if post:
|
if post:
|
||||||
parts.append(await render_template(
|
parts.append(_render_blog_link_card(post, blog_url(f"/{post.slug}")))
|
||||||
"fragments/link_card.html",
|
|
||||||
title=post.title,
|
|
||||||
feature_image=post.feature_image,
|
|
||||||
excerpt=post.custom_excerpt or post.excerpt,
|
|
||||||
published_at=post.published_at,
|
|
||||||
link=blog_url(f"/{post.slug}"),
|
|
||||||
))
|
|
||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
# Single mode
|
# Single mode
|
||||||
@@ -89,14 +97,7 @@ def register():
|
|||||||
post = await services.blog.get_post_by_slug(g.s, slug)
|
post = await services.blog.get_post_by_slug(g.s, slug)
|
||||||
if not post:
|
if not post:
|
||||||
return ""
|
return ""
|
||||||
return await render_template(
|
return _render_blog_link_card(post, blog_url(f"/{post.slug}"))
|
||||||
"fragments/link_card.html",
|
|
||||||
title=post.title,
|
|
||||||
feature_image=post.feature_image,
|
|
||||||
excerpt=post.custom_excerpt or post.excerpt,
|
|
||||||
published_at=post.published_at,
|
|
||||||
link=blog_url(f"/{post.slug}"),
|
|
||||||
)
|
|
||||||
|
|
||||||
_handlers["link-card"] = _link_card_handler
|
_handlers["link-card"] = _link_card_handler
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ from shared.browser.app.errors import errors
|
|||||||
|
|
||||||
from .jinja_setup import setup_jinja
|
from .jinja_setup import setup_jinja
|
||||||
from .user_loader import load_current_user
|
from .user_loader import load_current_user
|
||||||
|
from shared.sexp.jinja_bridge import setup_sexp_bridge
|
||||||
|
from shared.sexp.components import load_shared_components
|
||||||
|
|
||||||
|
|
||||||
# Async init of config (runs once at import)
|
# Async init of config (runs once at import)
|
||||||
@@ -104,6 +106,8 @@ def create_base_app(
|
|||||||
register_db(app)
|
register_db(app)
|
||||||
register_redis(app)
|
register_redis(app)
|
||||||
setup_jinja(app)
|
setup_jinja(app)
|
||||||
|
setup_sexp_bridge(app)
|
||||||
|
load_shared_components()
|
||||||
errors(app)
|
errors(app)
|
||||||
|
|
||||||
# Auto-register OAuth client blueprint for non-account apps
|
# Auto-register OAuth client blueprint for non-account apps
|
||||||
|
|||||||
51
shared/sexp/components.py
Normal file
51
shared/sexp/components.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Shared s-expression component definitions.
|
||||||
|
|
||||||
|
Loaded at app startup via ``load_shared_components()``. Each component
|
||||||
|
replaces a per-service Jinja fragment template with a single reusable
|
||||||
|
s-expression definition.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .jinja_bridge import register_components
|
||||||
|
|
||||||
|
|
||||||
|
def load_shared_components() -> None:
|
||||||
|
"""Register all shared s-expression components."""
|
||||||
|
register_components(_LINK_CARD)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ~link-card
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Replaces: blog/templates/fragments/link_card.html
|
||||||
|
# market/templates/fragments/link_card.html
|
||||||
|
# events/templates/fragments/link_card.html
|
||||||
|
# federation/templates/fragments/link_card.html
|
||||||
|
# artdag/l1/app/templates/fragments/link_card.html
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# sexp('(~link-card :link "/post/apple/" :title "Apple" :image "/img/a.jpg")')
|
||||||
|
# sexp('(~link-card :link url :title title :icon "fas fa-file-alt")', **ctx)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_LINK_CARD = '''
|
||||||
|
(defcomp ~link-card (&key link title image icon subtitle detail data-app)
|
||||||
|
(a :href link
|
||||||
|
:class "block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline"
|
||||||
|
:data-fragment "link-card"
|
||||||
|
:data-app data-app
|
||||||
|
:data-hx-disable true
|
||||||
|
(div :class "flex flex-row items-start gap-3 p-3"
|
||||||
|
(if image
|
||||||
|
(img :src image :alt "" :class "flex-shrink-0 w-16 h-16 rounded object-cover")
|
||||||
|
(div :class "flex-shrink-0 w-16 h-16 rounded bg-stone-100 flex items-center justify-center text-stone-400"
|
||||||
|
(i :class icon)))
|
||||||
|
(div :class "flex-1 min-w-0"
|
||||||
|
(div :class "font-medium text-stone-900 text-sm clamp-2" title)
|
||||||
|
(when subtitle
|
||||||
|
(div :class "text-xs text-stone-500 mt-0.5" subtitle))
|
||||||
|
(when detail
|
||||||
|
(div :class "text-xs text-stone-400 mt-1" detail))))))
|
||||||
|
'''
|
||||||
Reference in New Issue
Block a user