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 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 23:22:30 +00:00
parent 341fc4cf28
commit 7ccb463a8b
9 changed files with 34 additions and 6 deletions

View File

@@ -63,6 +63,7 @@ def _post_to_public(p: Post) -> Dict[str, Any]:
"slug": p.slug, "slug": p.slug,
"title": p.title, "title": p.title,
"html": p.html, "html": p.html,
"sx_content": p.sx_content,
"is_page": p.is_page, "is_page": p.is_page,
"excerpt": p.custom_excerpt or p.excerpt, "excerpt": p.custom_excerpt or p.excerpt,
"custom_excerpt": p.custom_excerpt, "custom_excerpt": p.custom_excerpt,

View File

@@ -265,6 +265,7 @@ def register(url_prefix, title):
return await make_response(html, 400) return await make_response(html, 400)
# Create directly in db_blog # Create directly in db_blog
sx_content_raw = form.get("sx_content", "").strip() or None
post = await writer_create( post = await writer_create(
g.s, g.s,
title=title, title=title,
@@ -274,6 +275,7 @@ def register(url_prefix, title):
feature_image=feature_image or None, feature_image=feature_image or None,
custom_excerpt=custom_excerpt or None, custom_excerpt=custom_excerpt or None,
feature_image_caption=feature_image_caption or None, feature_image_caption=feature_image_caption or None,
sx_content=sx_content_raw,
) )
await g.s.flush() await g.s.flush()
@@ -337,6 +339,7 @@ def register(url_prefix, title):
return await make_response(html, 400) return await make_response(html, 400)
# Create directly in db_blog # Create directly in db_blog
sx_content_raw = form.get("sx_content", "").strip() or None
page = await writer_create_page( page = await writer_create_page(
g.s, g.s,
title=title, title=title,
@@ -346,6 +349,7 @@ def register(url_prefix, title):
feature_image=feature_image or None, feature_image=feature_image or None,
custom_excerpt=custom_excerpt or None, custom_excerpt=custom_excerpt or None,
feature_image_caption=feature_image_caption or None, feature_image_caption=feature_image_caption or None,
sx_content=sx_content_raw,
) )
await g.s.flush() await g.s.flush()

View File

