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:
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)))))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user