chore: add /feed.xml to feeds 4907cd38
Steve Simkins · 2026-05-05 18:54 1 file(s) · +84 −0
apps/feeds/src/main.rs +84 −0
280 280
        .replace('\'', "'")
281 281
}
282 282
283 +
async fn atom_feed_handler(State(state): State<Arc<AppState>>) -> Response {
284 +
    let items = match fdb::list_items(
285 +
        &state.db,
286 +
        &fdb::ListItemsFilter {
287 +
            limit: Some(100),
288 +
            ..Default::default()
289 +
        },
290 +
    ) {
291 +
        Ok(items) => items,
292 +
        Err(e) => {
293 +
            tracing::error!("atom feed query failed: {e}");
294 +
            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
295 +
        }
296 +
    };
297 +
298 +
    let updated = items
299 +
        .iter()
300 +
        .map(|i| i.published_at)
301 +
        .max()
302 +
        .and_then(|ts| DateTime::from_timestamp(ts, 0))
303 +
        .unwrap_or_else(chrono::Utc::now)
304 +
        .to_rfc3339();
305 +
306 +
    let base = state.base_url.trim_end_matches('/');
307 +
    let self_url = format!("{base}/feed.xml");
308 +
309 +
    let mut xml = String::with_capacity(4096);
310 +
    xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
311 +
    xml.push_str("<feed xmlns=\"http://www.w3.org/2005/Atom\">\n");
312 +
    xml.push_str("  <title>Feeds</title>\n");
313 +
    xml.push_str(&format!(
314 +
        "  <link href=\"{}\" rel=\"self\" type=\"application/atom+xml\" />\n",
315 +
        escape_xml(&self_url)
316 +
    ));
317 +
    xml.push_str(&format!("  <link href=\"{}\" />\n", escape_xml(base)));
318 +
    xml.push_str(&format!("  <id>{}</id>\n", escape_xml(&self_url)));
319 +
    xml.push_str(&format!("  <updated>{updated}</updated>\n"));
320 +
321 +
    for item in &items {
322 +
        let published = DateTime::from_timestamp(item.published_at, 0)
323 +
            .unwrap_or_else(chrono::Utc::now)
324 +
            .to_rfc3339();
325 +
        let author_name = match item.author.as_deref() {
326 +
            Some(a) if !a.is_empty() => a,
327 +
            _ => item.feed_title.as_str(),
328 +
        };
329 +
        let entry_id = if item.guid.is_empty() {
330 +
            &item.link
331 +
        } else {
332 +
            &item.guid
333 +
        };
334 +
335 +
        xml.push_str("  <entry>\n");
336 +
        xml.push_str(&format!("    <title>{}</title>\n", escape_xml(&item.title)));
337 +
        xml.push_str(&format!(
338 +
            "    <link href=\"{}\" />\n",
339 +
            escape_xml(&item.link)
340 +
        ));
341 +
        xml.push_str(&format!("    <id>{}</id>\n", escape_xml(entry_id)));
342 +
        xml.push_str(&format!("    <updated>{published}</updated>\n"));
343 +
        xml.push_str(&format!("    <published>{published}</published>\n"));
344 +
        xml.push_str("    <author>\n");
345 +
        xml.push_str(&format!(
346 +
            "      <name>{}</name>\n",
347 +
            escape_xml(author_name)
348 +
        ));
349 +
        xml.push_str("    </author>\n");
350 +
        xml.push_str(&format!(
351 +
            "    <source><title>{}</title></source>\n",
352 +
            escape_xml(&item.feed_title)
353 +
        ));
354 +
        xml.push_str("  </entry>\n");
355 +
    }
356 +
357 +
    xml.push_str("</feed>\n");
358 +
359 +
    (
360 +
        [(header::CONTENT_TYPE, "application/atom+xml; charset=utf-8")],
361 +
        xml,
362 +
    )
363 +
        .into_response()
364 +
}
365 +
283 366
async fn static_handler(axum::extract::Path(path): axum::extract::Path<String>) -> Response {
284 367
    match Static::get(&path) {
285 368
        Some(file) => {
652 735
    let app = Router::new()
653 736
        .route("/", get(index_handler))
654 737
        .route("/feeds", get(feeds_handler))
738 +
        .route("/feed.xml", get(atom_feed_handler))
655 739
        .route("/static/{*path}", get(static_handler))
656 740
        .merge(admin_router)
657 741
        .merge(api_router)