From b3c603bc91098dbb1ee9d81d710cfd802e18a110 Mon Sep 17 00:00:00 2001 From: sspaeti Date: Thu, 12 Feb 2026 20:38:42 +0100 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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 -