From 7ccb463a8b91c90a80330d632f530ac70e17df86 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 1 Mar 2026 23:22:30 +0000 Subject: [PATCH] Wire sx_content through full read/write pipeline Model: add sx_content column to Post. Writer: accept sx_content in create_post, create_page, update_post. Routes: read sx_content from form data in new post, new page, and edit routes. Read pipeline: ghost_db includes sx_content in public dict, detail/home views prefer sx_content over html when available, PostDTO includes sx_content. Co-Authored-By: Claude Opus 4.6 --- blog/bp/blog/ghost_db.py | 1 + blog/bp/blog/routes.py | 4 ++++ blog/bp/post/admin/routes.py | 2 ++ blog/models/content.py | 1 + blog/services/__init__.py | 1 + blog/services/post_writer.py | 8 ++++++++ blog/sx/detail.sx | 13 +++++++++---- blog/sx/sx_components.py | 9 +++++++-- shared/contracts/dtos.py | 1 + 9 files changed, 34 insertions(+), 6 deletions(-) diff --git a/blog/bp/blog/ghost_db.py b/blog/bp/blog/ghost_db.py index 2081a10..96334e6 100644 --- a/blog/bp/blog/ghost_db.py +++ b/blog/bp/blog/ghost_db.py @@ -63,6 +63,7 @@ def _post_to_public(p: Post) -> Dict[str, Any]: "slug": p.slug, "title": p.title, "html": p.html, + "sx_content": p.sx_content, "is_page": p.is_page, "excerpt": p.custom_excerpt or p.excerpt, "custom_excerpt": p.custom_excerpt, diff --git a/blog/bp/blog/routes.py b/blog/bp/blog/routes.py index 32f6424..8e6debc 100644 --- a/blog/bp/blog/routes.py +++ b/blog/bp/blog/routes.py @@ -265,6 +265,7 @@ def register(url_prefix, title): return await make_response(html, 400) # Create directly in db_blog + sx_content_raw = form.get("sx_content", "").strip() or None post = await writer_create( g.s, title=title, @@ -274,6 +275,7 @@ def register(url_prefix, title): feature_image=feature_image or None, custom_excerpt=custom_excerpt or None, feature_image_caption=feature_image_caption or None, + sx_content=sx_content_raw, ) await g.s.flush() @@ -337,6 +339,7 @@ def register(url_prefix, title): return await make_response(html, 400) # Create directly in db_blog + sx_content_raw = form.get("sx_content", "").strip() or None page = await writer_create_page( g.s, title=title, @@ -346,6 +349,7 @@ def register(url_prefix, title): feature_image=feature_image or None, custom_excerpt=custom_excerpt or None, feature_image_caption=feature_image_caption or None, + sx_content=sx_content_raw, ) await g.s.flush() diff --git a/blog/bp/post/admin/routes.py b/blog/bp/post/admin/routes.py index f813ed8..5c6e711 100644 --- a/blog/bp/post/admin/routes.py +++ b/blog/bp/post/admin/routes.py @@ -562,6 +562,7 @@ def register(): elif status and status != current_status: effective_status = status + sx_content_raw = form.get("sx_content", "").strip() or None try: post = await writer_update( g.s, @@ -573,6 +574,7 @@ def register(): custom_excerpt=custom_excerpt or None, feature_image_caption=feature_image_caption or None, status=effective_status, + sx_content=sx_content_raw, ) except OptimisticLockError: return redirect( diff --git a/blog/models/content.py b/blog/models/content.py index 260fbdd..7875e41 100644 --- a/blog/models/content.py +++ b/blog/models/content.py @@ -60,6 +60,7 @@ class Post(Base): plaintext: Mapped[Optional[str]] = mapped_column(Text()) mobiledoc: Mapped[Optional[str]] = mapped_column(Text()) lexical: Mapped[Optional[str]] = mapped_column(Text()) + sx_content: Mapped[Optional[str]] = mapped_column(Text()) feature_image: Mapped[Optional[str]] = mapped_column(Text()) feature_image_alt: Mapped[Optional[str]] = mapped_column(Text()) diff --git a/blog/services/__init__.py b/blog/services/__init__.py index 5b7f51f..f3b1c94 100644 --- a/blog/services/__init__.py +++ b/blog/services/__init__.py @@ -19,6 +19,7 @@ def _post_to_dto(post: Post) -> PostDTO: is_page=post.is_page, feature_image=post.feature_image, html=post.html, + sx_content=post.sx_content, excerpt=post.excerpt, custom_excerpt=post.custom_excerpt, published_at=post.published_at, diff --git a/blog/services/post_writer.py b/blog/services/post_writer.py index face44d..b80741d 100644 --- a/blog/services/post_writer.py +++ b/blog/services/post_writer.py @@ -207,6 +207,7 @@ async def create_post( feature_image_caption: str | None = None, tag_names: list[str] | None = None, is_page: bool = False, + sx_content: str | None = None, ) -> Post: """Create a new post or page directly in db_blog.""" html, plaintext, reading_time = _render_and_extract(lexical_json) @@ -217,6 +218,7 @@ async def create_post( title=title or "Untitled", slug=slug, lexical=lexical_json if isinstance(lexical_json, str) else json.dumps(lexical_json), + sx_content=sx_content, html=html, plaintext=plaintext, reading_time=reading_time, @@ -281,6 +283,7 @@ async def create_page( custom_excerpt: str | None = None, feature_image_caption: str | None = None, tag_names: list[str] | None = None, + sx_content: str | None = None, ) -> Post: """Create a new page. Convenience wrapper around create_post.""" return await create_post( @@ -294,6 +297,7 @@ async def create_page( feature_image_caption=feature_image_caption, tag_names=tag_names, is_page=True, + sx_content=sx_content, ) @@ -308,6 +312,7 @@ async def update_post( custom_excerpt: str | None = ..., # type: ignore[assignment] feature_image_caption: str | None = ..., # type: ignore[assignment] status: str | None = None, + sx_content: str | None = ..., # type: ignore[assignment] ) -> Post: """Update post content. Optimistic lock via expected_updated_at. @@ -342,6 +347,9 @@ async def update_post( if title is not None: post.title = title + if sx_content is not _SENTINEL: + post.sx_content = sx_content + if feature_image is not _SENTINEL: post.feature_image = feature_image if custom_excerpt is not _SENTINEL: diff --git a/blog/sx/detail.sx b/blog/sx/detail.sx index c25f247..3ce5eb3 100644 --- a/blog/sx/detail.sx +++ b/blog/sx/detail.sx @@ -25,13 +25,15 @@ excerpt (div :class "hidden md:block" at-bar))) -(defcomp ~blog-detail-main (&key draft chrome feature-image html-content) +(defcomp ~blog-detail-main (&key draft chrome feature-image html-content sx-content) (<> (article :class "relative" draft chrome (when feature-image (div :class "mb-3 flex justify-center" (img :src feature-image :alt "" :class "rounded-lg w-full md:w-3/4 object-cover"))) - (when html-content (div :class "blog-content p-2" (~rich-text :html html-content)))) + (if sx-content + (div :class "blog-content p-2" sx-content) + (when html-content (div :class "blog-content p-2" (~rich-text :html html-content))))) (div :class "pb-8"))) (defcomp ~blog-meta (&key robots page-title desc canonical og-type og-title image twitter-card twitter-title) @@ -50,5 +52,8 @@ (meta :name "twitter:description" :content desc) (when image (meta :name "twitter:image" :content image)))) -(defcomp ~blog-home-main (&key html-content) - (article :class "relative" (div :class "blog-content p-2" (~rich-text :html html-content)))) +(defcomp ~blog-home-main (&key html-content sx-content) + (article :class "relative" + (if sx-content + (div :class "blog-content p-2" sx-content) + (div :class "blog-content p-2" (~rich-text :html html-content))))) diff --git a/blog/sx/sx_components.py b/blog/sx/sx_components.py index 81415c6..f7e8be0 100644 --- a/blog/sx/sx_components.py +++ b/blog/sx/sx_components.py @@ -716,11 +716,13 @@ def _post_main_panel_sx(ctx: dict) -> str: fi = post.get("feature_image") html_content = post.get("html", "") + sx_content = post.get("sx_content", "") return sx_call("blog-detail-main", draft=SxExpr(draft_sx) if draft_sx else None, chrome=SxExpr(chrome_sx) if chrome_sx else None, feature_image=fi, html_content=html_content, + sx_content=SxExpr(sx_content) if sx_content else None, ) @@ -770,10 +772,13 @@ def _post_meta_sx(ctx: dict) -> str: # --------------------------------------------------------------------------- def _home_main_panel_sx(ctx: dict) -> str: - """Home page content — renders the Ghost page HTML.""" + """Home page content — renders the Ghost page HTML or sx_content.""" post = ctx.get("post") or {} html = post.get("html", "") - return sx_call("blog-home-main", html_content=html) + sx_content = post.get("sx_content", "") + return sx_call("blog-home-main", + html_content=html, + sx_content=SxExpr(sx_content) if sx_content else None) # --------------------------------------------------------------------------- diff --git a/shared/contracts/dtos.py b/shared/contracts/dtos.py index c023580..930c9eb 100644 --- a/shared/contracts/dtos.py +++ b/shared/contracts/dtos.py @@ -87,6 +87,7 @@ class PostDTO: is_page: bool = False feature_image: str | None = None html: str | None = None + sx_content: str | None = None excerpt: str | None = None custom_excerpt: str | None = None published_at: datetime | None = None