diff --git a/docker-compose.yml b/docker-compose.yml index c8fbf27..ea5a310 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: build: ./frontend container_name: teeoff_frontend # NY LINJE: Tvinger produksjonsmodus for å stoppe WebSocket-feil og relasting - command: sh -c "npm run build && npm start" + command: npm start ports: - "3000:3000" # VIKTIG: Jeg har fjernet "- ./frontend:/app" her for å sikre stabilitet diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6308ff9..6c5403f 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -9,5 +9,8 @@ RUN npm install # Kopier resten av koden COPY . . -# Vi starter IKKE serveren i "dev"-modus (utviklingsmodus). -CMD ["sh", "-c", "npm run build && npm start"] +# BYGG koden her (kjøres jyb én gang når imaget bygges +RUN npm run build + +# Vi starter serveren i "produksjons"-modus (utviklingsmodus). +CMD ["npm", "start"] diff --git a/frontend/public/android-chrome-192x192.png b/frontend/public/android-chrome-192x192.png new file mode 100755 index 0000000..e2e214e Binary files /dev/null and b/frontend/public/android-chrome-192x192.png differ diff --git a/frontend/public/android-chrome-512x512.png b/frontend/public/android-chrome-512x512.png new file mode 100755 index 0000000..f8ea249 Binary files /dev/null and b/frontend/public/android-chrome-512x512.png differ diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100755 index 0000000..462d3da Binary files /dev/null and b/frontend/public/apple-touch-icon.png differ diff --git a/frontend/public/site.webmanifest b/frontend/public/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/frontend/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/frontend/src/app/FacilitySearch.tsx b/frontend/src/app/FacilitySearch.tsx index 5a52c94..dbb2247 100755 --- a/frontend/src/app/FacilitySearch.tsx +++ b/frontend/src/app/FacilitySearch.tsx @@ -235,6 +235,19 @@ const sanitizeHref = (value: string) => { return /^(https?:|mailto:|tel:|\/|#)/i.test(href) ? href : "#"; }; +const isInternalTeeoffHref = (href: string) => { + if (!href || href.startsWith("/") || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) { + return true; + } + + try { + const url = new URL(href, "https://nye.teeoff.no"); + return url.hostname === "teeoff.no" || url.hostname.endsWith(".teeoff.no"); + } catch { + return false; + } +}; + const sanitizeRichText = (value: string | null | undefined) => { const source = String(value || "").replace(/\r\n?/g, "\n"); if (!source.trim()) return ""; @@ -262,7 +275,10 @@ const sanitizeRichText = (value: string | null | undefined) => { .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] || "#"); - return keep(``); + if (isInternalTeeoffHref(href)) { + return keep(``); + } + return keep(``); }) .replace(/<\s*\/\s*a\s*>/gi, () => keep("")); diff --git a/frontend/src/app/android-chrome-192x192.png b/frontend/src/app/android-chrome-192x192.png new file mode 100755 index 0000000..e2e214e Binary files /dev/null and b/frontend/src/app/android-chrome-192x192.png differ diff --git a/frontend/src/app/android-chrome-512x512.png b/frontend/src/app/android-chrome-512x512.png new file mode 100755 index 0000000..f8ea249 Binary files /dev/null and b/frontend/src/app/android-chrome-512x512.png differ diff --git a/frontend/src/app/apple-touch-icon.png b/frontend/src/app/apple-touch-icon.png new file mode 100755 index 0000000..462d3da Binary files /dev/null and b/frontend/src/app/apple-touch-icon.png differ diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/frontend/src/app/favicon.ico and /dev/null differ diff --git a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx index 6db4bfa..afc9d0c 100644 --- a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx +++ b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx @@ -47,6 +47,19 @@ const sanitizeHref = (value: string) => { return /^(https?:|mailto:|tel:|\/|#)/i.test(href) ? href : "#"; }; +const isInternalTeeoffHref = (href: string) => { + if (!href || href.startsWith("/") || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("tel:")) { + return true; + } + + try { + const url = new URL(href, "https://nye.teeoff.no"); + return url.hostname === "teeoff.no" || url.hostname.endsWith(".teeoff.no"); + } catch { + return false; + } +}; + const sanitizeRichText = (value: string | null | undefined) => { const source = String(value || "").replace(/\r\n?/g, "\n"); if (!source.trim()) return ""; @@ -74,7 +87,10 @@ const sanitizeRichText = (value: string | null | undefined) => { .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] || "#"); - return keep(``); + if (isInternalTeeoffHref(href)) { + return keep(``); + } + return keep(``); }) .replace(/<\s*\/\s*a\s*>/gi, () => keep("")); diff --git a/frontend/src/app/golfbaner/[slug]/opengraph-image.tsx b/frontend/src/app/golfbaner/[slug]/opengraph-image.tsx new file mode 100755 index 0000000..41dcc5f --- /dev/null +++ b/frontend/src/app/golfbaner/[slug]/opengraph-image.tsx @@ -0,0 +1,133 @@ +import { ImageResponse } from "next/og"; +import { API_URL } from "@/config/constants"; +import { buildAbsoluteUrl } from "@/app/seo"; + +export const size = { + width: 1200, + height: 630, +}; + +export const contentType = "image/png"; + +type OpenGraphImageProps = { + params: Promise<{ slug: string }>; +}; + +async function getFacility(slug: string) { + const res = await fetch(`${API_URL}/facilities/${slug}`, { cache: "no-store" }); + return res.json(); +} + +export default async function OpenGraphImage({ params }: OpenGraphImageProps) { + const { slug } = await params; + const facility = await getFacility(slug); + + const title = facility?.name || "TeeOff"; + const location = [facility?.city, facility?.county].filter(Boolean).join(" · ") || "Norske golfanlegg"; + const imageUrl = facility?.image_url ? buildAbsoluteUrl(facility.image_url) : null; + + return new ImageResponse( + ( +