Nye-TeeOff/frontend/src/app/sitemap.ts

244 lines
6.6 KiB
TypeScript
Executable file

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<string | Date | null | undefined>) {
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<MetadataRoute.Sitemap> {
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];
}