feat: added rss to posts 58d87247
Steve · 2026-04-07 18:04 2 file(s) · +82 −0
apps/posts/.env.example +1 −0
3 3
COOKIE_SECURE=false
4 4
HOST=127.0.0.1
5 5
PORT=3000
6 +
SITE_URL=http://localhost:3000
apps/posts/src/server.rs +81 −0
745 745
    Redirect::to("/admin/settings?success=true").into_response()
746 746
}
747 747
748 +
// --- RSS feed handler ---
749 +
750 +
fn xml_escape(s: &str) -> String {
751 +
    s.replace('&', "&")
752 +
        .replace('<', "&lt;")
753 +
        .replace('>', "&gt;")
754 +
        .replace('"', "&quot;")
755 +
        .replace('\'', "&apos;")
756 +
}
757 +
758 +
async fn rss_feed(State(state): State<Arc<AppState>>) -> Response {
759 +
    let blog_title = get_blog_title(&state.db);
760 +
    let blog_description = db::get_setting(&state.db, "blog_description")
761 +
        .ok()
762 +
        .flatten()
763 +
        .unwrap_or_default();
764 +
    let site_url = std::env::var("SITE_URL").unwrap_or_else(|_| "http://localhost:3000".to_string());
765 +
    let site_url = site_url.trim_end_matches('/');
766 +
767 +
    let posts = match db::get_published_posts(&state.db) {
768 +
        Ok(posts) => posts,
769 +
        Err(e) => {
770 +
            tracing::error!("Failed to get posts for RSS: {}", e);
771 +
            return (StatusCode::INTERNAL_SERVER_ERROR, "Server error").into_response();
772 +
        }
773 +
    };
774 +
775 +
    let mut items = String::new();
776 +
    for post in &posts {
777 +
        let link = format!("{}/posts/{}", site_url, xml_escape(&post.slug));
778 +
        let title = xml_escape(&post.title);
779 +
        let description = match &post.meta_description {
780 +
            Some(d) if !d.is_empty() => xml_escape(d),
781 +
            _ => {
782 +
                let plain: String = post.content.chars().take(200).collect();
783 +
                xml_escape(&plain)
784 +
            }
785 +
        };
786 +
        let pub_date = post.published_date.as_deref().unwrap_or(&post.created_at);
787 +
        let guid = format!("{}/posts/{}", site_url, xml_escape(&post.slug));
788 +
789 +
        items.push_str(&format!(
790 +
            "    <item>\n      <title>{title}</title>\n      <link>{link}</link>\n      <guid>{guid}</guid>\n      <description>{description}</description>\n      <pubDate>{pub_date}</pubDate>\n    </item>\n"
791 +
        ));
792 +
    }
793 +
794 +
    let last_build = posts
795 +
        .first()
796 +
        .and_then(|p| p.published_date.as_deref())
797 +
        .unwrap_or("");
798 +
799 +
    let xml = format!(
800 +
        r#"<?xml version="1.0" encoding="UTF-8"?>
801 +
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
802 +
  <channel>
803 +
    <title>{title}</title>
804 +
    <link>{site_url}</link>
805 +
    <description>{desc}</description>
806 +
    <lastBuildDate>{last_build}</lastBuildDate>
807 +
    <atom:link href="{site_url}/feed.xml" rel="self" type="application/rss+xml"/>
808 +
{items}  </channel>
809 +
</rss>"#,
810 +
        title = xml_escape(&blog_title),
811 +
        site_url = site_url,
812 +
        desc = xml_escape(&blog_description),
813 +
        last_build = last_build,
814 +
        items = items,
815 +
    );
816 +
817 +
    (
818 +
        StatusCode::OK,
819 +
        [(
820 +
            axum::http::header::CONTENT_TYPE,
821 +
            HeaderValue::from_static("application/rss+xml; charset=utf-8"),
822 +
        )],
823 +
        xml,
824 +
    )
825 +
        .into_response()
826 +
}
827 +
748 828
// --- Date helper ---
749 829
750 830
fn days_to_ymd(mut days: i64) -> (i64, i64, i64) {
792 872
        .route("/", get(public_index))
793 873
        .route("/posts/{slug}", get(public_post))
794 874
        .route("/pages/{slug}", get(public_page))
875 +
        .route("/feed.xml", get(rss_feed))
795 876
        // Admin auth
796 877
        .route("/admin/login", get(get_login).post(post_login))
797 878
        .route("/admin/logout", get(get_logout))