2026-04-12 22:07:51 +02:00
import type { Metadata } from "next" ;
2026-04-12 15:57:37 +02:00
import { notFound } from "next/navigation" ;
2026-04-19 12:33:05 +02:00
import PlaceExplorer from "@/app/sted/[slug]/PlaceExplorer" ;
2026-04-12 15:57:37 +02:00
import {
2026-04-21 16:37:41 +02:00
buildPlaceAverageComparison ,
buildPlaceStats ,
buildPlaceStatsIntro ,
formatPlaceCount ,
formatPlaceCurrency ,
2026-04-12 15:57:37 +02:00
type FacilityRecord ,
enrichFacilities ,
filterFacilitiesByArea ,
getAvailablePlaceConfigs ,
getPlaceConfigFromSlug ,
2026-04-21 16:37:41 +02:00
getPlacePreposition ,
2026-04-12 15:57:37 +02:00
} from "@/app/facilityData" ;
import { API_URL } from "@/config/constants" ;
2026-04-12 22:07:51 +02:00
import {
createBreadcrumbJsonLd ,
createCollectionPageJsonLd ,
2026-04-18 09:00:16 +02:00
createItemListJsonLd ,
2026-04-12 22:07:51 +02:00
createPageMetadata ,
} from "@/app/seo" ;
2026-04-12 15:57:37 +02:00
export const dynamicParams = true ;
export const dynamic = "force-dynamic" ;
export async function generateStaticParams() {
return getAvailablePlaceConfigs ( ) . map ( ( slug ) = > ( { slug } ) ) ;
}
2026-04-12 22:07:51 +02:00
export async function generateMetadata ( {
params ,
} : {
params : Promise < { slug : string } > ;
} ) : Promise < Metadata > {
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 ,
2026-04-18 09:00:16 +02:00
description : ` ${ place . intro } TeeOff samler golfbaner i ${ place . label } med oppdatert banestatus og baneprofiler. ` ,
2026-04-12 22:07:51 +02:00
path : ` /sted/ ${ slug } ` ,
} ) ;
}
2026-04-12 15:57:37 +02:00
export default async function PlacePage ( { params } : { params : Promise < { slug : string } > } ) {
const { slug } = await params ;
const place = getPlaceConfigFromSlug ( slug ) ;
if ( ! place ) {
notFound ( ) ;
}
let facilities : FacilityRecord [ ] = [ ] ;
try {
const res = await fetch ( ` ${ API_URL } /facilities ` , {
next : { revalidate : 0 } ,
cache : "no-store" ,
} ) ;
if ( ! res . ok ) {
throw new Error ( ` API returnerte status ${ res . status } ` ) ;
}
facilities = await res . json ( ) ;
} catch ( error ) {
console . error ( "Kritisk feil ved henting av sted-data:" , error ) ;
facilities = [ ] ;
}
const safeData = Array . isArray ( facilities ) ? facilities : [ ] ;
2026-04-21 16:37:41 +02:00
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 ;
2026-04-12 22:07:51 +02:00
const collectionJsonLd = createCollectionPageJsonLd ( {
name : place.title ,
description : place.intro ,
path : ` /sted/ ${ slug } ` ,
} ) ;
2026-04-18 09:00:16 +02:00
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 ,
} ) ) ,
} ) ;
2026-04-12 22:07:51 +02:00
const breadcrumbJsonLd = createBreadcrumbJsonLd ( [
{ name : "Hjem" , path : "/" } ,
{ name : "Steder" , path : "/sted/norge" } ,
{ name : place.label , path : ` /sted/ ${ slug } ` } ,
] ) ;
2026-04-12 15:57:37 +02:00
return (
2026-04-12 22:07:51 +02:00
< >
< script
type = "application/ld+json"
dangerouslySetInnerHTML = { { __html : JSON.stringify ( collectionJsonLd ) } }
/ >
2026-04-18 09:00:16 +02:00
< script
type = "application/ld+json"
dangerouslySetInnerHTML = { { __html : JSON.stringify ( itemListJsonLd ) } }
/ >
2026-04-12 22:07:51 +02:00
< script
type = "application/ld+json"
dangerouslySetInnerHTML = { { __html : JSON.stringify ( breadcrumbJsonLd ) } }
/ >
< main className = "site-shell min-h-screen" >
< section className = "mx-auto max-w-[1400px] px-4 py-8 sm:px-6 sm:py-10 lg:px-8 lg:py-12" >
< div className = "max-w-4xl" >
< p className = "mb-3 text-[11px] font-extrabold uppercase tracking-[0.3em] text-[#6FA786]" > Steder < / p >
< h1 className = "section-title text-4xl text-[#112015] sm:text-5xl lg:text-6xl" > { place . title } < / h1 >
< p className = "mt-4 max-w-3xl text-base leading-8 text-[#617063]" > { place . intro } < / p >
< div className = "mt-5 flex flex-wrap gap-3" >
< span className = "rounded-full bg-white px-4 py-2 text-[11px] font-extrabold uppercase tracking-[0.18em] text-[#112015] shadow-sm" >
2026-04-21 16:37:41 +02:00
{ placeStats . facilityCount } golfanlegg
2026-04-12 22:07:51 +02:00
< / span >
< / div >
2026-04-12 15:57:37 +02:00
< / div >
2026-04-21 16:37:41 +02:00
< section className = "mt-8 overflow-hidden rounded-[2rem] border border-[#D7DED0] bg-white shadow-sm" >
< div className = "px-5 py-6 sm:px-6 lg:px-8 lg:py-8" >
< div className = "max-w-5xl" >
< p className = "text-[11px] font-extrabold uppercase tracking-[0.28em] text-[#6FA786]" > Nøkkeltall < / p >
< h2 className = "mt-2 text-3xl text-[#112015] sm:text-4xl" >
{ isNationalPlace ? "Fakta om golfanleggene i Norge" : ` Fakta om golfanleggene ${ placePreposition } ${ place . label } ` }
< / h2 >
< p className = "mt-4 text-base leading-8 text-[#213127]" > { placeStatsIntro } < / p >
< / div >
< / div >
< div className = "grid gap-px border-t border-[#D7DED0] bg-[#D7DED0] lg:grid-cols-3" >
< div className = "bg-white/88 px-5 py-5 sm:px-6 lg:px-8" >
< p className = "text-[11px] font-extrabold uppercase tracking-[0.2em] text-[#6FA786]" > Greenfee < / p >
< p className = "mt-2 text-2xl font-black text-[#112015]" >
{ placeStats . avgPrimetimeGreenfee !== null ? formatPlaceCurrency ( placeStats . avgPrimetimeGreenfee ) : "Mangler grunnlag" }
< / p >
< p className = "mt-2 text-sm leading-6 text-[#4F6052]" >
{ placeStats . avgPrimetimeGreenfee !== null
? "Gjennomsnittlig primetime-pris i høysesong."
: "Ingen tilstrekkelige greenfee-data tilgjengelig." }
< / p >
{ greenfeeComparison ? < p className = "mt-2 text-sm font-bold text-[#112015]" > { greenfeeComparison } < / p > : null }
< / div >
< div className = "bg-white/88 px-5 py-5 sm:px-6 lg:px-8" >
< p className = "text-[11px] font-extrabold uppercase tracking-[0.2em] text-[#6FA786]" > Medlemskap < / p >
< p className = "mt-2 text-2xl font-black text-[#112015]" >
{ placeStats . avgStandardMembership !== null ? formatPlaceCurrency ( placeStats . avgStandardMembership ) : "Mangler grunnlag" }
< / p >
< p className = "mt-2 text-sm leading-6 text-[#4F6052]" >
{ placeStats . avgStandardMembership !== null
? "Gjennomsnittlig standard medlemskap med spillerett."
: "Ingen tilstrekkelige medlemskapsdata tilgjengelig." }
< / p >
{ membershipComparison ? < p className = "mt-2 text-sm font-bold text-[#112015]" > { membershipComparison } < / p > : null }
< / div >
< div className = "bg-white/88 px-5 py-5 sm:px-6 lg:px-8" >
< p className = "text-[11px] font-extrabold uppercase tracking-[0.2em] text-[#6FA786]" > Hullstatistikk < / p >
< p className = "mt-2 text-sm leading-7 text-[#112015]" >
{ formatPlaceCount ( placeStats . hole18Count , "neuter" ) } 18 - hullsanlegg
< br / >
{ formatPlaceCount ( placeStats . hole9Count , "neuter" ) } 9 - hullsanlegg
< br / >
{ formatPlaceCount ( placeStats . hole6Count , "neuter" ) } 6 - hullsanlegg
< br / >
{ formatPlaceCount ( placeStats . hole27PlusCount , "neuter" ) } anlegg med 27 hull eller mer
< / p >
{ holeDistributionText ? < p className = "mt-3 text-sm leading-6 text-[#4F6052]" > { holeDistributionText } . < / p > : null }
{ shortestLongestText ? < p className = "mt-2 text-sm leading-6 text-[#4F6052]" > { shortestLongestText } < / p > : null }
< / div >
< / div >
< / section >
2026-04-19 12:33:05 +02:00
< PlaceExplorer
facilities = { safeData }
placeLabel = { place . label }
placeAreaFilter = { place . areaFilter }
placeTitle = { place . title }
/ >
2026-04-12 22:07:51 +02:00
< / section >
< / main >
< / >
2026-04-12 15:57:37 +02:00
) ;
}