Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
72 changes: 72 additions & 0 deletions app/blog/rss.xml/route.ts
Original file line number Diff line number Diff line change
@@ -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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}

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 = [
` <title>${escapeXml(post.title)}</title>`,
` <link>${escapeXml(link)}</link>`,
` <guid isPermaLink="true">${escapeXml(link)}</guid>`,
` <pubDate>${date}</pubDate>`,
]
if (post.excerpt) {
parts.push(` <description>${escapeXml(post.excerpt)}</description>`)
}
if (post.author_name) {
parts.push(` <dc:creator>${escapeXml(post.author_name)}</dc:creator>`)
}
for (const tag of post.tags ?? []) {
parts.push(` <category>${escapeXml(tag)}</category>`)
}
return ` <item>\n${parts.join('\n')}\n </item>`
})
.join('\n')

const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>SaaSRow Blog</title>
<link>${SITE}/blog</link>
<description>Articles, guides, and insights about software and productivity</description>
<language>en</language>
<lastBuildDate>${lastBuildDate}</lastBuildDate>
<pubDate>${pubDate}</pubDate>
<atom:link href="${SITE}/blog/rss.xml" rel="self" type="application/rss+xml" />
${items}
</channel>
</rss>`

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',
},
})
}
Loading