import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { dailyVerses } from '$lib/server/db/schema'; import { desc } from 'drizzle-orm'; // Helper: Escape XML special characters function escapeXml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Helper: Format YYYY-MM-DD to RFC 822 date string function formatRFC822(dateStr: string): string { // Parse date in America/New_York timezone (EST/EDT) // Assuming midnight ET const date = new Date(dateStr + 'T00:00:00-05:00'); return date.toUTCString().replace('GMT', 'EST'); } // Helper: Format YYYY-MM-DD to readable date function formatReadableDate(dateStr: string): string { const date = new Date(dateStr + 'T00:00:00'); return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric', timeZone: 'America/New_York' }); } // Helper: Format verse text (VerseDisplay + Imposter unbalanced punctuation handling) function formatVerseText(text: string): string { let formatted = text; // Handle unbalanced opening/closing punctuation (from Imposter.svelte) const pairs: [string, string][] = [ ['(', ')'], ['[', ']'], ['{', '}'], ['"', '"'], ["'", "'"], ['\u201C', '\u201D'], // " " ['\u2018', '\u2019'] // ' ' ]; // Check if text starts with opening punctuation without closing for (const [open, close] of pairs) { if (formatted.startsWith(open) && !formatted.includes(close)) { formatted += '...' + close; break; } } // Check if text ends with closing punctuation without opening for (const [open, close] of pairs) { if (formatted.endsWith(close) && !formatted.includes(open)) { formatted = open + '...' + formatted; break; } } // Check if text contains unbalanced opening quotes (not at start) without closing for (const [open, close] of pairs) { const openCount = (formatted.match(new RegExp(`\\${open}`, 'g')) || []).length; const closeCount = (formatted.match(new RegExp(`\\${close}`, 'g')) || []).length; if (openCount > closeCount) { formatted += close; break; } } // Capitalize first letter if lowercase (from VerseDisplay.svelte) formatted = formatted.replace(/^([a-z])/, (c) => c.toUpperCase()); // Replace trailing punctuation with ellipsis // Preserve closing quotes/brackets that may have been added formatted = formatted.replace(/[,:;-—]([)\]}\"\'\u201D\u2019]*)$/, '...$1'); return formatted; } export const GET: RequestHandler = async ({ request }) => { try { // Query last 30 verses, ordered by date descending const verses = await db .select() .from(dailyVerses) .orderBy(desc(dailyVerses.date)) .limit(30); // Generate ETag based on latest verse date const etag = verses[0]?.date ? `"bibdle-feed-${verses[0].date}"` : '"bibdle-feed-empty"'; // Check if client has cached version if (request.headers.get('If-None-Match') === etag) { return new Response(null, { status: 304 }); } // Get site URL from environment or use default const SITE_URL = process.env.PUBLIC_SITE_URL || 'https://bibdle.com'; // Build RSS XML const lastBuildDate = verses[0] ? formatRFC822(verses[0].date) : new Date().toUTCString(); const items = verses .map( (verse) => ` Bibdle verse for ${formatReadableDate(verse.date)} ${escapeXml(formatVerseText(verse.verseText))} ${SITE_URL} bibdle-verse-${verse.date} ${formatRFC822(verse.date)} ` ) .join(''); const xml = ` Bibdle ${SITE_URL} A daily Bible game en-us ${lastBuildDate} 720${items} `; return new Response(xml, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': 'public, max-age=3600, s-maxage=3600', ETag: etag } }); } catch (error) { console.error('RSS feed generation error:', error); return new Response('Internal Server Error', { status: 500 }); } };