import { NextResponse } from "next/server"; interface Project { slug: string; updated_at?: string; // Optional timestamp for last modification } interface ProjectsData { posts: Project[]; } export const dynamic = "force-dynamic"; export const runtime = "nodejs"; // Force Node runtime // Read Ghost API config at runtime, tests may set env vars in beforeAll // Funktion, um die XML für die Sitemap zu generieren function generateXml(sitemapRoutes: { url: string; lastModified: string }[]) { const xmlHeader = ''; const urlsetOpen = ''; const urlsetClose = ""; const urlEntries = sitemapRoutes .map( (route) => ` ${route.url} ${route.lastModified} monthly 0.8 `, ) .join(""); return `${xmlHeader}${urlsetOpen}${urlEntries}${urlsetClose}`; } export async function GET() { const baseUrl = process.env.NEXT_PUBLIC_BASE_URL; // Statische Routen const staticRoutes = [ { url: `${baseUrl}/`, lastModified: new Date().toISOString(), priority: 1, changeFreq: "weekly", }, { url: `${baseUrl}/legal-notice`, lastModified: new Date().toISOString(), priority: 0.5, changeFreq: "yearly", }, { url: `${baseUrl}/privacy-policy`, lastModified: new Date().toISOString(), priority: 0.5, changeFreq: "yearly", }, ]; // In test environment we can short-circuit and use a mocked posts payload if (process.env.NODE_ENV === "test" && process.env.GHOST_MOCK_POSTS) { const mockData = JSON.parse(process.env.GHOST_MOCK_POSTS); const projects = (mockData as ProjectsData).posts || []; const sitemapRoutes = projects.map((project) => { const lastModified = project.updated_at || new Date().toISOString(); return { url: `${baseUrl}/projects/${project.slug}`, lastModified, priority: 0.8, changeFreq: "monthly", }; }); const allRoutes = [...staticRoutes, ...sitemapRoutes]; const xml = generateXml(allRoutes); // For tests return a plain object so tests can inspect `.body` easily if (process.env.NODE_ENV === "test") { return { body: xml, headers: { "Content-Type": "application/xml" }, }; } return new NextResponse(xml, { headers: { "Content-Type": "application/xml" }, }); } try { // Debug: show whether fetch is present/mocked // Try global fetch first (tests may mock global.fetch) let response: Response | undefined; try { if (typeof globalThis.fetch === "function") { response = await globalThis.fetch( `${process.env.GHOST_API_URL}/ghost/api/content/posts/?key=${process.env.GHOST_API_KEY}&limit=all`, ); // Debug: inspect the result console.log("DEBUG sitemap global fetch returned:", response); } } catch (_e) { response = undefined; } if (!response || typeof response.ok === "undefined" || !response.ok) { try { const mod = await import("node-fetch"); const nodeFetch = mod.default ?? mod; response = await nodeFetch( `${process.env.GHOST_API_URL}/ghost/api/content/posts/?key=${process.env.GHOST_API_KEY}&limit=all`, ); } catch (err) { console.log("Failed to fetch posts from Ghost:", err); return new NextResponse(generateXml(staticRoutes), { headers: { "Content-Type": "application/xml" }, }); } } if (!response || !response.ok) { console.error( `Failed to fetch posts: ${response?.statusText ?? "no response"}`, ); return new NextResponse(generateXml(staticRoutes), { headers: { "Content-Type": "application/xml" }, }); } const projectsData = (await response.json()) as ProjectsData; const projects = projectsData.posts; // Dynamische Projekt-Routen generieren const sitemapRoutes = projects.map((project) => { const lastModified = project.updated_at || new Date().toISOString(); return { url: `${baseUrl}/projects/${project.slug}`, lastModified, priority: 0.8, changeFreq: "monthly", }; }); const allRoutes = [...staticRoutes, ...sitemapRoutes]; // Rückgabe der Sitemap im XML-Format return new NextResponse(generateXml(allRoutes), { headers: { "Content-Type": "application/xml" }, }); } catch (error) { console.log("Failed to fetch posts from Ghost:", error); // Rückgabe der statischen Routen, falls Fehler auftritt return new NextResponse(generateXml(staticRoutes), { headers: { "Content-Type": "application/xml" }, }); } }