import { existsSync, statSync } from "node:fs"; import path from "node:path"; import type { MetadataRoute } from "next"; import { API_URL } from "@/config/constants"; import { getAvailablePlaceConfigs } from "@/app/facilityData"; import { getCourseVisits, getOpinionArticles } from "@/content/courseVisits"; import { buildAbsoluteUrl } from "@/app/seo"; type SitemapFacility = { slug?: string; status_updated_at?: string | null; vtg_updated_at?: string | null; }; type SitePageSeoRecord = { updated_at?: string | null; }; type PlacePageRecord = { updated_at?: string | null; }; type StaticRouteConfig = { path: string; changeFrequency: "daily" | "weekly" | "monthly"; priority: number; sourceFiles: string[]; sitePageKey?: string; }; export const revalidate = 3600; export const dynamic = "force-dynamic"; const staticRouteConfigs: StaticRouteConfig[] = [ { path: "/", changeFrequency: "daily", priority: 1, sourceFiles: ["src/app/page.tsx"], }, { path: "/golfbaner", changeFrequency: "daily", priority: 0.95, sourceFiles: ["src/app/golfbaner/page.tsx", "src/app/pageSeo.ts"], sitePageKey: "golfbaner", }, { path: "/medlemskap", changeFrequency: "daily", priority: 0.8, sourceFiles: ["src/app/medlemskap/page.tsx", "src/app/pageSeo.ts"], sitePageKey: "medlemskap", }, { path: "/vtg", changeFrequency: "daily", priority: 0.8, sourceFiles: ["src/app/vtg/page.tsx", "src/app/pageSeo.ts"], sitePageKey: "vtg", }, { path: "/banebesok", changeFrequency: "weekly", priority: 0.72, sourceFiles: ["src/app/banebesok/page.tsx", "src/app/pageSeo.ts"], sitePageKey: "banebesok", }, { path: "/meninger", changeFrequency: "weekly", priority: 0.7, sourceFiles: ["src/app/meninger/page.tsx", "src/app/pageSeo.ts"], sitePageKey: "meninger", }, { path: "/turneringer", changeFrequency: "daily", priority: 0.68, sourceFiles: ["src/app/turneringer/page.tsx"], }, { path: "/klubbnummer", changeFrequency: "weekly", priority: 0.64, sourceFiles: ["src/app/klubbnummer/page.tsx"], }, { path: "/om", changeFrequency: "monthly", priority: 0.45, sourceFiles: ["src/app/om/page.tsx"], }, { path: "/kontakt", changeFrequency: "monthly", priority: 0.42, sourceFiles: ["src/app/kontakt/page.tsx"], }, { path: "/personvern-og-cookies", changeFrequency: "monthly", priority: 0.38, sourceFiles: ["src/app/personvern-og-cookies/page.tsx"], }, ]; function parseDate(value: string | Date | null | undefined) { if (!value) return null; const candidate = value instanceof Date ? value : new Date(value); return Number.isNaN(candidate.getTime()) ? null : candidate; } function maxDate(values: Array) { let current: Date | null = null; for (const value of values) { const parsed = parseDate(value); if (!parsed) continue; if (!current || parsed.getTime() > current.getTime()) { current = parsed; } } return current; } function getSourceLastModified(relativePaths: string[]) { let current: Date | null = null; for (const relativePath of relativePaths) { const absolutePath = path.join(process.cwd(), relativePath); if (!existsSync(absolutePath)) continue; const stats = statSync(absolutePath); if (!current || stats.mtime.getTime() > current.getTime()) { current = stats.mtime; } } return current; } async function fetchSitePageSeoUpdatedAt(pageKey: string) { try { const response = await fetch(`${API_URL}/page-seo/${pageKey}`, { next: { revalidate }, }); if (!response.ok) return null; const data = (await response.json()) as SitePageSeoRecord; return parseDate(data.updated_at); } catch { return null; } } async function fetchPlacePageUpdatedAt(slug: string) { try { const response = await fetch(`${API_URL}/place-pages/${slug}`, { next: { revalidate }, }); if (!response.ok) return null; const data = (await response.json()) as PlacePageRecord; return parseDate(data.updated_at); } catch { return null; } } export default async function sitemap(): Promise { let facilities: SitemapFacility[] = []; try { const res = await fetch(`${API_URL}/facilities?view=sitemap`, { next: { revalidate }, }); if (res.ok) { const data = await res.json(); facilities = Array.isArray(data) ? data : []; } } catch { facilities = []; } const staticRoutes = await Promise.all( staticRouteConfigs.map(async (route) => { const sitePageUpdatedAt = route.sitePageKey ? await fetchSitePageSeoUpdatedAt(route.sitePageKey) : null; return { url: buildAbsoluteUrl(route.path), lastModified: maxDate([sitePageUpdatedAt, getSourceLastModified(route.sourceFiles)]) || new Date(), changeFrequency: route.changeFrequency, priority: route.priority, }; }), ); const placeSourceLastModified = getSourceLastModified([ "src/app/sted/[slug]/page.tsx", "src/app/placeSeo.ts", ]); const placeRoutes = await Promise.all( getAvailablePlaceConfigs().map(async (slug) => ({ url: buildAbsoluteUrl(`/sted/${slug}`), lastModified: maxDate([await fetchPlacePageUpdatedAt(slug), placeSourceLastModified]) || new Date(), changeFrequency: "daily" as const, priority: slug === "norge" ? 0.9 : 0.75, })), ); const facilityPageSourceLastModified = getSourceLastModified([ "src/app/golfbaner/[slug]/page.tsx", ]); const facilityRoutes = facilities .filter((facility) => Boolean(facility.slug)) .map((facility) => ({ url: buildAbsoluteUrl(`/golfbaner/${facility.slug}`), lastModified: maxDate([ facility.status_updated_at, facility.vtg_updated_at, facilityPageSourceLastModified, ]) || new Date(), changeFrequency: "daily" as const, priority: 0.7, })); const courseVisitRoutes = (await getCourseVisits()).map((article) => ({ url: buildAbsoluteUrl(`/banebesok/${article.slug}`), lastModified: article.updatedAt || article.publishedAt, changeFrequency: "monthly" as const, priority: 0.58, })); const opinionRoutes = (await getOpinionArticles()).map((article) => ({ url: buildAbsoluteUrl(`/meninger/${article.slug}`), lastModified: article.updatedAt || article.publishedAt, changeFrequency: "monthly" as const, priority: 0.56, })); return [...staticRoutes, ...placeRoutes, ...facilityRoutes, ...courseVisitRoutes, ...opinionRoutes]; }