Add auto-generated RSS feed with scheduled publishing support
- Created /rss.xml API route that generates valid RSS 2.0 XML - RSS feed integrates with scheduled publishing system automatically - Includes full post content, proper metadata, and CDATA encoding - Added RSS feed link to HTML metadata for auto-discovery - RSS link already exists in blog footer navigation - 1-hour caching with error handling and fallback RSS - Feed updates automatically when new posts go live Features: ✅ Works with scheduled posts (future posts excluded) ✅ Proper XML escaping and CDATA content encoding ✅ Rich metadata including author, categories, publish dates ✅ Auto-discovery via HTML <link> tags ✅ Graceful error handling with fallback feed 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,11 @@ export const metadata: Metadata = {
|
|||||||
metadataBase: new URL('https://blog.chorus.services'),
|
metadataBase: new URL('https://blog.chorus.services'),
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: 'https://blog.chorus.services',
|
canonical: 'https://blog.chorus.services',
|
||||||
|
types: {
|
||||||
|
'application/rss+xml': [
|
||||||
|
{ url: '/rss.xml', title: 'CHORUS PING! RSS Feed' }
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: 'website',
|
type: 'website',
|
||||||
|
|||||||
101
app/rss.xml/route.ts
Normal file
101
app/rss.xml/route.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { getSortedPostsData } from '@/lib/blog'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
// RSS feed configuration
|
||||||
|
const SITE_URL = 'https://blog.chorus.services'
|
||||||
|
const SITE_TITLE = 'CHORUS Services Blog'
|
||||||
|
const SITE_DESCRIPTION = 'Contextual AI orchestration, on-premises infrastructure, and the future of intelligent systems.'
|
||||||
|
const SITE_LANGUAGE = 'en-us'
|
||||||
|
const AUTHOR_EMAIL = 'noreply@chorus.services'
|
||||||
|
const AUTHOR_NAME = 'CHORUS Services'
|
||||||
|
|
||||||
|
function escapeXml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRssDate(dateString: string): string {
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toUTCString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Get all published posts (includes scheduled posts that should be live)
|
||||||
|
const posts = getSortedPostsData()
|
||||||
|
|
||||||
|
// Generate RSS XML
|
||||||
|
const rssXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>${escapeXml(SITE_TITLE)}</title>
|
||||||
|
<link>${SITE_URL}</link>
|
||||||
|
<description>${escapeXml(SITE_DESCRIPTION)}</description>
|
||||||
|
<language>${SITE_LANGUAGE}</language>
|
||||||
|
<managingEditor>${AUTHOR_EMAIL} (${AUTHOR_NAME})</managingEditor>
|
||||||
|
<webMaster>${AUTHOR_EMAIL} (${AUTHOR_NAME})</webMaster>
|
||||||
|
<lastBuildDate>${formatRssDate(new Date().toISOString())}</lastBuildDate>
|
||||||
|
<atom:link href="${SITE_URL}/rss.xml" rel="self" type="application/rss+xml" />
|
||||||
|
<generator>CHORUS Blog RSS Generator</generator>
|
||||||
|
<ttl>60</ttl>
|
||||||
|
${posts.map(post => ` <item>
|
||||||
|
<title>${escapeXml(post.title)}</title>
|
||||||
|
<link>${SITE_URL}/posts/${post.slug}</link>
|
||||||
|
<description>${escapeXml(post.description)}</description>
|
||||||
|
<author>${AUTHOR_EMAIL} (${post.author.name})</author>
|
||||||
|
<category>${post.tags.map(tag => escapeXml(tag)).join(', ')}</category>
|
||||||
|
<guid isPermaLink="true">${SITE_URL}/posts/${post.slug}</guid>
|
||||||
|
<pubDate>${formatRssDate(post.date)}</pubDate>
|
||||||
|
<content:encoded><![CDATA[
|
||||||
|
<p><strong>Author:</strong> ${escapeXml(post.author.name)}${post.author.role ? `, ${escapeXml(post.author.role)}` : ''}</p>
|
||||||
|
<p><strong>Reading Time:</strong> ${post.readingTime} min</p>
|
||||||
|
<p><strong>Tags:</strong> ${post.tags.map(tag => escapeXml(tag)).join(', ')}</p>
|
||||||
|
<hr>
|
||||||
|
<div>${post.content}</div>
|
||||||
|
<hr>
|
||||||
|
<p><a href="${SITE_URL}/posts/${post.slug}">Read full post on CHORUS Blog</a></p>
|
||||||
|
]]></content:encoded>
|
||||||
|
</item>`).join('\n')}
|
||||||
|
</channel>
|
||||||
|
</rss>`
|
||||||
|
|
||||||
|
return new NextResponse(rssXml, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/rss+xml; charset=utf-8',
|
||||||
|
'Cache-Control': 'public, max-age=3600, s-maxage=3600', // Cache for 1 hour
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating RSS feed:', error)
|
||||||
|
|
||||||
|
// Return a minimal RSS feed with error info
|
||||||
|
const errorRss = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>${escapeXml(SITE_TITLE)} - Error</title>
|
||||||
|
<link>${SITE_URL}</link>
|
||||||
|
<description>Error generating RSS feed</description>
|
||||||
|
<language>${SITE_LANGUAGE}</language>
|
||||||
|
<lastBuildDate>${formatRssDate(new Date().toISOString())}</lastBuildDate>
|
||||||
|
<item>
|
||||||
|
<title>RSS Feed Error</title>
|
||||||
|
<link>${SITE_URL}</link>
|
||||||
|
<description>There was an error generating the RSS feed. Please try again later.</description>
|
||||||
|
<pubDate>${formatRssDate(new Date().toISOString())}</pubDate>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>`
|
||||||
|
|
||||||
|
return new NextResponse(errorRss, {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/rss+xml; charset=utf-8',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user