Move home/post detail/like rendering from Python to .sx defcomps
- Home page: inline shared helpers, render_to_sx("blog-home-main")
- Post detail: new ~blog-post-detail-content defcomp with data from service
- Like toggle: call render_to_sx("market-like-toggle-button") directly
- Add post_meta_data() and post_detail_data() to BlogPageService
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -118,15 +118,38 @@ def register(url_prefix, title):
|
||||
ctx["page_cart_total"] = float(page_summary.total + page_summary.calendar_total + page_summary.ticket_total)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
from sx.sx_components import render_home_page, render_home_oob
|
||||
from shared.sx.helpers import (
|
||||
render_to_sx, root_header_sx, full_page_sx, oob_page_sx,
|
||||
post_header_sx, oob_header_sx, mobile_menu_sx,
|
||||
post_mobile_nav_sx, mobile_root_nav_sx,
|
||||
)
|
||||
from shared.sx.parser import SxExpr
|
||||
from shared.services.registry import services
|
||||
|
||||
tctx = await get_template_context()
|
||||
tctx.update(ctx)
|
||||
|
||||
post = ctx.get("post", {})
|
||||
content = await render_to_sx("blog-home-main",
|
||||
html_content=post.get("html", ""),
|
||||
sx_content=SxExpr(post.get("sx_content", "")) if post.get("sx_content") else None)
|
||||
meta_data = services.get("blog_page").post_meta_data(post, ctx.get("base_title", ""))
|
||||
meta = await render_to_sx("blog-meta", **meta_data)
|
||||
|
||||
if not is_htmx_request():
|
||||
html = await render_home_page(tctx)
|
||||
root_hdr = await root_header_sx(tctx)
|
||||
post_hdr = await post_header_sx(tctx)
|
||||
header_rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
||||
menu = mobile_menu_sx(await post_mobile_nav_sx(tctx), await mobile_root_nav_sx(tctx))
|
||||
html = await full_page_sx(tctx, header_rows=header_rows, content=content,
|
||||
meta=meta, menu=menu)
|
||||
return await make_response(html)
|
||||
else:
|
||||
sx_src = await render_home_oob(tctx)
|
||||
root_hdr = await root_header_sx(tctx)
|
||||
post_hdr = await post_header_sx(tctx)
|
||||
rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
||||
header_oob = await oob_header_sx("root-header-child", "post-header-child", rows)
|
||||
sx_src = await oob_page_sx(oobs=header_oob, content=content)
|
||||
return sx_response(sx_src)
|
||||
|
||||
@blogs_bp.get("/index")
|
||||
|
||||
@@ -105,27 +105,68 @@ def register():
|
||||
@cache_page(tag="post.post_detail")
|
||||
async def post_detail(slug: str):
|
||||
from shared.sx.page import get_template_context
|
||||
from sx.sx_components import render_post_page, render_post_oob
|
||||
from shared.sx.helpers import (
|
||||
render_to_sx, root_header_sx, full_page_sx, oob_page_sx,
|
||||
post_header_sx, oob_header_sx, mobile_menu_sx,
|
||||
post_mobile_nav_sx, mobile_root_nav_sx,
|
||||
)
|
||||
from shared.services.registry import services
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from shared.utils import host_url
|
||||
|
||||
tctx = await get_template_context()
|
||||
|
||||
# Render post content via .sx defcomp
|
||||
post = tctx.get("post") or {}
|
||||
user = getattr(g, "user", None)
|
||||
rights = tctx.get("rights") or {}
|
||||
blog_url_base = host_url(url_for("blog.index")).rstrip("/index").rstrip("/")
|
||||
csrf = generate_csrf_token()
|
||||
svc = services.get("blog_page")
|
||||
detail_data = svc.post_detail_data(post, user, rights, csrf, blog_url_base)
|
||||
content = await render_to_sx("blog-post-detail-content", **detail_data)
|
||||
meta_data = svc.post_meta_data(post, tctx.get("base_title", ""))
|
||||
meta = await render_to_sx("blog-meta", **meta_data)
|
||||
|
||||
if not is_htmx_request():
|
||||
html = await render_post_page(tctx)
|
||||
root_hdr = await root_header_sx(tctx)
|
||||
post_hdr = await post_header_sx(tctx)
|
||||
header_rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
||||
menu = mobile_menu_sx(await post_mobile_nav_sx(tctx), await mobile_root_nav_sx(tctx))
|
||||
html = await full_page_sx(tctx, header_rows=header_rows, content=content,
|
||||
meta=meta, menu=menu)
|
||||
return await make_response(html)
|
||||
else:
|
||||
sx_src = await render_post_oob(tctx)
|
||||
root_hdr = await root_header_sx(tctx)
|
||||
post_hdr = await post_header_sx(tctx)
|
||||
rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
||||
header_oob = await oob_header_sx("root-header-child", "post-header-child", rows)
|
||||
sx_src = await oob_page_sx(oobs=header_oob, content=content, menu=
|
||||
mobile_menu_sx(await post_mobile_nav_sx(tctx), await mobile_root_nav_sx(tctx)))
|
||||
return sx_response(sx_src)
|
||||
|
||||
@bp.post("/like/toggle/")
|
||||
@clear_cache(tag="post.post_detail", tag_scope="user")
|
||||
async def like_toggle(slug: str):
|
||||
from shared.utils import host_url
|
||||
from sx.sx_components import render_like_toggle_button
|
||||
from shared.sx.helpers import render_to_sx
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
like_url = host_url(url_for('blog.post.like_toggle', slug=slug))
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
async def _like_btn(liked):
|
||||
if liked:
|
||||
colour, icon, label = "text-red-600", "fa-solid fa-heart", "Unlike this post"
|
||||
else:
|
||||
colour, icon, label = "text-stone-300", "fa-regular fa-heart", "Like this post"
|
||||
return await render_to_sx("market-like-toggle-button",
|
||||
colour=colour, action=like_url,
|
||||
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
label=label, icon_cls=icon)
|
||||
|
||||
# Get post_id from g.post_data
|
||||
if not g.user:
|
||||
return sx_response(await render_like_toggle_button(slug, False, like_url), status=403)
|
||||
return sx_response(await _like_btn(False), status=403)
|
||||
|
||||
post_id = g.post_data["post"]["id"]
|
||||
user_id = g.user.id
|
||||
@@ -133,9 +174,8 @@ def register():
|
||||
result = await call_action("likes", "toggle", payload={
|
||||
"user_id": user_id, "target_type": "post", "target_id": post_id,
|
||||
})
|
||||
liked = result["liked"]
|
||||
|
||||
return sx_response(await render_like_toggle_button(slug, liked, like_url))
|
||||
return sx_response(await _like_btn(result["liked"]))
|
||||
|
||||
@bp.get("/w/<widget_domain>/")
|
||||
async def widget_paginate(slug: str, widget_domain: str):
|
||||
|
||||
@@ -355,6 +355,78 @@ class BlogPageService:
|
||||
"sumup_configured": sumup_configured,
|
||||
}
|
||||
|
||||
def post_meta_data(self, post, base_title):
|
||||
"""Compute SEO meta tag values from post dict."""
|
||||
import re
|
||||
from quart import request as req
|
||||
|
||||
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"
|
||||
|
||||
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"):
|
||||
desc = re.sub(r'<[^>]+>', '', post["html"])
|
||||
desc = desc.replace("\n", " ").replace("\r", " ").strip()[:160]
|
||||
|
||||
image = (post.get("og_image") or post.get("twitter_image") or post.get("feature_image") or "")
|
||||
canonical = post.get("canonical_url") or (req.url if req else "")
|
||||
|
||||
post_title = post.get("meta_title") or post.get("title") or ""
|
||||
page_title = f"{post_title} \u2014 {base_title}" if post_title else base_title
|
||||
og_title = post.get("og_title") or page_title
|
||||
tw_title = post.get("twitter_title") or page_title
|
||||
is_article = not post.get("is_page")
|
||||
|
||||
return {
|
||||
"robots": robots, "page_title": page_title, "desc": desc,
|
||||
"canonical": canonical,
|
||||
"og_type": "article" if is_article else "website",
|
||||
"og_title": og_title, "image": image,
|
||||
"twitter_card": "summary_large_image" if image else "summary",
|
||||
"twitter_title": tw_title,
|
||||
}
|
||||
|
||||
def post_detail_data(self, post, user, rights, csrf, blog_url_base):
|
||||
"""Serialize post detail view data for ~blog-post-detail-content defcomp."""
|
||||
slug = post.get("slug", "")
|
||||
is_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
|
||||
user_id = getattr(user, "id", None) if user else None
|
||||
|
||||
# Tags and authors
|
||||
tags = []
|
||||
for t in (post.get("tags") or []):
|
||||
name = t.get("name") or getattr(t, "name", "")
|
||||
fi = t.get("feature_image") or getattr(t, "feature_image", None)
|
||||
tags.append({"name": name, "src": fi or "", "initial": name[:1] if name else ""})
|
||||
authors = []
|
||||
for a in (post.get("authors") or []):
|
||||
name = a.get("name") or getattr(a, "name", "")
|
||||
img = a.get("profile_image") or getattr(a, "profile_image", None)
|
||||
authors.append({"name": name, "image": img or ""})
|
||||
|
||||
return {
|
||||
"slug": slug,
|
||||
"is_draft": post.get("status") == "draft",
|
||||
"publish_requested": post.get("publish_requested", False),
|
||||
"can_edit": is_admin or (user_id is not None and post.get("user_id") == user_id),
|
||||
"edit_href": f"{blog_url_base}/{slug}/admin/edit/",
|
||||
"is_page": bool(post.get("is_page")),
|
||||
"has_user": bool(user),
|
||||
"liked": post.get("is_liked", False),
|
||||
"like_url": f"{blog_url_base}/{slug}/like/toggle/",
|
||||
"csrf": csrf,
|
||||
"custom_excerpt": post.get("custom_excerpt") or "",
|
||||
"tags": tags,
|
||||
"authors": authors,
|
||||
"feature_image": post.get("feature_image"),
|
||||
"html_content": post.get("html", ""),
|
||||
"sx_content": post.get("sx_content", ""),
|
||||
}
|
||||
|
||||
async def preview_data(self, session, *, slug=None, **kw):
|
||||
"""Build preview data with prettified/rendered content."""
|
||||
from quart import g
|
||||
|
||||
@@ -36,6 +36,37 @@
|
||||
(when html-content (div :class "blog-content p-2" (~rich-text :html html-content)))))
|
||||
(div :class "pb-8")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Data-driven composition — replaces _post_main_panel_sx
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~blog-post-detail-content (&key slug is-draft publish-requested can-edit edit-href
|
||||
is-page has-user liked like-url csrf
|
||||
custom-excerpt tags authors
|
||||
feature-image html-content sx-content)
|
||||
(let* ((hx-select "#main-panel")
|
||||
(draft-sx (when is-draft
|
||||
(~blog-detail-draft
|
||||
:publish-requested publish-requested
|
||||
:edit (when can-edit
|
||||
(~blog-detail-edit-link :href edit-href :hx-select hx-select)))))
|
||||
(chrome-sx (when (not is-page)
|
||||
(~blog-detail-chrome
|
||||
:like (when has-user
|
||||
(~blog-detail-like
|
||||
:like-url like-url
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:heart (if liked "\u2764\ufe0f" "\U0001f90d")))
|
||||
:excerpt (when (not (= custom-excerpt ""))
|
||||
(~blog-detail-excerpt :excerpt custom-excerpt))
|
||||
:at-bar (~blog-at-bar :tags tags :authors authors)))))
|
||||
(~blog-detail-main
|
||||
:draft draft-sx
|
||||
:chrome chrome-sx
|
||||
:feature-image feature-image
|
||||
:html-content html-content
|
||||
:sx-content sx-content)))
|
||||
|
||||
(defcomp ~blog-meta (&key robots page-title desc canonical og-type og-title image twitter-card twitter-title)
|
||||
(<>
|
||||
(meta :name "robots" :content robots)
|
||||
|
||||
Reference in New Issue
Block a user