added favicon, improve layout

This commit is contained in:
2026-06-07 20:18:31 +02:00
parent b865fe982e
commit 39f08c7218
5 changed files with 86 additions and 7 deletions
+75 -3
View File
@@ -38,6 +38,28 @@ fn image_src_is_resolvable(element: &scraper::ElementRef) -> bool {
}
}
fn escape_html_attr(value: &str) -> String {
value
.replace('&', "&")
.replace('"', """)
.replace('<', "&lt;")
.replace('>', "&gt;")
}
// Some feeds (e.g. Deutsche Welle) don't embed an <img> in the item content at
// all — they carry the article image as an RSS <enclosure> instead. Build an
// <img> tag from it so those feeds get a preview image too.
fn enclosure_image_html(item: &Item) -> Option<String> {
let enclosure = item.enclosure()?;
if !enclosure.mime_type().to_lowercase().starts_with("image/") {
return None;
}
Some(format!(
r#"<img src="{}">"#,
escape_html_attr(enclosure.url())
))
}
fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) {
let item_title = item.title.clone().unwrap();
log::info!("Create feed item: {}", item_title);
@@ -48,9 +70,17 @@ fn create_feed_item(item: Item, feed: &Feed, connection: &mut PgConnection) {
let mut content = "".to_string();
let selector_img = Selector::parse("img").unwrap();
if let Some(image) = frag.select(&selector_img).find(image_src_is_resolvable) {
content.push_str(&image.html());
content.push_str("<br>");
match frag.select(&selector_img).find(image_src_is_resolvable) {
Some(image) => {
content.push_str(&image.html());
content.push_str("<br>");
}
None => {
if let Some(image_html) = enclosure_image_html(&item) {
content.push_str(&image_html);
content.push_str("<br>");
}
}
}
for node in frag.tree.nodes() {
@@ -176,6 +206,48 @@ mod tests {
assert!(html.select(&selector).find(image_src_is_resolvable).is_none());
}
#[test]
fn enclosure_image_html_builds_img_for_image_enclosures() {
let mut item = Item::default();
item.set_enclosure(rss::Enclosure {
url: "https://static.dw.com/image/73880499_302.jpg".to_string(),
length: "2000".to_string(),
mime_type: "image/jpeg".to_string(),
});
assert_eq!(
Some(r#"<img src="https://static.dw.com/image/73880499_302.jpg">"#.to_string()),
enclosure_image_html(&item)
);
}
#[test]
fn enclosure_image_html_ignores_non_image_enclosures() {
let mut item = Item::default();
item.set_enclosure(rss::Enclosure {
url: "https://example.test/episode.mp3".to_string(),
length: "2000".to_string(),
mime_type: "audio/mpeg".to_string(),
});
assert_eq!(None, enclosure_image_html(&item));
}
#[test]
fn enclosure_image_html_escapes_url_attribute() {
let mut item = Item::default();
item.set_enclosure(rss::Enclosure {
url: "https://example.test/img.jpg?a=1&b=\"x\"".to_string(),
length: "2000".to_string(),
mime_type: "image/jpeg".to_string(),
});
assert_eq!(
Some(r#"<img src="https://example.test/img.jpg?a=1&amp;b=&quot;x&quot;">"#.to_string()),
enclosure_image_html(&item)
);
}
#[actix_web::test]
async fn create_feed_item_does_not_duplicate_existing_items() {
let mut connection = establish_connection();