From 1e32c7e945a47d6c9a6c374c7406e67d4e9d8a08 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Tue, 21 Jul 2026 21:32:28 +0200 Subject: [PATCH] Rewrite published posts after template changes. --- README.md | 2 +- crates/bds-core/src/engine/metadata_diff.rs | 11 +---- crates/bds-core/src/engine/post.rs | 49 ++++++++++++++++++++- specs/engine_side_effects.allium | 4 +- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 29736d6..de17025 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The project is under active development. Core blogging workflows are broadly ava ## Available Features - Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars, dialogs, tasks, embedded Wry previews, and a live Pico CSS theme editor. -- Post and translation authoring with change-aware draft/published lifecycle, metadata, tags, categories, links, media, and batch gallery-image import. +- Post and translation authoring with change-aware draft/published lifecycle, in-place published-frontmatter updates, metadata, tags, categories, links, media, and batch gallery-image import. - Media import, thumbnails, metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors. - WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping. - Post, Liquid template, and Lua script editing with dedicated syntax highlighting and explicit syntax-check feedback, including publish-time enforcement of the bDS2 Liquid tag/filter/operator subset, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms, including airplane-gated Git sync. diff --git a/crates/bds-core/src/engine/metadata_diff.rs b/crates/bds-core/src/engine/metadata_diff.rs index c34c6c7..d4d06e8 100644 --- a/crates/bds-core/src/engine/metadata_diff.rs +++ b/crates/bds-core/src/engine/metadata_diff.rs @@ -17,8 +17,7 @@ use crate::engine::{EngineError, EngineResult}; use crate::model::{Media, MediaTranslation, Post, PostTranslation, Script, Template}; use crate::util::frontmatter::{ ScriptFrontmatter, TemplateFrontmatter, read_post_file, read_script_file, read_template_file, - read_translation_file, write_post_file, write_script_file, write_template_file, - write_translation_file, + read_translation_file, write_script_file, write_template_file, write_translation_file, }; use crate::util::sidecar::{ MediaSidecar, MediaTranslationSidecar, read_sidecar, read_translation_sidecar, @@ -388,13 +387,7 @@ fn rewrite_post_from_database( entity_id: &str, ) -> EngineResult<()> { let post = qp::get_post_by_id(conn, entity_id)?; - let path = data_dir.join(&post.file_path); - let body = fs::read_to_string(&path) - .ok() - .and_then(|content| read_post_file(&content).ok().map(|(_, body)| body)) - .unwrap_or_default(); - atomic_write_str(&path, &write_post_file(&post, &body))?; - Ok(()) + crate::engine::post::rewrite_post_file_from_database(data_dir, &post) } fn rewrite_post_translation_from_database( diff --git a/crates/bds-core/src/engine/post.rs b/crates/bds-core/src/engine/post.rs index 51235dd..cdf307b 100644 --- a/crates/bds-core/src/engine/post.rs +++ b/crates/bds-core/src/engine/post.rs @@ -156,6 +156,10 @@ pub fn update_post( let reopen_published = published_metadata_changed || (post.status == PostStatus::Published && content.is_some_and(|value| published_body.as_deref() != Some(value))); + let rewrite_published_template = post.status == PostStatus::Published + && !reopen_published + && template_slug + .is_some_and(|value| value.is_some() && post.template_slug.as_deref() != value); if let Some(t) = title { post.title = t.to_string(); @@ -211,6 +215,10 @@ pub fn update_post( post.updated_at = now_unix_ms(); qp::update_post(conn, &post)?; + if rewrite_published_template { + rewrite_published_post(conn, data_dir, &post.id)?; + } + // Re-index FTS fts_index_post(conn, data_dir, &post)?; @@ -220,6 +228,32 @@ pub fn update_post( Ok(post) } +/// Rewrite a published post file from the current database frontmatter while +/// retaining the body that lives in the file. +pub fn rewrite_published_post( + conn: &Connection, + data_dir: &Path, + post_id: &str, +) -> EngineResult<()> { + let post = qp::get_post_by_id(conn, post_id)?; + if post.status == PostStatus::Published && !post.file_path.is_empty() { + rewrite_post_file_from_database(data_dir, &post)?; + } + Ok(()) +} + +pub(crate) fn rewrite_post_file_from_database(data_dir: &Path, post: &Post) -> EngineResult<()> { + let path = data_dir.join(&post.file_path); + let body = post.content.clone().unwrap_or_else(|| { + fs::read_to_string(&path) + .ok() + .and_then(|content| read_post_file(&content).ok().map(|(_, body)| body)) + .unwrap_or_default() + }); + atomic_write_str(&path, &write_post_file(post, &body))?; + Ok(()) +} + /// Publish a post: write file, clear content, set published_at. pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> EngineResult { let post = qp::get_post_by_id(conn, post_id)?; @@ -1472,7 +1506,11 @@ mod tests { #[test] fn published_template_slug_only_change_stays_published() { let (db, dir) = setup(); - let post = create_published_post(&db, &dir, "Published", "body"); + let mut post = create_published_post(&db, &dir, "Published", "body"); + post.checksum = Some("caller-checksum".into()); + qp::update_post(db.conn(), &post).unwrap(); + let post_path = dir.path().join(&post.file_path); + let original_file = fs::read_to_string(&post_path).unwrap(); let updated = update_post( db.conn(), @@ -1494,6 +1532,15 @@ mod tests { assert_eq!(updated.status, PostStatus::Published); assert_eq!(updated.template_slug.as_deref(), Some("page")); assert_eq!(updated.content, None); + assert_eq!(updated.published_at, post.published_at); + assert_eq!(updated.checksum.as_deref(), Some("caller-checksum")); + + let rewritten_file = fs::read_to_string(post_path).unwrap(); + assert_ne!(rewritten_file, original_file); + let (frontmatter, body) = read_post_file(&rewritten_file).unwrap(); + assert_eq!(frontmatter.template_slug.as_deref(), Some("page")); + assert_eq!(frontmatter.published_at, post.published_at); + assert_eq!(body, "body"); } #[test] diff --git a/specs/engine_side_effects.allium b/specs/engine_side_effects.allium index e9f212a..7e63c29 100644 --- a/specs/engine_side_effects.allium +++ b/specs/engine_side_effects.allium @@ -57,7 +57,9 @@ rule UpdatePostSideEffects { -- If post is published and content/metadata changes: -- auto-transition status back to draft -- If slug changed and file exists: rename .md file - -- If templateSlug changed on published post: rewrite .md frontmatter + -- If templateSlug changed on published post: atomically rewrite .md + -- frontmatter while preserving the file body, publishedAt, and the + -- caller-supplied post checksum. ensures: FTSIndexUpdated(post) if changes.content: ensures: PostLinksUpdated(post)