This release transforms PING into a sophisticated newspaper-style digital publication with enhanced readability and professional presentation. Major Features: - New FeaturedPostHero component with full-width newspaper design - Completely redesigned homepage with responsive newspaper grid layout - Enhanced PostCard component with refined typography and spacing - Improved mobile-first responsive design (mobile → tablet → desktop → 2XL) - Archive section with multi-column layout for deeper content discovery Technical Improvements: - Enhanced blog post validation and error handling in lib/blog.ts - Better date handling and normalization for scheduled posts - Improved Dockerfile with correct content volume mount paths - Fixed port configuration (3025 throughout stack) - Updated Tailwind config with refined typography and newspaper aesthetics - Added getFeaturedPost() function for hero selection UI/UX Enhancements: - Professional newspaper-style borders and dividers - Improved dark mode styling throughout - Better content hierarchy and visual flow - Enhanced author bylines and metadata presentation - Refined color palette with newspaper sophistication Documentation: - Added DESIGN_BRIEF_NEWSPAPER_LAYOUT.md detailing design principles - Added TESTING_RESULTS_25_POSTS.md with test scenarios This release establishes PING as a premium publication platform for AI orchestration and contextual intelligence thought leadership. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
104 lines
3.8 KiB
TypeScript
104 lines
3.8 KiB
TypeScript
import { getSortedPostsData } from '@/lib/blog'
|
|
import { NextResponse } from 'next/server'
|
|
|
|
// Force dynamic rendering to pick up volume-mounted content
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
// 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',
|
|
},
|
|
})
|
|
}
|
|
} |