244 lines
6.6 KiB
TypeScript
Executable file
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];
|
|
}
|