diff --git a/crates/bds-core/src/engine/preview.rs b/crates/bds-core/src/engine/preview.rs index 472dfbd..95ea447 100644 --- a/crates/bds-core/src/engine/preview.rs +++ b/crates/bds-core/src/engine/preview.rs @@ -1218,6 +1218,105 @@ mod tests { assert!(pagefind_body.contains("window.pagefind")); } + #[test] + fn preview_rewrites_media_aliases_and_serves_the_canonical_file() { + let _guard = preview_port_guard().lock().unwrap(); + let (dir, db) = setup_preview_fixture(); + std::fs::create_dir_all(dir.path().join("media/2022/11")).unwrap(); + std::fs::write( + dir.path().join("media/2022/11/canonical-id.jpg"), + b"canonical image", + ) + .unwrap(); + queries::media::insert_media( + db.conn(), + &Media { + id: "canonical-id".into(), + project_id: "project-1".into(), + filename: "canonical-id.jpg".into(), + original_name: "20221111_0177.jpg".into(), + mime_type: "image/jpeg".into(), + size: 15, + width: Some(100), + height: Some(80), + title: None, + alt: None, + caption: None, + author: None, + language: None, + file_path: "media/2022/11/canonical-id.jpg".into(), + sidecar_path: "media/2022/11/canonical-id.jpg.meta".into(), + checksum: None, + tags: Vec::new(), + created_at: 1_668_124_800_000, + updated_at: 1_668_124_800_000, + }, + ) + .unwrap(); + let markdown = "[Open image](/media/2022/11/20221111_0177.jpg?download=1#preview)\n\n![Embedded image](bds-media://canonical-id)"; + let mut post = make_draft_post(); + post.content = Some(markdown.into()); + queries::post::insert_post(db.conn(), &post).unwrap(); + + let server = start_preview_server( + dir.path().join("bds.db"), + dir.path().to_path_buf(), + "project-1".into(), + ) + .unwrap(); + let client = reqwest::blocking::Client::new(); + let preview_html = client + .get(format!( + "http://{PREVIEW_HOST}:{PREVIEW_PORT}/__draft/post-1" + )) + .send() + .unwrap() + .text() + .unwrap(); + let image_response = client + .get(format!( + "http://{PREVIEW_HOST}:{PREVIEW_PORT}/media/2022/11/canonical-id.jpg?download=1" + )) + .send() + .unwrap(); + let image_status = image_response.status(); + let image_content_type = image_response + .headers() + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap() + .to_string(); + let image_body = image_response.bytes().unwrap(); + server.stop().unwrap(); + + let mut published_post = post; + published_post.status = PostStatus::Published; + let generated = crate::render::build_site_render_artifacts( + db.conn(), + dir.path(), + "project-1", + &make_metadata(), + &[(published_post, markdown.into())], + ) + .unwrap(); + let generated_html = &generated + .pages + .iter() + .find(|page| page.url_path == "/2024/03/09/hello") + .unwrap() + .html; + let expected = "href=\"/media/2022/11/canonical-id.jpg?download=1#preview\""; + + assert!(preview_html.contains(expected)); + assert!(generated_html.contains(expected)); + assert!(preview_html.contains("src=\"/media/2022/11/canonical-id.jpg\"")); + assert!(generated_html.contains("src=\"/media/2022/11/canonical-id.jpg\"")); + assert_eq!(image_status, StatusCode::OK); + assert_eq!(image_content_type, "image/jpeg"); + assert_eq!(image_body.as_ref(), b"canonical image"); + } + #[test] fn preview_server_serves_style_preview() { let _guard = preview_port_guard().lock().unwrap(); diff --git a/crates/bds-core/src/render/site.rs b/crates/bds-core/src/render/site.rs index 6c490cb..10f59ef 100644 --- a/crates/bds-core/src/render/site.rs +++ b/crates/bds-core/src/render/site.rs @@ -22,7 +22,7 @@ use crate::render::{ }; use crate::scripting::{CoreHost, HostApi, UnavailableHost}; use crate::util::frontmatter::{read_script_file, read_template_file, read_translation_file}; -use crate::util::slugify; +use crate::util::{slugify, year_month_from_unix_ms}; const STARTER_SINGLE_POST_TEMPLATE: &str = include_str!("../../../../assets/starter-templates/single-post.liquid"); @@ -187,6 +187,7 @@ fn build_site_render_artifacts_with_mode( .map(|media| (media.id.clone(), media)) .collect::>(); let project_media = media_items.iter().map(media_context).collect::>(); + let canonical_media_map = canonical_media_paths(&media_items); let project_tags = build_published_tag_counts(published_posts, &tags); let mut artifacts = SiteRenderArtifacts::default(); @@ -238,6 +239,7 @@ fn build_site_render_artifacts_with_mode( &category_settings, &menu_items, &post_data_json_by_id, + &canonical_media_map, &project_media, &project_tags, &bundle, @@ -331,6 +333,7 @@ fn build_site_render_artifacts_with_mode( &canonical_map, &menu_items, &post_data_json_by_id, + &canonical_media_map, &project_media, &project_tags, &bundle, @@ -805,6 +808,7 @@ fn render_list_route( category_settings: &HashMap, menu_items: &[Value], post_data_json_by_id: &HashMap, + canonical_media_path_by_source_path: &HashMap, project_media: &[Value], project_tags: &[Value], bundle: &TemplateBundle, @@ -849,7 +853,7 @@ fn render_list_route( "total_items": route.total_items, "items_per_page": route.items_per_page, "canonical_post_path_by_slug": canonical_map, - "canonical_media_path_by_source_path": canonical_media_paths(posts), + "canonical_media_path_by_source_path": canonical_media_path_by_source_path, "post_data_json_by_id": post_data_json_by_id, "project": { "media": project_media }, "Tags": project_tags, @@ -883,6 +887,7 @@ fn render_post_route( canonical_post_path_by_slug: &HashMap, menu_items: &[Value], post_data_json_by_id: &HashMap, + canonical_media_path_by_source_path: &HashMap, project_media: &[Value], project_tags: &[Value], bundle: &TemplateBundle, @@ -959,7 +964,7 @@ fn render_post_route( "tag_color_by_name": taxonomy_counts.2, "backlinks": backlinks, "canonical_post_path_by_slug": canonical_post_path_by_slug, - "canonical_media_path_by_source_path": canonical_media_paths(all_posts), + "canonical_media_path_by_source_path": canonical_media_path_by_source_path, "post_data_json_by_id": post_data_json_by_id, "project": { "media": project_media }, "Tags": project_tags, @@ -1229,31 +1234,22 @@ fn canonical_post_path_by_slug( .collect() } -fn canonical_media_paths(posts: &[RenderPostRecord]) -> HashMap { +fn canonical_media_paths(media_items: &[Media]) -> HashMap { let mut map = HashMap::new(); - for record in posts { - for media_reference in extract_media_refs(&record.body_markdown) { - map.entry(media_reference.clone()) - .or_insert_with(|| media_reference.clone()); + for media in media_items { + let target = canonical_media_path(media); + let (year, month) = year_month_from_unix_ms(media.created_at); + for source in [ + format!("media/{year}/{month}/{}", media.original_name).to_lowercase(), + format!("bds-media://{}", media.id), + media.file_path.trim_start_matches('/').to_lowercase(), + ] { + map.entry(source).or_insert_with(|| target.clone()); } } map } -fn extract_media_refs(markdown: &str) -> Vec { - let mut refs = Vec::new(); - let mut remainder = markdown; - while let Some(index) = remainder.find("bds-media://") { - let rest = &remainder[index..]; - let end = rest - .find(|ch: char| ch == ')' || ch == ']' || ch.is_whitespace()) - .unwrap_or(rest.len()); - refs.push(rest[..end].to_string()); - remainder = &rest[end..]; - } - refs -} - fn build_linked_media_by_post_id( conn: &Connection, posts: &[RenderPostRecord],