import type { Metadata } from "next"; import { notFound } from "next/navigation"; import PlaceExplorer from "@/app/sted/[slug]/PlaceExplorer"; import { buildPlaceAverageComparison, buildPlaceStats, buildPlaceStatsIntro, formatPlaceCount, formatPlaceCurrency, type FacilityRecord, enrichFacilities, filterFacilitiesByArea, getPlaceConfigFromSlug, getPlacePreposition, } from "@/app/facilityData"; import { API_URL } from "@/config/constants"; import { createBreadcrumbJsonLd, createCollectionPageJsonLd, createItemListJsonLd, createPageMetadata, } from "@/app/seo"; import { fetchPublicFacilities } from "@/app/publicFacilities"; type PlacePageData = { slug?: string; factbox_intro_html?: string | null; updated_at?: string | null; }; const escapeHtml = (value: string) => value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); const sanitizeHref = (value: string) => { const href = value.trim(); return /^(https?:|mailto:|tel:|\/|#)/i.test(href) ? href : "#"; }; const isInternalTeeoffHref = (href: string) => /^(\/|#|mailto:|tel:)/i.test(href) || /^https?:\/\/([^/]+\.)?teeoff\.no(\/|$)/i.test(href); const sanitizePlaceRichText = (value: string | null | undefined) => { const source = String(value || "").replace(/\r\n?/g, "\n"); if (!source.trim()) return ""; const placeholders = new Map(); let index = 0; const keep = (html: string) => { const key = `__HTML_TOKEN_${index++}__`; placeholders.set(key, html); return key; }; let safe = source .replace(/<\s*br\s*\/?\s*>/gi, () => keep("
")) .replace(/<\s*(strong|b)\s*>/gi, () => keep("")) .replace(/<\s*\/\s*(strong|b)\s*>/gi, () => keep("")) .replace(/<\s*(em|i)\s*>/gi, () => keep("")) .replace(/<\s*\/\s*(em|i)\s*>/gi, () => keep("")) .replace(/<\s*u\s*>/gi, () => keep("")) .replace(/<\s*\/\s*u\s*>/gi, () => keep("")) .replace(/<\s*(p|blockquote)\s*>/gi, (_, tag: string) => keep(`<${tag.toLowerCase()}>`)) .replace(/<\s*\/\s*(p|blockquote)\s*>/gi, (_, tag: string) => keep(``)) .replace(/<\s*(ul|ol)\s*>/gi, (_, tag: string) => keep(`<${tag.toLowerCase()}>`)) .replace(/<\s*\/\s*(ul|ol)\s*>/gi, (_, tag: string) => keep(``)) .replace(/<\s*li\s*>/gi, () => keep("
  • ")) .replace(/<\s*\/\s*li\s*>/gi, () => keep("
  • ")) .replace(/<\s*(h2|h3)\s*>/gi, (_, tag: string) => keep(`<${tag.toLowerCase()}>`)) .replace(/<\s*\/\s*(h2|h3)\s*>/gi, (_, tag: string) => keep(``)) .replace(/<\s*a\b([^>]*)>/gi, (_, attrs: string) => { const hrefMatch = attrs.match(/href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i); const href = sanitizeHref(hrefMatch?.[1] || hrefMatch?.[2] || hrefMatch?.[3] || "#"); if (isInternalTeeoffHref(href)) { return keep(``); } return keep(``); }) .replace(/<\s*\/\s*a\s*>/gi, () => keep("")); safe = escapeHtml(safe).replace(/\n/g, "
    "); for (const [token, html] of placeholders) { safe = safe.replaceAll(token, html); } return safe; }; export const dynamicParams = true; export const revalidate = 3600; export async function generateMetadata({ params, }: { params: Promise<{ slug: string }>; }): Promise { const { slug } = await params; const place = getPlaceConfigFromSlug(slug); if (!place) { return createPageMetadata({ title: "Sted ikke funnet", description: "Denne stedssiden finnes ikke på TeeOff.", path: `/sted/${slug}`, }); } return createPageMetadata({ title: place.title, description: `${place.intro} TeeOff samler golfbaner i ${place.label} med oppdatert banestatus og baneprofiler.`, path: `/sted/${slug}`, }); } export default async function PlacePage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const place = getPlaceConfigFromSlug(slug); if (!place) { notFound(); } let placePage: PlacePageData | null = null; const facilities = await fetchPublicFacilities("place", revalidate); try { const res = await fetch(`${API_URL}/place-pages/${slug}`, { next: { revalidate }, }); if (!res.ok) { throw new Error(`API returnerte status ${res.status}`); } placePage = await res.json(); } catch (error) { console.error("Kritisk feil ved henting av sted-sideinnhold:", error); placePage = null; } const safeData = Array.isArray(facilities) ? facilities : []; const enrichedFacilities = enrichFacilities(safeData); const facilitiesInPlace = filterFacilitiesByArea(enrichedFacilities, place.areaFilter); const placeStats = buildPlaceStats(facilitiesInPlace); const nationalStats = buildPlaceStats(enrichedFacilities); const placeStatsIntro = buildPlaceStatsIntro(place.label, placeStats); const isNationalPlace = place.slug === "norge"; const placePreposition = isNationalPlace ? "i" : getPlacePreposition(place.label); const greenfeeComparison = isNationalPlace ? null : buildPlaceAverageComparison(placeStats.avgPrimetimeGreenfee, nationalStats.avgPrimetimeGreenfee); const membershipComparison = isNationalPlace ? null : buildPlaceAverageComparison(placeStats.avgStandardMembership, nationalStats.avgStandardMembership); const holeDistributionParts = [ `${formatPlaceCount(placeStats.par3HoleCount)} par 3-hull`, `${formatPlaceCount(placeStats.par4HoleCount)} par 4-hull`, `${formatPlaceCount(placeStats.par5HoleCount)} par 5-hull`, placeStats.par6HoleCount > 0 ? `${formatPlaceCount(placeStats.par6HoleCount)} par 6-hull` : null, ].filter((part): part is string => Boolean(part)); const holeDistributionText = holeDistributionParts.length > 1 ? `${holeDistributionParts.slice(0, -1).join(", ")} og ${holeDistributionParts.at(-1)}` : holeDistributionParts[0] ?? null; const shortestLongestText = placeStats.shortestHoleMeters !== null && placeStats.longestHoleMeters !== null ? `Det korteste golfhullet ${placePreposition} ${place.label} er ${placeStats.shortestHoleMeters} meter, mens det lengste er ${placeStats.longestHoleMeters} meter.` : null; const collectionJsonLd = createCollectionPageJsonLd({ name: place.title, description: place.intro, path: `/sted/${slug}`, }); const itemListJsonLd = createItemListJsonLd({ name: place.title, path: `/sted/${slug}`, items: facilitiesInPlace .filter((facility) => facility?.slug && facility?.name) .map((facility) => ({ name: facility.name, path: `/golfbaner/${facility.slug}`, description: facility.description, })), }); const breadcrumbJsonLd = createBreadcrumbJsonLd([ { name: "Hjem", path: "/" }, { name: "Steder", path: "/sted/norge" }, { name: place.label, path: `/sted/${slug}` }, ]); return ( <>