2026-09-02 16:28:58 +01:00
|
|
|
import { getCachedPayload } from '@/lib/payload';
|
|
|
|
|
import { absoluteUrl } from '@/lib/site';
|
2026-09-08 17:12:25 +01:00
|
|
|
import { getFlags } from '@/lib/flags';
|
2026-09-02 16:28:58 +01:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* 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() {
|
2026-09-08 17:12:25 +01:00
|
|
|
// 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 });
|
|
|
|
|
|
2026-09-02 16:28:58 +01:00
|
|
|
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) => `
|
|
|
|
|
<item>
|
|
|
|
|
<title>${escapeXml(String(post.title))}</title>
|
|
|
|
|
<link>${absoluteUrl(`/blog/${post.slug}`)}</link>
|
|
|
|
|
<guid isPermaLink="true">${absoluteUrl(`/blog/${post.slug}`)}</guid>
|
|
|
|
|
<description>${escapeXml(String(post.excerpt))}</description>
|
|
|
|
|
<pubDate>${new Date(String(post.publishedAt)).toUTCString()}</pubDate>
|
|
|
|
|
</item>`).join('');
|
|
|
|
|
|
|
|
|
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<rss version="2.0">
|
|
|
|
|
<channel>
|
|
|
|
|
<title>schoolcompare blog</title>
|
|
|
|
|
<link>${absoluteUrl('/blog')}</link>
|
|
|
|
|
<description>What school performance data shows, and what it does not.</description>
|
|
|
|
|
<language>en-GB</language>${items}
|
|
|
|
|
</channel>
|
|
|
|
|
</rss>`;
|
|
|
|
|
|
|
|
|
|
return new Response(xml, {
|
|
|
|
|
headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' },
|
|
|
|
|
});
|
|
|
|
|
}
|