import { getCachedPayload } from '@/lib/payload'; import { absoluteUrl } from '@/lib/site'; import { getFlags } from '@/lib/flags'; /* * Dynamic, not ISR. * * This route has no dynamic params, so Next prerenders it at build time — and * CI builds the image with no database reachable, which fails the build. It is * a single indexed query against Postgres on the same Docker network, so * rendering per request is cheap, and it means a newly published post appears * here immediately rather than waiting on a revalidation. */ export const dynamic = 'force-dynamic'; function escapeXml(value: string): string { return value.replace(/[<>&'"]/g, (char) => ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"' }[char]!)); } export async function GET() { // A dark blog has no feed. 404 rather than an empty channel: an empty feed // is a live feed with nothing in it, which a reader would keep polling. const flags = await getFlags(); if (flags.blog !== true) return new Response('Not found', { status: 404 }); const payload = await getCachedPayload(); const { docs } = await payload.find({ collection: 'posts', where: { _status: { equals: 'published' } }, sort: '-publishedAt', limit: 50, depth: 0, }); const items = docs.map((post) => ` ${escapeXml(String(post.title))} ${absoluteUrl(`/blog/${post.slug}`)} ${absoluteUrl(`/blog/${post.slug}`)} ${escapeXml(String(post.excerpt))} ${new Date(String(post.publishedAt)).toUTCString()} `).join(''); const xml = ` schoolcompare blog ${absoluteUrl('/blog')} What school performance data shows, and what it does not. en-GB${items} `; return new Response(xml, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' }, }); }