From 3e12466c5790d61d5c5d761ccef7bd3271aede85 Mon Sep 17 00:00:00 2001 From: Hampus Date: Sun, 16 Aug 2026 15:02:16 +0200 Subject: [PATCH] fix(unfurl): restore document title fallback for link embeds (#1673) --- fluxer_unfurl/src/html_parser.rs | 43 ++++++++++++++++--- .../src/resolvers/default_resolver.rs | 6 ++- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/fluxer_unfurl/src/html_parser.rs b/fluxer_unfurl/src/html_parser.rs index 2ace67ea1..b90cbd0dc 100644 --- a/fluxer_unfurl/src/html_parser.rs +++ b/fluxer_unfurl/src/html_parser.rs @@ -135,7 +135,10 @@ pub fn parse_opengraph(html: &str) -> OgMetadata { }; if og.title.is_none() { - og.title = meta.first("twitter:title"); + og.title = meta + .first("twitter:title") + .or_else(|| document_title(&doc)) + .or_else(|| meta.first("title")); } if og.description.is_none() { @@ -145,6 +148,16 @@ pub fn parse_opengraph(html: &str) -> OgMetadata { og } +fn document_title(doc: &Html) -> Option { + let selector = Selector::parse("title").ok()?; + let text = doc.select(&selector).next()?.text().collect::(); + let trimmed = text.trim(); + if trimmed.is_empty() { + return None; + } + Some(trimmed.to_owned()) +} + #[allow(dead_code)] pub fn parse_twitter_card(html: &str) -> TwitterCardMetadata { let doc = Html::parse_document(html); @@ -335,7 +348,7 @@ mod tests { #[test] fn handles_missing_tags() { let m = og("X"); - assert!(m.title.is_none()); + assert_eq!(m.title.as_deref(), Some("X")); assert!(m.description.is_none() && m.image.is_none() && m.url.is_none()); } @@ -380,18 +393,34 @@ mod tests { } #[test] - fn title_uses_open_graph_then_twitter_only() { + fn title_fallback_chain_matches_ts() { let m = og(r#""#); assert_eq!(m.title.as_deref(), Some("OG")); let m = og(r#""#); assert_eq!(m.title.as_deref(), Some("TW")); + let m = og(r#"HTML Title"#); + assert_eq!(m.title.as_deref(), Some("HTML Title")); + let m = og(r#""#); + assert_eq!(m.title.as_deref(), Some("Meta")); } #[test] - fn title_never_falls_back_to_document_title() { - let m = og(r#"HTML Title"#); - assert!(m.title.is_none()); - let m = og(r#""#); + fn title_prefers_open_graph_over_the_document_title() { + let m = og( + r#"HTML Title"#, + ); + assert_eq!(m.title.as_deref(), Some("OG")); + let m = og( + r#"HTML Title"#, + ); + assert_eq!(m.title.as_deref(), Some("TW")); + } + + #[test] + fn document_title_is_trimmed_and_blank_is_ignored() { + let m = og(r#" Hello World "#); + assert_eq!(m.title.as_deref(), Some("Hello World")); + let m = og(r#" "#); assert!(m.title.is_none()); } diff --git a/fluxer_unfurl/src/resolvers/default_resolver.rs b/fluxer_unfurl/src/resolvers/default_resolver.rs index 23c6bb612..236aaff5c 100644 --- a/fluxer_unfurl/src/resolvers/default_resolver.rs +++ b/fluxer_unfurl/src/resolvers/default_resolver.rs @@ -197,7 +197,11 @@ async fn resolve_html(ctx: &ResolveContext<'_>) -> anyhow::Result