diff --git a/app/blog/page.tsx b/app/blog/page.tsx index c91ba22..489635c 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -7,6 +7,11 @@ export const revalidate = 60 export const metadata: Metadata = { title: 'Blog', description: 'Articles, guides, and insights about software and productivity', + alternates: { + types: { + 'application/rss+xml': '/blog/rss.xml', + }, + }, } export default async function Page() { diff --git a/app/blog/rss.xml/route.ts b/app/blog/rss.xml/route.ts new file mode 100644 index 0000000..b6676f8 --- /dev/null +++ b/app/blog/rss.xml/route.ts @@ -0,0 +1,72 @@ +import { getBlogList } from '@/lib/api' + +// Regenerate at most hourly so new posts show up without a full rebuild. +export const revalidate = 3600 + +const SITE = (process.env.NEXT_PUBLIC_SITE_URL || 'https://www.saasrow.com').replace( + /\/+$/, + '', +) + +function escapeXml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +export async function GET() { + const posts = await getBlogList() + + const lastBuildDate = new Date().toUTCString() + const latest = posts[0] + const pubDate = latest + ? new Date(latest.published_at || latest.created_at).toUTCString() + : lastBuildDate + + const items = posts + .map((post) => { + const link = `${SITE}/blog/${post.slug}` + const date = new Date(post.published_at || post.created_at).toUTCString() + const parts = [ + ` ${escapeXml(post.title)}`, + ` ${escapeXml(link)}`, + ` ${escapeXml(link)}`, + ` ${date}`, + ] + if (post.excerpt) { + parts.push(` ${escapeXml(post.excerpt)}`) + } + if (post.author_name) { + parts.push(` ${escapeXml(post.author_name)}`) + } + for (const tag of post.tags ?? []) { + parts.push(` ${escapeXml(tag)}`) + } + return ` \n${parts.join('\n')}\n ` + }) + .join('\n') + + const xml = ` + + + SaaSRow Blog + ${SITE}/blog + Articles, guides, and insights about software and productivity + en + ${lastBuildDate} + ${pubDate} + +${items} + +` + + return new Response(xml, { + headers: { + 'Content-Type': 'application/rss+xml; charset=utf-8', + 'Cache-Control': 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400', + }, + }) +}