Rewrite published posts after template changes.
This commit is contained in:
@@ -7,7 +7,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
|||||||
## Available Features
|
## 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ use crate::engine::{EngineError, EngineResult};
|
|||||||
use crate::model::{Media, MediaTranslation, Post, PostTranslation, Script, Template};
|
use crate::model::{Media, MediaTranslation, Post, PostTranslation, Script, Template};
|
||||||
use crate::util::frontmatter::{
|
use crate::util::frontmatter::{
|
||||||
ScriptFrontmatter, TemplateFrontmatter, read_post_file, read_script_file, read_template_file,
|
ScriptFrontmatter, TemplateFrontmatter, read_post_file, read_script_file, read_template_file,
|
||||||
read_translation_file, write_post_file, write_script_file, write_template_file,
|
read_translation_file, write_script_file, write_template_file, write_translation_file,
|
||||||
write_translation_file,
|
|
||||||
};
|
};
|
||||||
use crate::util::sidecar::{
|
use crate::util::sidecar::{
|
||||||
MediaSidecar, MediaTranslationSidecar, read_sidecar, read_translation_sidecar,
|
MediaSidecar, MediaTranslationSidecar, read_sidecar, read_translation_sidecar,
|
||||||
@@ -388,13 +387,7 @@ fn rewrite_post_from_database(
|
|||||||
entity_id: &str,
|
entity_id: &str,
|
||||||
) -> EngineResult<()> {
|
) -> EngineResult<()> {
|
||||||
let post = qp::get_post_by_id(conn, entity_id)?;
|
let post = qp::get_post_by_id(conn, entity_id)?;
|
||||||
let path = data_dir.join(&post.file_path);
|
crate::engine::post::rewrite_post_file_from_database(data_dir, &post)
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rewrite_post_translation_from_database(
|
fn rewrite_post_translation_from_database(
|
||||||
|
|||||||
@@ -156,6 +156,10 @@ pub fn update_post(
|
|||||||
let reopen_published = published_metadata_changed
|
let reopen_published = published_metadata_changed
|
||||||
|| (post.status == PostStatus::Published
|
|| (post.status == PostStatus::Published
|
||||||
&& content.is_some_and(|value| published_body.as_deref() != Some(value)));
|
&& 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 {
|
if let Some(t) = title {
|
||||||
post.title = t.to_string();
|
post.title = t.to_string();
|
||||||
@@ -211,6 +215,10 @@ pub fn update_post(
|
|||||||
post.updated_at = now_unix_ms();
|
post.updated_at = now_unix_ms();
|
||||||
qp::update_post(conn, &post)?;
|
qp::update_post(conn, &post)?;
|
||||||
|
|
||||||
|
if rewrite_published_template {
|
||||||
|
rewrite_published_post(conn, data_dir, &post.id)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-index FTS
|
// Re-index FTS
|
||||||
fts_index_post(conn, data_dir, &post)?;
|
fts_index_post(conn, data_dir, &post)?;
|
||||||
|
|
||||||
@@ -220,6 +228,32 @@ pub fn update_post(
|
|||||||
Ok(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.
|
/// Publish a post: write file, clear content, set published_at.
|
||||||
pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> EngineResult<Post> {
|
pub fn publish_post(conn: &Connection, data_dir: &Path, post_id: &str) -> EngineResult<Post> {
|
||||||
let post = qp::get_post_by_id(conn, post_id)?;
|
let post = qp::get_post_by_id(conn, post_id)?;
|
||||||
@@ -1472,7 +1506,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn published_template_slug_only_change_stays_published() {
|
fn published_template_slug_only_change_stays_published() {
|
||||||
let (db, dir) = setup();
|
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(
|
let updated = update_post(
|
||||||
db.conn(),
|
db.conn(),
|
||||||
@@ -1494,6 +1532,15 @@ mod tests {
|
|||||||
assert_eq!(updated.status, PostStatus::Published);
|
assert_eq!(updated.status, PostStatus::Published);
|
||||||
assert_eq!(updated.template_slug.as_deref(), Some("page"));
|
assert_eq!(updated.template_slug.as_deref(), Some("page"));
|
||||||
assert_eq!(updated.content, None);
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ rule UpdatePostSideEffects {
|
|||||||
-- If post is published and content/metadata changes:
|
-- If post is published and content/metadata changes:
|
||||||
-- auto-transition status back to draft
|
-- auto-transition status back to draft
|
||||||
-- If slug changed and file exists: rename .md file
|
-- 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)
|
ensures: FTSIndexUpdated(post)
|
||||||
if changes.content:
|
if changes.content:
|
||||||
ensures: PostLinksUpdated(post)
|
ensures: PostLinksUpdated(post)
|
||||||
|
|||||||
Reference in New Issue
Block a user