From b3c603bc91098dbb1ee9d81d710cfd802e18a110 Mon Sep 17 00:00:00 2001 From: sspaeti Date: Thu, 12 Feb 2026 20:38:42 +0100 Subject: [PATCH 1/9] Add feature-gated frontmatter module for OG meta tags Adds a `frontmatter` feature flag that parses YAML frontmatter from chapter content and injects OG/Twitter meta tag data into the Handlebars template context. All custom code is isolated in a single module (frontmatter.rs) with 3 one-liner integration points behind #[cfg(feature = "frontmatter")] for easy future upgrades. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 20 ++++ Cargo.toml | 1 + crates/mdbook-html/Cargo.toml | 2 + crates/mdbook-html/src/frontmatter.rs | 101 ++++++++++++++++++ crates/mdbook-html/src/html/mod.rs | 6 +- .../src/html_handlebars/hbs_renderer.rs | 4 + crates/mdbook-html/src/lib.rs | 3 + 7 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 crates/mdbook-html/src/frontmatter.rs diff --git a/Cargo.lock b/Cargo.lock index 0b35d30c61..f405fb8bfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1035,6 +1035,7 @@ dependencies = [ "regex", "serde", "serde_json", + "serde_yaml", "sha2", "tempfile", "toml", @@ -1705,6 +1706,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2219,6 +2233,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "utf-8" version = "0.7.6" diff --git a/Cargo.toml b/Cargo.toml index 2d97562e42..20af504772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,7 @@ default = ["watch", "serve", "search"] watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff", "dep:walkdir"] serve = ["dep:futures-util", "dep:tokio", "dep:axum", "dep:tower-http"] search = ["mdbook-html/search"] +frontmatter = ["mdbook-html/frontmatter"] [[bin]] doc = false diff --git a/crates/mdbook-html/Cargo.toml b/crates/mdbook-html/Cargo.toml index 7ca8b08090..87cc3acf23 100644 --- a/crates/mdbook-html/Cargo.toml +++ b/crates/mdbook-html/Cargo.toml @@ -23,6 +23,7 @@ pulldown-cmark.workspace = true regex.workspace = true serde.workspace = true serde_json.workspace = true +serde_yaml = { version = "0.9", optional = true } sha2.workspace = true tracing.workspace = true @@ -35,3 +36,4 @@ workspace = true [features] search = ["dep:elasticlunr-rs"] +frontmatter = ["dep:serde_yaml"] diff --git a/crates/mdbook-html/src/frontmatter.rs b/crates/mdbook-html/src/frontmatter.rs new file mode 100644 index 0000000000..0a901f886b --- /dev/null +++ b/crates/mdbook-html/src/frontmatter.rs @@ -0,0 +1,101 @@ +//! Frontmatter parsing support for mdBook. +//! +//! Extracts YAML frontmatter from markdown content and injects +//! Open Graph / Twitter Card metadata into the Handlebars template context. + +use serde::Deserialize; +use serde_json::json; + +/// Parsed YAML frontmatter fields. +#[derive(Deserialize, Debug)] +pub(crate) struct FrontMatter { + /// Page title for OG/Twitter meta tags. + pub title: String, + /// Page description for OG/Twitter meta tags. + pub description: String, + /// Featured image URL for OG/Twitter meta tags. + pub featured_image_url: String, +} + +/// Strips YAML frontmatter (between `---` markers) from content, +/// returning the content without the frontmatter block. +pub(crate) fn strip_frontmatter(content: &str) -> String { + let trimmed = content.trim_start(); + if !trimmed.starts_with("---") { + return content.to_string(); + } + // Find the closing `---` after the opening one + let after_open = &trimmed[3..]; + if let Some(end) = after_open.find("\n---") { + // Skip past the closing `---` and any trailing newline + let rest = &after_open[end + 4..]; + rest.trim_start_matches('\n').to_string() + } else { + content.to_string() + } +} + +/// Parses YAML frontmatter from content and injects OG metadata +/// into the Handlebars template context data map. +pub(crate) fn inject_frontmatter_data( + content: &str, + data: &mut serde_json::Map, +) { + let trimmed = content.trim_start(); + if !trimmed.starts_with("---") { + return; + } + let after_open = &trimmed[3..]; + let Some(end) = after_open.find("\n---") else { + return; + }; + let yaml_str = &after_open[..end]; + + match serde_yaml::from_str::(yaml_str) { + Ok(fm) => { + data.insert("is_frontmatter".to_owned(), json!(true)); + data.insert("og_title".to_owned(), json!(fm.title)); + data.insert("og_description".to_owned(), json!(fm.description)); + data.insert("og_image_url".to_owned(), json!(fm.featured_image_url)); + } + Err(e) => { + eprintln!("Frontmatter: deserialization error: {e:?}"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strip_frontmatter() { + let input = "---\ntitle: \"Hello\"\n---\n# Content"; + assert_eq!(strip_frontmatter(input), "# Content"); + } + + #[test] + fn test_strip_no_frontmatter() { + let input = "# Just content"; + assert_eq!(strip_frontmatter(input), "# Just content"); + } + + #[test] + fn test_inject_frontmatter_data() { + let input = "---\ntitle: \"My Title\"\ndescription: \"My Desc\"\nfeatured_image_url: \"https://example.com/img.png\"\n---\n# Content"; + let mut data = serde_json::Map::new(); + inject_frontmatter_data(input, &mut data); + assert_eq!(data["is_frontmatter"], json!(true)); + assert_eq!(data["og_title"], json!("My Title")); + assert_eq!(data["og_description"], json!("My Desc")); + assert_eq!(data["og_image_url"], json!("https://example.com/img.png")); + } + + #[test] + fn test_inject_no_frontmatter() { + let input = "# Just content"; + let mut data = serde_json::Map::new(); + inject_frontmatter_data(input, &mut data); + assert!(!data.contains_key("is_frontmatter")); + } +} diff --git a/crates/mdbook-html/src/html/mod.rs b/crates/mdbook-html/src/html/mod.rs index 8a70700f7e..626ffeeeb6 100644 --- a/crates/mdbook-html/src/html/mod.rs +++ b/crates/mdbook-html/src/html/mod.rs @@ -96,7 +96,11 @@ pub(crate) fn build_trees<'book>( let path = ch.path.as_ref().unwrap(); let html_path = ch.path.as_ref().unwrap().with_extension("html"); let options = HtmlRenderOptions::new(path, html_config, edition); - let tree = build_tree(&ch.content, &options); + #[cfg(feature = "frontmatter")] + let chapter_content = crate::frontmatter::strip_frontmatter(&ch.content); + #[cfg(not(feature = "frontmatter"))] + let chapter_content = ch.content.clone(); + let tree = build_tree(&chapter_content, &options); ChapterTree { chapter: ch, diff --git a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs index 8edac3cace..ef39aef145 100644 --- a/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs +++ b/crates/mdbook-html/src/html_handlebars/hbs_renderer.rs @@ -115,6 +115,10 @@ impl HtmlHandlebars { nav("previous", prev_ch); nav("next", next_ch); + // Inject frontmatter OG metadata into template context + #[cfg(feature = "frontmatter")] + crate::frontmatter::inject_frontmatter_data(&ch.content, &mut ctx.data); + // Render the handlebars template with the data debug!("Render template"); let rendered = ctx.handlebars.render("index", &ctx.data)?; diff --git a/crates/mdbook-html/src/lib.rs b/crates/mdbook-html/src/lib.rs index 9bb7f00851..d012d395dc 100644 --- a/crates/mdbook-html/src/lib.rs +++ b/crates/mdbook-html/src/lib.rs @@ -5,4 +5,7 @@ mod html_handlebars; pub mod theme; pub(crate) mod utils; +#[cfg(feature = "frontmatter")] +pub(crate) mod frontmatter; + pub use html_handlebars::HtmlHandlebars; From c749ea4240257969765e9c3a275074101890dbc1 Mon Sep 17 00:00:00 2001 From: sspaeti Date: Thu, 12 Feb 2026 21:00:23 +0100 Subject: [PATCH 2/9] add Makefile back --- Makefile | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..94b6e8924e --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +.DEFAULT_GOAL := build + +build: + cargo build --release --features frontmatter + cp target/release/mdbook ~/.local/bin/mdbook-released + +debug: + cargo build --features frontmatter + cp target/debug/mdbook ~/.local/bin/mdbook-debug + mdbook-debug serve ~/git/book/dedp/ -p 3333 + From c3e8b009f4d782ffd4d6c9d5bc08f31862eb3d4e Mon Sep 17 00:00:00 2001 From: sspaeti Date: Thu, 12 Feb 2026 22:16:57 +0100 Subject: [PATCH 3/9] update serde_yml --- Cargo.lock | 28 ++++++++++++++++----------- Makefile | 3 +++ crates/mdbook-html/Cargo.toml | 4 ++-- crates/mdbook-html/src/frontmatter.rs | 2 +- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f405fb8bfa..dee7f85e5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -862,6 +862,16 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1035,7 +1045,7 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_yaml", + "serde_yml", "sha2", "tempfile", "toml", @@ -1707,16 +1717,18 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "serde_yml" +version = "0.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" dependencies = [ "indexmap", "itoa", + "libyml", + "memchr", "ryu", "serde", - "unsafe-libyaml", + "version_check", ] [[package]] @@ -2233,12 +2245,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "utf-8" version = "0.7.6" diff --git a/Makefile b/Makefile index 94b6e8924e..7c7b47b25c 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,9 @@ build: cargo build --release --features frontmatter cp target/release/mdbook ~/.local/bin/mdbook-released +test: + cargo test --features frontmatter -p mdbook-html -- frontmatter + debug: cargo build --features frontmatter cp target/debug/mdbook ~/.local/bin/mdbook-debug diff --git a/crates/mdbook-html/Cargo.toml b/crates/mdbook-html/Cargo.toml index 87cc3acf23..931734d3bc 100644 --- a/crates/mdbook-html/Cargo.toml +++ b/crates/mdbook-html/Cargo.toml @@ -23,7 +23,7 @@ pulldown-cmark.workspace = true regex.workspace = true serde.workspace = true serde_json.workspace = true -serde_yaml = { version = "0.9", optional = true } +serde_yml = { version = "0.0.12", optional = true } sha2.workspace = true tracing.workspace = true @@ -36,4 +36,4 @@ workspace = true [features] search = ["dep:elasticlunr-rs"] -frontmatter = ["dep:serde_yaml"] +frontmatter = ["dep:serde_yml"] diff --git a/crates/mdbook-html/src/frontmatter.rs b/crates/mdbook-html/src/frontmatter.rs index 0a901f886b..08b02e8eb7 100644 --- a/crates/mdbook-html/src/frontmatter.rs +++ b/crates/mdbook-html/src/frontmatter.rs @@ -51,7 +51,7 @@ pub(crate) fn inject_frontmatter_data( }; let yaml_str = &after_open[..end]; - match serde_yaml::from_str::(yaml_str) { + match serde_yml::from_str::(yaml_str) { Ok(fm) => { data.insert("is_frontmatter".to_owned(), json!(true)); data.insert("og_title".to_owned(), json!(fm.title)); From 89ac94b46988edde5f14fc24b40efa44232ff6fc Mon Sep 17 00:00:00 2001 From: sspaeti Date: Thu, 12 Feb 2026 22:18:53 +0100 Subject: [PATCH 4/9] remove personal makefile --- Makefile | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 7c7b47b25c..0000000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -.DEFAULT_GOAL := build - -build: - cargo build --release --features frontmatter - cp target/release/mdbook ~/.local/bin/mdbook-released - -test: - cargo test --features frontmatter -p mdbook-html -- frontmatter - -debug: - cargo build --features frontmatter - cp target/debug/mdbook ~/.local/bin/mdbook-debug - mdbook-debug serve ~/git/book/dedp/ -p 3333 - From 916cb5625a9528c1c79fccab49f09a725edead74 Mon Sep 17 00:00:00 2001 From: sspaeti Date: Fri, 27 Feb 2026 23:37:58 +0100 Subject: [PATCH 5/9] add created date and last updated date to front matter --- crates/mdbook-html/src/frontmatter.rs | 123 +++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 10 deletions(-) diff --git a/crates/mdbook-html/src/frontmatter.rs b/crates/mdbook-html/src/frontmatter.rs index 08b02e8eb7..6f34d2cf01 100644 --- a/crates/mdbook-html/src/frontmatter.rs +++ b/crates/mdbook-html/src/frontmatter.rs @@ -1,20 +1,57 @@ //! Frontmatter parsing support for mdBook. //! //! Extracts YAML frontmatter from markdown content and injects -//! Open Graph / Twitter Card metadata into the Handlebars template context. +//! Open Graph / Twitter Card metadata and date information +//! into the Handlebars template context. use serde::Deserialize; use serde_json::json; /// Parsed YAML frontmatter fields. -#[derive(Deserialize, Debug)] +/// All fields are optional so chapters can have any subset of metadata. +#[derive(Deserialize, Debug, Default)] +#[serde(default)] pub(crate) struct FrontMatter { /// Page title for OG/Twitter meta tags. - pub title: String, + pub title: Option, /// Page description for OG/Twitter meta tags. - pub description: String, + pub description: Option, /// Featured image URL for OG/Twitter meta tags. - pub featured_image_url: String, + pub featured_image_url: Option, + /// Creation date in YYYY-MM-DD format. + pub createddate: Option, + /// Last modified date in YYYY-MM-DD format. + pub lastmod: Option, +} + +/// Formats a "YYYY-MM-DD" date string into human-readable "Mon DD, YYYY" format. +/// Returns None if the date string is malformed. +fn format_display_date(date_str: &str) -> Option { + let parts: Vec<&str> = date_str.split('-').collect(); + if parts.len() != 3 { + return None; + } + let year = parts[0]; + let month: u32 = parts[1].parse().ok()?; + let day: u32 = parts[2].parse().ok()?; + + let month_abbr = match month { + 1 => "Jan", + 2 => "Feb", + 3 => "Mar", + 4 => "Apr", + 5 => "May", + 6 => "Jun", + 7 => "Jul", + 8 => "Aug", + 9 => "Sep", + 10 => "Oct", + 11 => "Nov", + 12 => "Dec", + _ => return None, + }; + + Some(format!("{} {}, {}", month_abbr, day, year)) } /// Strips YAML frontmatter (between `---` markers) from content, @@ -35,7 +72,7 @@ pub(crate) fn strip_frontmatter(content: &str) -> String { } } -/// Parses YAML frontmatter from content and injects OG metadata +/// Parses YAML frontmatter from content and injects metadata /// into the Handlebars template context data map. pub(crate) fn inject_frontmatter_data( content: &str, @@ -53,10 +90,40 @@ pub(crate) fn inject_frontmatter_data( match serde_yml::from_str::(yaml_str) { Ok(fm) => { - data.insert("is_frontmatter".to_owned(), json!(true)); - data.insert("og_title".to_owned(), json!(fm.title)); - data.insert("og_description".to_owned(), json!(fm.description)); - data.insert("og_image_url".to_owned(), json!(fm.featured_image_url)); + // OG metadata — only set is_frontmatter when OG fields are present + let has_og = fm.title.is_some() + || fm.description.is_some() + || fm.featured_image_url.is_some(); + if has_og { + data.insert("is_frontmatter".to_owned(), json!(true)); + } + if let Some(ref title) = fm.title { + data.insert("og_title".to_owned(), json!(title)); + } + if let Some(ref desc) = fm.description { + data.insert("og_description".to_owned(), json!(desc)); + } + if let Some(ref img) = fm.featured_image_url { + data.insert("og_image_url".to_owned(), json!(img)); + } + + // Date metadata + let has_dates = fm.createddate.is_some() || fm.lastmod.is_some(); + if has_dates { + data.insert("has_dates".to_owned(), json!(true)); + } + if let Some(ref date) = fm.createddate { + data.insert("createddate".to_owned(), json!(date)); + if let Some(display) = format_display_date(date) { + data.insert("createddate_display".to_owned(), json!(display)); + } + } + if let Some(ref date) = fm.lastmod { + data.insert("lastmod".to_owned(), json!(date)); + if let Some(display) = format_display_date(date) { + data.insert("lastmod_display".to_owned(), json!(display)); + } + } } Err(e) => { eprintln!("Frontmatter: deserialization error: {e:?}"); @@ -98,4 +165,40 @@ mod tests { inject_frontmatter_data(input, &mut data); assert!(!data.contains_key("is_frontmatter")); } + + #[test] + fn test_inject_dates_only() { + let input = "---\ncreateddate: \"2024-03-15\"\nlastmod: \"2025-09-12\"\n---\n# Content"; + let mut data = serde_json::Map::new(); + inject_frontmatter_data(input, &mut data); + // No OG fields -> is_frontmatter should NOT be set + assert!(!data.contains_key("is_frontmatter")); + // Dates should be set + assert_eq!(data["has_dates"], json!(true)); + assert_eq!(data["createddate"], json!("2024-03-15")); + assert_eq!(data["lastmod"], json!("2025-09-12")); + assert_eq!(data["createddate_display"], json!("Mar 15, 2024")); + assert_eq!(data["lastmod_display"], json!("Sep 12, 2025")); + } + + #[test] + fn test_inject_full_frontmatter_with_dates() { + let input = "---\ntitle: \"My Title\"\ndescription: \"Desc\"\nfeatured_image_url: \"https://img.png\"\ncreateddate: \"2023-01-01\"\nlastmod: \"2025-12-25\"\n---\n# Content"; + let mut data = serde_json::Map::new(); + inject_frontmatter_data(input, &mut data); + assert_eq!(data["is_frontmatter"], json!(true)); + assert_eq!(data["og_title"], json!("My Title")); + assert_eq!(data["has_dates"], json!(true)); + assert_eq!(data["createddate_display"], json!("Jan 1, 2023")); + assert_eq!(data["lastmod_display"], json!("Dec 25, 2025")); + } + + #[test] + fn test_format_display_date() { + assert_eq!(format_display_date("2025-09-12"), Some("Sep 12, 2025".to_string())); + assert_eq!(format_display_date("2023-01-01"), Some("Jan 1, 2023".to_string())); + assert_eq!(format_display_date("2024-12-25"), Some("Dec 25, 2024".to_string())); + assert_eq!(format_display_date("invalid"), None); + assert_eq!(format_display_date("2025-13-01"), None); + } } From 0a193ef07194493c419210412d559eca9b214e72 Mon Sep 17 00:00:00 2001 From: sspaeti Date: Fri, 27 Feb 2026 23:40:59 +0100 Subject: [PATCH 6/9] Revert "add created date and last updated date to front matter" This reverts commit 916cb5625a9528c1c79fccab49f09a725edead74. --- crates/mdbook-html/src/frontmatter.rs | 123 +++----------------------- 1 file changed, 10 insertions(+), 113 deletions(-) diff --git a/crates/mdbook-html/src/frontmatter.rs b/crates/mdbook-html/src/frontmatter.rs index 6f34d2cf01..08b02e8eb7 100644 --- a/crates/mdbook-html/src/frontmatter.rs +++ b/crates/mdbook-html/src/frontmatter.rs @@ -1,57 +1,20 @@ //! Frontmatter parsing support for mdBook. //! //! Extracts YAML frontmatter from markdown content and injects -//! Open Graph / Twitter Card metadata and date information -//! into the Handlebars template context. +//! Open Graph / Twitter Card metadata into the Handlebars template context. use serde::Deserialize; use serde_json::json; /// Parsed YAML frontmatter fields. -/// All fields are optional so chapters can have any subset of metadata. -#[derive(Deserialize, Debug, Default)] -#[serde(default)] +#[derive(Deserialize, Debug)] pub(crate) struct FrontMatter { /// Page title for OG/Twitter meta tags. - pub title: Option, + pub title: String, /// Page description for OG/Twitter meta tags. - pub description: Option, + pub description: String, /// Featured image URL for OG/Twitter meta tags. - pub featured_image_url: Option, - /// Creation date in YYYY-MM-DD format. - pub createddate: Option, - /// Last modified date in YYYY-MM-DD format. - pub lastmod: Option, -} - -/// Formats a "YYYY-MM-DD" date string into human-readable "Mon DD, YYYY" format. -/// Returns None if the date string is malformed. -fn format_display_date(date_str: &str) -> Option { - let parts: Vec<&str> = date_str.split('-').collect(); - if parts.len() != 3 { - return None; - } - let year = parts[0]; - let month: u32 = parts[1].parse().ok()?; - let day: u32 = parts[2].parse().ok()?; - - let month_abbr = match month { - 1 => "Jan", - 2 => "Feb", - 3 => "Mar", - 4 => "Apr", - 5 => "May", - 6 => "Jun", - 7 => "Jul", - 8 => "Aug", - 9 => "Sep", - 10 => "Oct", - 11 => "Nov", - 12 => "Dec", - _ => return None, - }; - - Some(format!("{} {}, {}", month_abbr, day, year)) + pub featured_image_url: String, } /// Strips YAML frontmatter (between `---` markers) from content, @@ -72,7 +35,7 @@ pub(crate) fn strip_frontmatter(content: &str) -> String { } } -/// Parses YAML frontmatter from content and injects metadata +/// Parses YAML frontmatter from content and injects OG metadata /// into the Handlebars template context data map. pub(crate) fn inject_frontmatter_data( content: &str, @@ -90,40 +53,10 @@ pub(crate) fn inject_frontmatter_data( match serde_yml::from_str::(yaml_str) { Ok(fm) => { - // OG metadata — only set is_frontmatter when OG fields are present - let has_og = fm.title.is_some() - || fm.description.is_some() - || fm.featured_image_url.is_some(); - if has_og { - data.insert("is_frontmatter".to_owned(), json!(true)); - } - if let Some(ref title) = fm.title { - data.insert("og_title".to_owned(), json!(title)); - } - if let Some(ref desc) = fm.description { - data.insert("og_description".to_owned(), json!(desc)); - } - if let Some(ref img) = fm.featured_image_url { - data.insert("og_image_url".to_owned(), json!(img)); - } - - // Date metadata - let has_dates = fm.createddate.is_some() || fm.lastmod.is_some(); - if has_dates { - data.insert("has_dates".to_owned(), json!(true)); - } - if let Some(ref date) = fm.createddate { - data.insert("createddate".to_owned(), json!(date)); - if let Some(display) = format_display_date(date) { - data.insert("createddate_display".to_owned(), json!(display)); - } - } - if let Some(ref date) = fm.lastmod { - data.insert("lastmod".to_owned(), json!(date)); - if let Some(display) = format_display_date(date) { - data.insert("lastmod_display".to_owned(), json!(display)); - } - } + data.insert("is_frontmatter".to_owned(), json!(true)); + data.insert("og_title".to_owned(), json!(fm.title)); + data.insert("og_description".to_owned(), json!(fm.description)); + data.insert("og_image_url".to_owned(), json!(fm.featured_image_url)); } Err(e) => { eprintln!("Frontmatter: deserialization error: {e:?}"); @@ -165,40 +98,4 @@ mod tests { inject_frontmatter_data(input, &mut data); assert!(!data.contains_key("is_frontmatter")); } - - #[test] - fn test_inject_dates_only() { - let input = "---\ncreateddate: \"2024-03-15\"\nlastmod: \"2025-09-12\"\n---\n# Content"; - let mut data = serde_json::Map::new(); - inject_frontmatter_data(input, &mut data); - // No OG fields -> is_frontmatter should NOT be set - assert!(!data.contains_key("is_frontmatter")); - // Dates should be set - assert_eq!(data["has_dates"], json!(true)); - assert_eq!(data["createddate"], json!("2024-03-15")); - assert_eq!(data["lastmod"], json!("2025-09-12")); - assert_eq!(data["createddate_display"], json!("Mar 15, 2024")); - assert_eq!(data["lastmod_display"], json!("Sep 12, 2025")); - } - - #[test] - fn test_inject_full_frontmatter_with_dates() { - let input = "---\ntitle: \"My Title\"\ndescription: \"Desc\"\nfeatured_image_url: \"https://img.png\"\ncreateddate: \"2023-01-01\"\nlastmod: \"2025-12-25\"\n---\n# Content"; - let mut data = serde_json::Map::new(); - inject_frontmatter_data(input, &mut data); - assert_eq!(data["is_frontmatter"], json!(true)); - assert_eq!(data["og_title"], json!("My Title")); - assert_eq!(data["has_dates"], json!(true)); - assert_eq!(data["createddate_display"], json!("Jan 1, 2023")); - assert_eq!(data["lastmod_display"], json!("Dec 25, 2025")); - } - - #[test] - fn test_format_display_date() { - assert_eq!(format_display_date("2025-09-12"), Some("Sep 12, 2025".to_string())); - assert_eq!(format_display_date("2023-01-01"), Some("Jan 1, 2023".to_string())); - assert_eq!(format_display_date("2024-12-25"), Some("Dec 25, 2024".to_string())); - assert_eq!(format_display_date("invalid"), None); - assert_eq!(format_display_date("2025-13-01"), None); - } } From e944348ad066c527a889ddf7279ec30dcf8c86dc Mon Sep 17 00:00:00 2001 From: sspaeti Date: Mon, 6 Jul 2026 09:55:41 +0200 Subject: [PATCH 7/9] adding open Graph meta tags to templage index file --- .../mdbook-html/front-end/templates/index.hbs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/mdbook-html/front-end/templates/index.hbs b/crates/mdbook-html/front-end/templates/index.hbs index b1834189f9..35098a1fb9 100644 --- a/crates/mdbook-html/front-end/templates/index.hbs +++ b/crates/mdbook-html/front-end/templates/index.hbs @@ -15,10 +15,34 @@ {{> head}} - + + {{#if is_frontmatter}} + + + + + {{else}} + + + + + {{/if}} + {{#if og_image_url}} + + + {{else}} + + + {{/if}} + + + + + + {{#if favicon_svg}} {{/if}} From 42273c4a5eeb7df070531a93ea1753c1d42d7ef0 Mon Sep 17 00:00:00 2001 From: sspaeti Date: Mon, 6 Jul 2026 10:09:52 +0200 Subject: [PATCH 8/9] merge master --- Cargo.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index fbe0cd9ad8..fcc04dfd76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1084,7 +1084,8 @@ dependencies = [ "regex", "serde", "serde_json", - "sha2", + "serde_yml", + "sha2 0.11.0", "tempfile", "toml", "tracing", From 446612a5546f52853815ff532b01d61c3e20e6e7 Mon Sep 17 00:00:00 2001 From: sspaeti Date: Mon, 6 Jul 2026 10:17:50 +0200 Subject: [PATCH 9/9] fix: test error with property="og:description" --- tests/testsuite/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs index 641243c103..af40370afd 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -279,7 +279,7 @@ fn env_entire_book_table() { .check_file_contains("book/index.html", "Chapter 1") .check_file_contains( "book/index.html", - r#""#, + r#""#, ); }