Refresh every stale post route during validation.
This commit is contained in:
@@ -29,7 +29,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
||||
- Persistent conversational AI with safe Markdown, streamed and cancellable responses, model/session/token tracking, bounded project-aware blog tools, and localized conversation management in the Chat workspace. Allowlisted render tools add persistent native cards, charts, forms, lists, metrics, mind maps, tables, and tabs without executing assistant-provided HTML or JavaScript.
|
||||
- SSH-agent-based SCP or rsync publishing.
|
||||
- Integrated Git workflow for each blog project's current repository, with repository initialization, read-only origin discovery, Git LFS image tracking, status and diffs, branch/file history with bDS2-compatible sync-status colors, commits, cancellable fetch/pull/push, and post-pull filesystem reconciliation; network actions respect airplane mode.
|
||||
- Site, media, and translation validation plus `ruds://new-post` Blogmark capture and Lua transforms; captures open directly in the post editor as soon as the draft exists without changing the current or restored sidebar state or waiting for on-device semantic indexing, and defer automatic translation until an explicit manual save, which queues one task per still-missing language. Rendering tasks keep long generated URLs compact in progress messages. Publishing never starts automatic translation. bDS2 keeps its separate `bds2://` bookmarklet protocol.
|
||||
- Site validation detects and repairs stale content at every generated post URL, alongside media and translation validation plus `ruds://new-post` Blogmark capture and Lua transforms; captures open directly in the post editor as soon as the draft exists without changing the current or restored sidebar state or waiting for on-device semantic indexing, and defer automatic translation until an explicit manual save, which queues one task per still-missing language. Rendering tasks keep long generated URLs compact in progress messages. Publishing never starts automatic translation. bDS2 keeps its separate `bds2://` bookmarklet protocol.
|
||||
|
||||
RuDS uses no JavaScript application runtime and loads no CSS or JavaScript from CDNs. The preview is served by the Rust application and displayed by the operating-system webview.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::db::queries;
|
||||
use crate::engine::generation::has_published_snapshot;
|
||||
use crate::engine::{EngineError, EngineResult};
|
||||
use crate::model::Post;
|
||||
use crate::render::{build_canonical_post_path, build_site_route_manifest};
|
||||
use crate::render::{build_post_route_paths, build_site_route_manifest};
|
||||
|
||||
const MTIME_GRANULARITY_TOLERANCE_MS: i64 = 1_000;
|
||||
|
||||
@@ -145,10 +145,8 @@ fn stale_post_paths(
|
||||
if language != main_language && post.do_not_translate {
|
||||
continue;
|
||||
}
|
||||
let relative_path = format!(
|
||||
"{}/index.html",
|
||||
build_canonical_post_path(post, language, main_language).trim_start_matches('/')
|
||||
);
|
||||
for url_path in build_post_route_paths(post, language, main_language) {
|
||||
let relative_path = format!("{}/index.html", url_path.trim_start_matches('/'));
|
||||
if !expected.contains(&relative_path) || !actual.contains(&relative_path) {
|
||||
continue;
|
||||
}
|
||||
@@ -166,6 +164,7 @@ fn stale_post_paths(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stale
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ pub use generation::{
|
||||
pub use markdown::render_markdown_to_html;
|
||||
pub(crate) use page_renderer::validate_liquid_template_syntax;
|
||||
pub use page_renderer::{RenderError, render_liquid_template};
|
||||
pub(crate) use routes::{PostLanguageVariant, blog_page_title, select_post_language_variant};
|
||||
pub(crate) use routes::{
|
||||
PostLanguageVariant, blog_page_title, build_post_route_paths, select_post_language_variant,
|
||||
};
|
||||
pub use routes::{
|
||||
RenderedPage, build_canonical_post_path, render_starter_list_page,
|
||||
render_starter_list_page_with_media_map, render_starter_single_post_page,
|
||||
|
||||
@@ -161,6 +161,22 @@ pub fn build_canonical_post_path(post: &Post, language: &str, main_language: &st
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_post_route_paths(
|
||||
post: &Post,
|
||||
language: &str,
|
||||
main_language: &str,
|
||||
) -> Vec<String> {
|
||||
let mut paths = vec![build_canonical_post_path(post, language, main_language)];
|
||||
if post.categories.iter().any(|category| category == "page") {
|
||||
paths.push(if language == main_language {
|
||||
format!("/{}", post.slug)
|
||||
} else {
|
||||
format!("/{language}/{}", post.slug)
|
||||
});
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
pub fn render_starter_single_post_page(
|
||||
post: &Post,
|
||||
body_markdown: &str,
|
||||
|
||||
@@ -19,7 +19,8 @@ use crate::model::{
|
||||
};
|
||||
use crate::render::{
|
||||
PostLanguageVariant, RenderCategorySettings, RenderTemplateLookup, blog_page_title,
|
||||
build_canonical_post_path, resolve_post_template, select_post_language_variant,
|
||||
build_canonical_post_path, build_post_route_paths, resolve_post_template,
|
||||
select_post_language_variant,
|
||||
};
|
||||
use crate::scripting::{CoreHost, HostApi, UnavailableHost};
|
||||
use crate::util::frontmatter::{read_script_file, read_template_file, read_translation_file};
|
||||
@@ -203,26 +204,16 @@ pub fn build_site_route_manifest(
|
||||
);
|
||||
|
||||
for record in posts {
|
||||
let canonical_path = build_canonical_post_path(&record.post, &language, &main_language);
|
||||
let mut paths = vec![canonical_path];
|
||||
if record
|
||||
.post
|
||||
.categories
|
||||
.iter()
|
||||
.any(|category| category == "page")
|
||||
{
|
||||
paths.push(if language == main_language {
|
||||
format!("/{}", record.post.slug)
|
||||
} else {
|
||||
format!("/{language}/{}", record.post.slug)
|
||||
});
|
||||
}
|
||||
manifest.extend(paths.into_iter().map(|url_path| SitePage {
|
||||
manifest.extend(
|
||||
build_post_route_paths(&record.post, &language, &main_language)
|
||||
.into_iter()
|
||||
.map(|url_path| SitePage {
|
||||
language: language.clone(),
|
||||
relative_path: format!("{}/index.html", url_path.trim_start_matches('/')),
|
||||
url_path,
|
||||
html: String::new(),
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,23 +512,12 @@ pub fn build_site_render_artifacts_from_context(
|
||||
let mut single_routes = Vec::new();
|
||||
for record in localized_posts {
|
||||
let canonical_path = build_canonical_post_path(&record.post, language, main_language);
|
||||
let mut post_paths = vec![(canonical_path, GenerationSection::Single)];
|
||||
if record
|
||||
.post
|
||||
.categories
|
||||
.iter()
|
||||
.any(|category| category == "page")
|
||||
{
|
||||
post_paths.push((
|
||||
if language == main_language {
|
||||
format!("/{}", record.post.slug)
|
||||
for url_path in build_post_route_paths(&record.post, language, main_language) {
|
||||
let route_section = if url_path == canonical_path {
|
||||
GenerationSection::Single
|
||||
} else {
|
||||
format!("/{language}/{}", record.post.slug)
|
||||
},
|
||||
GenerationSection::Core,
|
||||
));
|
||||
}
|
||||
for (url_path, route_section) in post_paths {
|
||||
GenerationSection::Core
|
||||
};
|
||||
let relative_path = format!("{}/index.html", url_path.trim_start_matches('/'));
|
||||
artifacts.route_manifest.push(SitePage {
|
||||
language: language.clone(),
|
||||
|
||||
@@ -1023,6 +1023,86 @@ fn site_validation_detects_post_sources_newer_than_generated_routes() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn site_validation_repairs_every_stale_route_for_page_posts() {
|
||||
use std::fs::{File, FileTimes};
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
let (db, dir) = setup();
|
||||
let metadata = make_metadata();
|
||||
let mut post = make_post("wiki", 1_710_000_000_000);
|
||||
post.categories = vec!["page".into(), "wiki".into()];
|
||||
write_published_snapshot(&dir, &mut post, "Old wiki body");
|
||||
bds_core::db::queries::post::insert_post(db.conn(), &post).unwrap();
|
||||
let old_source = PublishedPostSource {
|
||||
post: post.clone(),
|
||||
body_markdown: "Old wiki body".into(),
|
||||
};
|
||||
|
||||
generate_starter_site(db.conn(), dir.path(), "p1", &metadata, &[old_source], "en").unwrap();
|
||||
write_published_snapshot(&dir, &mut post, "Updated wiki body");
|
||||
|
||||
let canonical_path = "2024/03/09/wiki/index.html";
|
||||
let flat_path = "wiki/index.html";
|
||||
for path in [canonical_path, flat_path] {
|
||||
File::options()
|
||||
.write(true)
|
||||
.open(dir.path().join(path))
|
||||
.unwrap()
|
||||
.set_times(FileTimes::new().set_modified(UNIX_EPOCH + Duration::from_secs(10)))
|
||||
.unwrap();
|
||||
let mut hash = bds_core::db::queries::generated_file_hash::get_generated_file_hash(
|
||||
db.conn(),
|
||||
"p1",
|
||||
path,
|
||||
)
|
||||
.unwrap();
|
||||
hash.updated_at = 10_000;
|
||||
bds_core::db::queries::generated_file_hash::upsert_generated_file_hash(db.conn(), &hash)
|
||||
.unwrap();
|
||||
}
|
||||
File::options()
|
||||
.write(true)
|
||||
.open(dir.path().join(&post.file_path))
|
||||
.unwrap()
|
||||
.set_times(FileTimes::new().set_modified(UNIX_EPOCH + Duration::from_secs(20)))
|
||||
.unwrap();
|
||||
|
||||
let validation = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||
assert_eq!(
|
||||
validation.stale_pages,
|
||||
vec![canonical_path.to_string(), flat_path.to_string()]
|
||||
);
|
||||
|
||||
let updated_source = load_published_post_source(dir.path(), post)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let sections = sections_from_validation_report(&validation, &metadata);
|
||||
apply_validation_sections(
|
||||
db.conn(),
|
||||
dir.path(),
|
||||
"p1",
|
||||
&metadata,
|
||||
&[updated_source],
|
||||
&validation,
|
||||
§ions,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
std::fs::read_to_string(dir.path().join(canonical_path))
|
||||
.unwrap()
|
||||
.contains("Updated wiki body")
|
||||
);
|
||||
assert!(
|
||||
std::fs::read_to_string(dir.path().join(flat_path))
|
||||
.unwrap()
|
||||
.contains("Updated wiki body")
|
||||
);
|
||||
let repaired = validate_site(db.conn(), dir.path(), "p1").unwrap();
|
||||
assert!(repaired.stale_pages.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn site_validation_refreshes_sitemap_without_rendering_pages() {
|
||||
let (db, dir) = setup();
|
||||
|
||||
@@ -82,9 +82,8 @@ fn prepare_macos_x64_runtime_from_archive(
|
||||
for entry in archive.entries()? {
|
||||
let mut entry = entry?;
|
||||
let path = entry.path()?;
|
||||
let destination = if path.ends_with(format!(
|
||||
"lib/libonnxruntime.{MACOS_X64_ORT_VERSION}.dylib"
|
||||
)) {
|
||||
let destination =
|
||||
if path.ends_with(format!("lib/libonnxruntime.{MACOS_X64_ORT_VERSION}.dylib")) {
|
||||
Some(format!("libonnxruntime.{MACOS_X64_ORT_VERSION}.dylib"))
|
||||
} else if path.ends_with("LICENSE") {
|
||||
Some("ONNXRuntime-LICENSE.txt".to_owned())
|
||||
@@ -335,9 +334,7 @@ fn runtime_files(
|
||||
.filter_map(|name| name.into_string().ok())
|
||||
.filter(|name| name.starts_with("libonnxruntime") && name.contains(".dylib"))
|
||||
.collect::<Vec<_>>();
|
||||
let has_versioned = names
|
||||
.iter()
|
||||
.any(|name| name != "libonnxruntime.dylib");
|
||||
let has_versioned = names.iter().any(|name| name != "libonnxruntime.dylib");
|
||||
if has_versioned {
|
||||
names.retain(|name| name != "libonnxruntime.dylib");
|
||||
}
|
||||
|
||||
@@ -260,8 +260,7 @@ fn tagged_releases_are_built_and_packaged_with_rust_tools() {
|
||||
let workspace_manifest = fs::read_to_string(workspace.join("Cargo.toml")).unwrap();
|
||||
let core_manifest = fs::read_to_string(workspace.join("crates/bds-core/Cargo.toml")).unwrap();
|
||||
assert!(
|
||||
workspace_manifest
|
||||
.contains("pagefind = { version = \"1.5.2\", default-features = false }"),
|
||||
workspace_manifest.contains("pagefind = { version = \"1.5.2\", default-features = false }"),
|
||||
"Pagefind's unused Actix serving stack must stay disabled"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -296,8 +296,9 @@ rule ValidateSite {
|
||||
-- missing: expected routes with no non-empty index.html
|
||||
-- extra: index.html routes absent from the expected set, including
|
||||
-- zero-byte files
|
||||
-- stale: existing post routes whose source mtime is more than one second
|
||||
-- newer than both the output mtime and tracked generation time
|
||||
-- stale: every existing generated route for a post whose source mtime is
|
||||
-- more than one second newer than both the output mtime and
|
||||
-- tracked generation time, including canonical and flat page URLs
|
||||
-- Standalone HTML, XML, JSON, assets, Pagefind files, and content hashes are
|
||||
-- not validation comparison inputs.
|
||||
ensures: ValidationReport(missing_pages, extra_pages, stale_pages)
|
||||
|
||||
Reference in New Issue
Block a user