From 914bbecffbd23e2e9117de0c97f670324881f509 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Thu, 26 Feb 2026 22:53:27 -0600 Subject: [PATCH] Add digest RSS feed endpoint for blog consumers --- README.md | 5 ++ src/app/api/rss/route.ts | 143 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 src/app/api/rss/route.ts diff --git a/README.md b/README.md index 4bc23e0..5cdf5e2 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,11 @@ Set these in `.env.local`: - `POST /api/digest` requires `x-api-key: ` - Used for cron-based digest publishing +## RSS feeds + +- `GET /api/rss` - Standard digest RSS feed for blog/article consumers +- `GET /api/podcast/rss` - Podcast RSS feed (audio episodes) + ## MP3 Audio Hosting Upload and host MP3 files for individual blog posts. diff --git a/src/app/api/rss/route.ts b/src/app/api/rss/route.ts new file mode 100644 index 0000000..3fdf5fe --- /dev/null +++ b/src/app/api/rss/route.ts @@ -0,0 +1,143 @@ +import { NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; +import { extractExcerpt, extractTitle } from "@/lib/podcast"; + +export const dynamic = "force-dynamic"; + +interface DigestRow { + id: string; + date: string | null; + content: string; + timestamp: number | string; + tags: string[] | null; +} + +function escapeXml(input: string): string { + return input + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function cdataSafe(input: string): string { + return input.replaceAll("]]>", "]]]]>"); +} + +function asRfc2822(value: string | number | null | undefined): string { + if (typeof value === "number") { + return new Date(value).toUTCString(); + } + + if (typeof value === "string" && value.trim().length > 0) { + const numeric = Number(value); + if (Number.isFinite(numeric)) { + return new Date(numeric).toUTCString(); + } + return new Date(value).toUTCString(); + } + + return new Date().toUTCString(); +} + +function buildErrorFeed(baseUrl: string): string { + return ` + + + OpenClaw Daily Digest + ${escapeXml(baseUrl)} + Error loading digest RSS feed. Please try again later. + +`; +} + +export async function GET(request: Request) { + const url = new URL(request.url); + const baseUrl = + process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") || + `${url.protocol}//${url.host}`; + + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseKey = + process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseKey) { + return new NextResponse(buildErrorFeed(baseUrl), { + status: 500, + headers: { + "Content-Type": "application/xml; charset=utf-8", + }, + }); + } + + try { + const supabase = createClient(supabaseUrl, supabaseKey); + + const { data, error } = await supabase + .from("blog_messages") + .select("id, date, content, timestamp, tags") + .order("timestamp", { ascending: false }) + .limit(100); + + if (error) { + throw error; + } + + const rows = (data || []) as DigestRow[]; + + const itemsXml = rows + .map((row) => { + const title = escapeXml(extractTitle(row.content || "Daily Digest")); + const excerpt = escapeXml(extractExcerpt(row.content || "", 420)); + const pubDate = asRfc2822(row.timestamp || row.date); + const postUrl = `${baseUrl}/?post=${row.id}`; + const guid = `openclaw-digest-${row.id}`; + const categories = (row.tags || []) + .map((tag) => `${escapeXml(tag)}`) + .join(""); + + return ` + ${title} + ${escapeXml(postUrl)} + ${escapeXml(guid)} + ${pubDate} + ${excerpt} + + ${categories} +`; + }) + .join("\n"); + + const lastBuildDate = + rows.length > 0 ? asRfc2822(rows[0].timestamp || rows[0].date) : new Date().toUTCString(); + + const feedXml = ` + + + OpenClaw Daily Digest + ${escapeXml(baseUrl)} + Daily digest posts from OpenClaw Blog Backup. + en-us + ${lastBuildDate} + + ${itemsXml} + +`; + + return new NextResponse(feedXml, { + headers: { + "Content-Type": "application/xml; charset=utf-8", + "Cache-Control": "public, max-age=300", + }, + }); + } catch (error) { + console.error("Error generating digest RSS feed:", error); + return new NextResponse(buildErrorFeed(baseUrl), { + status: 500, + headers: { + "Content-Type": "application/xml; charset=utf-8", + }, + }); + } +}