Rewrite published posts after template changes.

This commit is contained in:
2026-07-21 21:32:28 +02:00
parent 80f291b8ea
commit 1e32c7e945
4 changed files with 54 additions and 12 deletions

View File

@@ -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.

View File

@@ -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(

View File

@@ -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<Post> {
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]

View File

@@ -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)