import type { Metadata } from "next"; import { FALLBACK_IMAGE } from "@/config/constants"; import type { FacilityRecord } from "@/app/facilityData"; type MetadataInput = { title: string; description: string; path?: string; image?: string | null; type?: "website" | "article"; }; type BreadcrumbItem = { name: string; path: string; }; type CollectionPageInput = { name: string; description: string; path: string; }; type SocialLink = { url?: string | null; }; type FacilitySeoRecord = FacilityRecord & { address?: string | null; email?: string | null; social_links?: unknown; }; export const SITE_NAME = "TeeOff"; export const SITE_URL = (process.env.NEXT_PUBLIC_SITE_URL || "https://nye.teeoff.no").replace(/\/$/, ""); export const DEFAULT_DESCRIPTION = "Oppdatert banestatus, priser, Veien til Golf og informasjon om norske golfanlegg samlet på ett sted."; export const DEFAULT_OG_IMAGE = buildAbsoluteUrl(FALLBACK_IMAGE); export const ORGANIZATION_ID = `${SITE_URL}#organization`; export const WEBSITE_ID = `${SITE_URL}#website`; const SAME_AS_LINKS = [ "https://www.facebook.com/TeeOff.norge/", "https://twitter.com/TeeOffno", "https://www.instagram.com/teeoffno/", ]; export function buildAbsoluteUrl(path = "/") { if (!path) return SITE_URL; if (/^https?:\/\//i.test(path)) return path; return `${SITE_URL}${path.startsWith("/") ? path : `/${path}`}`; } export function resolveImageUrl(image?: string | null) { if (!image) return DEFAULT_OG_IMAGE; return buildAbsoluteUrl(image); } export function stripHtml(value: string | null | undefined) { return String(value || "") .replace(/<[^>]+>/g, " ") .replace(/ /gi, " ") .replace(/\s+/g, " ") .trim(); } export function trimDescription(value: string | null | undefined, maxLength = 160) { const plain = stripHtml(value); if (plain.length <= maxLength) return plain; return `${plain.slice(0, maxLength - 1).trimEnd()}…`; } export function createPageMetadata({ title, description, path = "/", image, type = "website", }: MetadataInput): Metadata { const ogImage = resolveImageUrl(image); return { title, description, alternates: { canonical: path, }, openGraph: { type, locale: "nb_NO", siteName: SITE_NAME, title, description, url: buildAbsoluteUrl(path), images: [ { url: ogImage, width: 1600, height: 900, alt: title, }, ], }, twitter: { card: "summary_large_image", site: "@TeeOffno", creator: "@TeeOffno", title, description, images: [ogImage], }, }; } export function createOrganizationJsonLd() { return { "@context": "https://schema.org", "@type": "Organization", "@id": ORGANIZATION_ID, name: SITE_NAME, url: SITE_URL, logo: { "@type": "ImageObject", url: buildAbsoluteUrl("/icons/cropped-siteicon-1.png"), }, sameAs: SAME_AS_LINKS, }; } export function createWebsiteJsonLd() { return { "@context": "https://schema.org", "@type": "WebSite", "@id": WEBSITE_ID, url: SITE_URL, name: SITE_NAME, inLanguage: "nb-NO", publisher: { "@id": ORGANIZATION_ID, }, }; } export function createBreadcrumbJsonLd(items: BreadcrumbItem[]) { return { "@context": "https://schema.org", "@type": "BreadcrumbList", itemListElement: items.map((item, index) => ({ "@type": "ListItem", position: index + 1, name: item.name, item: buildAbsoluteUrl(item.path), })), }; } export function createCollectionPageJsonLd({ name, description, path }: CollectionPageInput) { return { "@context": "https://schema.org", "@type": "CollectionPage", name, description, url: buildAbsoluteUrl(path), inLanguage: "nb-NO", isPartOf: { "@id": WEBSITE_ID, }, about: { "@id": ORGANIZATION_ID, }, }; } export function createFacilityJsonLd(facility: FacilitySeoRecord) { const socialLinks = parseJson(facility.social_links, []); const sameAs = [facility.website_url, ...socialLinks.map((entry) => entry?.url || null)].filter(Boolean); return { "@context": "https://schema.org", "@type": "GolfCourse", "@id": buildAbsoluteUrl(`/golfbaner/${facility.slug}#golfcourse`), name: facility.name, description: trimDescription(facility.description) || `${facility.name} er et golfanlegg på TeeOff med oppdatert banestatus og praktisk klubbinfo.`, url: buildAbsoluteUrl(`/golfbaner/${facility.slug}`), image: resolveImageUrl(facility.image_url), telephone: facility.phone || undefined, email: facility.email || undefined, address: facility.address || facility.city || facility.county ? { "@type": "PostalAddress", streetAddress: facility.address || undefined, addressLocality: facility.city || undefined, addressRegion: facility.county || undefined, addressCountry: "NO", } : undefined, geo: typeof facility.lat === "number" && typeof facility.lng === "number" ? { "@type": "GeoCoordinates", latitude: facility.lat, longitude: facility.lng, } : undefined, sameAs: sameAs.length > 0 ? sameAs : undefined, isPartOf: { "@id": WEBSITE_ID, }, }; } function parseJson(value: unknown, fallback: T): T { if (!value) return fallback; if (typeof value === "object") return value as T; try { return JSON.parse(String(value)) as T; } catch { return fallback; } }