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",
},
});
});