@@ -562,6 +562,7 @@ def register():
elif status and status != current_status: elif status and status != current_status:
effective_status = status effective_status = status
sx_content_raw = form.get("sx_content", "").strip() or None
try: try:
post = await writer_update( post = await writer_update(
g.s, g.s,
@@ -573,6 +574,7 @@ def register():
custom_excerpt=custom_excerpt or None, custom_excerpt=custom_excerpt or None,
feature_image_caption=feature_image_caption or None, feature_image_caption=feature_image_caption or None,
status=effective_status, status=effective_status,
sx_content=sx_content_raw,
) )
except OptimisticLockError: except OptimisticLockError:
return redirect( return redirect(

View File

@@ -60,6 +60,7 @@ class Post(Base):
plaintext: Mapped[Optional[str]] = mapped_column(Text()) plaintext: Mapped[Optional[str]] = mapped_column(Text())
mobiledoc: Mapped[Optional[str]] = mapped_column(Text()) mobiledoc: Mapped[Optional[str]] = mapped_column(Text())
lexical: 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: Mapped[Optional[str]] = mapped_column(Text())
feature_image_alt: Mapped[Optional[str]] = mapped_column(Text()) feature_image_alt: Mapped[Optional[str]] = mapped_column(Text())

View File

@@ -19,6 +19,7 @@ def _post_to_dto(post: Post) -> PostDTO:
is_page=post.is_page, is_page=post.is_page,
feature_image=post.feature_image, feature_image=post.feature_image,
html=post.html, html=post.html,
sx_content=post.sx_content,
excerpt=post.excerpt, excerpt=post.excerpt,
custom_excerpt=post.custom_excerpt, custom_excerpt=post.custom_excerpt,
published_at=post.published_at, published_at=post.published_at,

View File

@@ -207,6 +207,7 @@ async def create_post(
feature_image_caption: str | None = None, feature_image_caption: str | None = None,
tag_names: list[str] | None = None, tag_names: list[str] | None = None,
is_page: bool = False, is_page: bool = False,
sx_content: str | None = None,
) -> Post: ) -> Post:
"""Create a new post or page directly in db_blog.""" """Create a new post or page directly in db_blog."""
html, plaintext, reading_time = _render_and_extract(lexical_json) html, plaintext, reading_time = _render_and_extract(lexical_json)
@@ -217,6 +218,7 @@ async def create_post(
title=title or "Untitled", title=title or "Untitled",
slug=slug, slug=slug,
lexical=lexical_json if isinstance(lexical_json, str) else json.dumps(lexical_json), lexical=lexical_json if isinstance(lexical_json, str) else json.dumps(lexical_json),
sx_content=sx_content,
html=html, html=html,
plaintext=plaintext, plaintext=plaintext,
reading_time=reading_time, reading_time=reading_time,
@@ -281,6 +283,7 @@ async def create_page(
custom_excerpt: str | None = None, custom_excerpt: str | None = None,
feature_image_caption: str | None = None, feature_image_caption: str | None = None,
tag_names: list[str] | None = None, tag_names: list[str] | None = None,
sx_content: str | None = None,
) -> Post: ) -> Post:
"""Create a new page. Convenience wrapper around create_post.""" """Create a new page. Convenience wrapper around create_post."""
return await create_post( return await create_post(
@@ -294,6 +297,7 @@ async def create_page(
feature_image_caption=feature_image_caption, feature_image_caption=feature_image_caption,
tag_names=tag_names, tag_names=tag_names,
is_page=True, is_page=True,
sx_content=sx_content,
) )
@@ -308,6 +312,7 @@ async def update_post(
custom_excerpt: str | None = ..., # type: ignore[assignment] custom_excerpt: str | None = ..., # type: ignore[assignment]
feature_image_caption: str | None = ..., # type: ignore[assignment] feature_image_caption: str | None = ..., # type: ignore[assignment]
status: str | None = None, status: str | None = None,
sx_content: str | None = ..., # type: ignore[assignment]
) -> Post: ) -> Post:
"""Update post content. Optimistic lock via expected_updated_at. """Update post content. Optimistic lock via expected_updated_at.
@@ -342,6 +347,9 @@ async def update_post(
if title is not None: if title is not None:
post.title = title post.title = title
if sx_content is not _SENTINEL:
post.sx_content = sx_content
if feature_image is not _SENTINEL: if feature_image is not _SENTINEL:
post.feature_image = feature_image post.feature_image = feature_image
if custom_excerpt is not _SENTINEL: if custom_excerpt is not _SENTINEL:

View File

@@ -25,13 +25,15 @@
excerpt excerpt
(div :class "hidden md:block" at-bar))) (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" (<> (article :class "relative"
draft draft
chrome chrome
(when feature-image (div :class "mb-3 flex justify-center" (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"))) (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"))) (div :class "pb-8")))
(defcomp ~blog-meta (&key robots page-title desc canonical og-type og-title image twitter-card twitter-title) (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) (meta :name "twitter:description" :content desc)
(when image (meta :name "twitter:image" :content image)))) (when image (meta :name "twitter:image" :content image))))
(defcomp ~blog-home-main (&key html-content) (defcomp ~blog-home-main (&key html-content sx-content)
(article :class "relative" (div :class "blog-content p-2" (~rich-text :html html-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)))))

View File

@@ -716,11 +716,13 @@ def _post_main_panel_sx(ctx: dict) -> str:
fi = post.get("feature_image") fi = post.get("feature_image")
html_content = post.get("html", "") html_content = post.get("html", "")
sx_content = post.get("sx_content", "")
return sx_call("blog-detail-main", return sx_call("blog-detail-main",
draft=SxExpr(draft_sx) if draft_sx else None, draft=SxExpr(draft_sx) if draft_sx else None,
chrome=SxExpr(chrome_sx) if chrome_sx else None, chrome=SxExpr(chrome_sx) if chrome_sx else None,
feature_image=fi, html_content=html_content, 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: 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 {} post = ctx.get("post") or {}
html = post.get("html", "") 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)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -87,6 +87,7 @@ class PostDTO:
is_page: bool = False is_page: bool = False
feature_image: str | None = None feature_image: str | None = None
html: str | None = None html: str | None = None
sx_content: str | None = None
excerpt: str | None = None excerpt: str | None = None
custom_excerpt: str | None = None custom_excerpt: str | None = None
published_at: datetime | None = None published_at: datetime | None = None