2026-09-02 16:28:58 +01:00
|
|
|
/*
|
|
|
|
|
* A second sitemap for the URLs Next owns.
|
|
|
|
|
*
|
|
|
|
|
* /sitemap.xml is proxied from FastAPI (app/(frontend)/sitemap.xml), which
|
|
|
|
|
* knows nothing about Payload — the backend and frontend ship as separate
|
|
|
|
|
* images. Rather than teach it, the Next-owned URLs get their own sitemap and
|
|
|
|
|
* robots.txt lists both.
|
|
|
|
|
*/
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
export async function GET() {
|
2026-09-08 17:12:25 +01:00
|
|
|
/*
|
|
|
|
|
* A dark page must not be advertised. Submitting a URL that 404s is the one
|
|
|
|
|
* thing a sitemap is not allowed to do, so each entry is gated on the same
|
|
|
|
|
* flag that gates the page itself.
|
|
|
|
|
*
|
|
|
|
|
* With both flags dark this emits a valid, empty <urlset> rather than a 404:
|
|
|
|
|
* robots.txt names this sitemap unconditionally, and an empty sitemap is a
|
|
|
|
|
* well-formed statement that there is nothing here yet.
|
|
|
|
|
*/
|
|
|
|
|
const flags = await getFlags();
|
|
|
|
|
const aboutEnabled = flags.about_page === true;
|
|
|
|
|
const blogEnabled = flags.blog === true;
|
|
|
|
|
|
|
|
|
|
// Only query Payload when the blog is actually being advertised.
|
|
|
|
|
const docs = blogEnabled
|
|
|
|
|
? (await (await getCachedPayload()).find({
|
|
|
|
|
collection: 'posts',
|
|
|
|
|
where: { _status: { equals: 'published' } },
|
|
|
|
|
sort: '-publishedAt',
|
|
|
|
|
limit: 500,
|
|
|
|
|
depth: 0,
|
|
|
|
|
})).docs
|
|
|
|
|
: [];
|
2026-09-02 16:28:58 +01:00
|
|
|
|
|
|
|
|
const urls: Array<{ loc: string; lastmod: string | null }> = [
|
2026-09-08 17:12:25 +01:00
|
|
|
...(aboutEnabled ? [{ loc: absoluteUrl('/about'), lastmod: null }] : []),
|
|
|
|
|
...(blogEnabled ? [{ loc: absoluteUrl('/blog'), lastmod: null }] : []),
|
2026-09-02 16:28:58 +01:00
|
|
|
...docs.map((post) => ({
|
|
|
|
|
loc: absoluteUrl(`/blog/${post.slug}`),
|
|
|
|
|
lastmod: new Date(String(post.updatedAt ?? post.publishedAt)).toISOString(),
|
|
|
|
|
})),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
|
|
|
${urls.map(({ loc, lastmod }) =>
|
|
|
|
|
` <url><loc>${loc}</loc>${lastmod ? `<lastmod>${lastmod}</lastmod>` : ''}</url>`,
|
|
|
|
|
).join('\n')}
|
|
|
|
|
</urlset>`;
|
|
|
|
|
|
|
|
|
|
return new Response(xml, {
|
|
|
|
|
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
|
|
|
});
|
|
|
|
|
}
|