import { httpAction } from "./_generated/server"; import { api } from "./_generated/api"; // Site configuration for RSS feed const SITE_URL = process.env.SITE_URL || "https://markdown.fast"; const SITE_TITLE = "markdown sync framework"; const SITE_DESCRIPTION = "An open-source publishing framework for AI agents and developers. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify."; // Escape XML special characters function escapeXml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // Generate RSS XML from posts (description only) function generateRssXml( posts: Array<{ title: string; description: string; slug: string; date: string; }>, feedPath: string = "/rss.xml", ): string { const items = posts .map((post) => { const pubDate = new Date(post.date).toUTCString(); const url = `${SITE_URL}/${post.slug}`; return ` ${escapeXml(post.title)} ${url} ${url} ${pubDate} ${escapeXml(post.description)} `; }) .join(""); return ` ${escapeXml(SITE_TITLE)} ${SITE_URL} ${escapeXml(SITE_DESCRIPTION)} en-us ${new Date().toUTCString()} ${items} `; } // Generate RSS XML with full content (for LLMs and readers) function generateFullRssXml( posts: Array<{ title: string; description: string; slug: string; date: string; content: string; readTime?: string; tags: string[]; }>, ): string { const items = posts .map((post) => { const pubDate = new Date(post.date).toUTCString(); const url = `${SITE_URL}/${post.slug}`; return ` ${escapeXml(post.title)} ${url} ${url} ${pubDate} ${escapeXml(post.description)} ${post.tags.map((tag) => `${escapeXml(tag)}`).join("\n ")} `; }) .join(""); return ` ${escapeXml(SITE_TITLE)} - Full Content ${SITE_URL} ${escapeXml(SITE_DESCRIPTION)} Full article content for readers and AI. en-us ${new Date().toUTCString()} ${items} `; } // HTTP action to serve RSS feed (descriptions only) export const rssFeed = httpAction(async (ctx) => { const posts = await ctx.runQuery(api.posts.getAllPosts); const xml = generateRssXml( posts.map((post) => ({ title: post.title, description: post.description, slug: post.slug, date: post.date, })), ); return new Response(xml, { headers: { "Content-Type": "application/rss+xml; charset=utf-8", "Cache-Control": "public, max-age=3600, s-maxage=7200", }, }); }); // HTTP action to serve full RSS feed (with complete content) export const rssFullFeed = httpAction(async (ctx) => { const posts = await ctx.runQuery(api.posts.getAllPosts); // Fetch full content for each post const fullPosts = await Promise.all( posts.map(async (post) => { const fullPost = await ctx.runQuery(api.posts.getPostBySlug, { slug: post.slug, }); return { title: post.title, description: post.description, slug: post.slug, date: post.date, content: fullPost?.content || "", readTime: post.readTime, tags: post.tags, }; }), ); const xml = generateFullRssXml(fullPosts); return new Response(xml, { headers: { "Content-Type": "application/rss+xml; charset=utf-8", "Cache-Control": "public, max-age=3600, s-maxage=7200", }, }); });