Refresh every stale post route during validation.

This commit is contained in:
2026-08-15 10:23:10 +02:00
parent 58596c8de0
commit 1f03d91c5f
9 changed files with 149 additions and 75 deletions

View File

@@ -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,24 +145,23 @@ 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('/')
);
if !expected.contains(&relative_path) || !actual.contains(&relative_path) {
continue;
}
let Some(output_modified) = modified_ms(&output_dir.join(&relative_path)) else {
continue;
};
let effective_generated = output_modified.max(
generated_at
.get(&relative_path)
.copied()
.unwrap_or_default(),
);
if source_modified > effective_generated + MTIME_GRANULARITY_TOLERANCE_MS {
stale.push(relative_path);
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;
}
let Some(output_modified) = modified_ms(&output_dir.join(&relative_path)) else {
continue;
};
let effective_generated = output_modified.max(
generated_at
.get(&relative_path)
.copied()
.unwrap_or_default(),
);
if source_modified > effective_generated + MTIME_GRANULARITY_TOLERANCE_MS {
stale.push(relative_path);
}
}
}
}

View File

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

View File

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

View File

@@ -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 {
language: language.clone(),
relative_path: format!("{}/index.html", url_path.trim_start_matches('/')),
url_path,
html: String::new(),
}));
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)
} else {
format!("/{language}/{}", record.post.slug)
},
GenerationSection::Core,
));
}
for (url_path, route_section) in post_paths {
for url_path in build_post_route_paths(&record.post, language, main_language) {
let route_section = if url_path == canonical_path {
GenerationSection::Single
} else {
GenerationSection::Core
};
let relative_path = format!("{}/index.html", url_path.trim_start_matches('/'));
artifacts.route_manifest.push(SitePage {
language: language.clone(),

View File

@@ -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,
&sections,
)
.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();