diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index e8b09de..4e1db1c 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -203,7 +203,9 @@
"Bash(curl -s -o /dev/null -w '%{http_code}\\\\n' https://teecup.teeoff.no/t/00000000-0000-0000-0000-000000000000)",
"Bash(python3 -m zipfile -l \"tee-cup-login-screen \\(4\\).zip\")",
"Bash(rm -f /opt/teecup/.env.scratch /tmp/cowner.txt)",
- "Bash(curl -s -o /dev/null -w '/clubs/ukjent: %{http_code}\\\\n' https://teecup.teeoff.no/clubs/ukjent-slug-garantert)"
+ "Bash(curl -s -o /dev/null -w '/clubs/ukjent: %{http_code}\\\\n' https://teecup.teeoff.no/clubs/ukjent-slug-garantert)",
+ "Bash(rm -f /opt/teecup/.env.scratch /tmp/ogowner.txt)",
+ "Bash(curl -s https://teecup.teeoff.no/t/00000000-0000-0000-0000-000000000000)"
],
"additionalDirectories": [
"/opt/teeoff/deploy",
diff --git a/CLAUDE.md b/CLAUDE.md
index 08d517a..88e13c3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -580,12 +580,34 @@ Ferdig og verifisert:
**ADR-018s planlagte skjermer er dermed komplette.** Gjenstår: Open
Graph-metadata for deling, MinIO/bilder — begge bevisst egne, senere
runder.
+- **Open Graph-metadata LIVE (2026-07-18), samme dag — ADR-018 dermed
+ helt ferdig:** `generateMetadata()` lagt til på `/t/[id]` og
+ `/clubs/[slug]` (ekte tittel/beskrivelse fra API-et, `og:site_name`,
+ trygg fallback-tittel ved ukjent id/slug — feil her skal ALDRI hindre
+ selve siden i å laste).
+ **Reell driftsfeil funnet FØR den nådde produksjon, ikke etter:**
+ `generateMetadata()` kjører server-side ved REQUEST-tid, ikke i
+ nettleseren — går derfor IKKE gjennom `next.config.mjs` sin
+ `rewrites()` (som kun gjelder nettleser-trafikk inn til Next.js-
+ serveren). Måtte derfor lese `TEECUP_API_ORIGIN` direkte, men den
+ variabelen fantes KUN i `Dockerfile` sitt builder-steg — `ENV` satt i
+ ett `FROM`-steg arves ikke til et senere. Rettet ved å sette samme
+ `ARG`/`ENV` på nytt i runner-steget også.
+ **Verifisert presist at fiksen faktisk virker, ikke bare at bygget gikk
+ gjennom:** bygget det EKTE produksjonsimaget (ikke dev-server) pekt mot
+ en scratch-backend med en ekte offentlig turnering+org, hentet den
+ faktiske server-rendrede HTML-en og bekreftet ekte `
`/`og:title`/
+ `og:description` — ikke bare at TypeScript kompilerte. Ukjent
+ turnering-id ga korrekt trygg fallback-tittel.
+ **Bevisst utenfor omfang:** `og:image` — ingen ekte bilde finnes ennå
+ (MinIO-runden). La til `metadataBase` i `app/layout.tsx` nå likevel, som
+ forarbeid slik at et fremtidig relativt bilde-URL løses riktig uten en
+ egen fiks da.
+ **Rullet ut live**, kun `teecup_frontend`, `teeoff.no` upåvirket.
Neste steg:
-1. Open Graph-metadata (og:title/og:image/og:description) på
- `/t/[id]`/`/clubs/[slug]` for delingsforhåndsvisning, deretter
- MinIO/bildeopplasting som egen runde — de to gjenværende punktene fra
- ADR-018.
+1. MinIO/bildeopplasting (hero-bilder, sponsorlogoer, og:image) — siste
+ gjenstående punkt fra ADR-018, egen runde.
2. Flere V0-skjermer (økt/program, blind draw, scorekort, leaderboard) —
samme mønster: design i V0 (fortsett i samme prosjekt), FORVENT en full
re-eksport hver gang — diff mot live-treet i et scratch-område før noe
diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md
index 72c034a..9f737fc 100644
--- a/FEATURE_BACKLOG.md
+++ b/FEATURE_BACKLOG.md
@@ -405,11 +405,11 @@
---
-## Landingssider (turnering + organisasjon) — ADR-018 ✅ LIVE 2026-07-18
+## Landingssider (turnering + organisasjon) — ADR-018 ✅ HELT FERDIG 2026-07-18
Reist av brukeren 2026-07-18, rett etter registrerings-ADR-en (ADR-017).
-Backend OG begge planlagte frontend-skjermer bygget og live samme dag.
-Open Graph-metadata og MinIO/bilder gjenstår, bevisst egne runder.
+Backend, begge frontend-skjermer OG Open Graph-metadata bygget og live
+samme dag. Kun MinIO/bilder (bevisst egen runde) gjenstår.
| Del | Status | Notat |
|---|---|---|
@@ -421,7 +421,7 @@ Open Graph-metadata og MinIO/bilder gjenstår, bevisst egne runder.
| Lesbar URL (slug) for organisasjon | ✅ | Fantes faktisk allerede i skjemaet siden migrasjon 001 (oversett, funnet da migrasjon 009 feilet mot scratch — se CLAUDE.md-status). Kun `CHECK`-constraints lagt til i 009. |
| Organisator kan faktisk SETTE disse feltene | ✅ | Implisitt hull fylt under bygging: `PATCH /orgs/{id}/tournaments/{id}` (visibility/description/registrering), `PATCH /orgs/{id}` (slug/public_profile), full sponsor-CRUD. |
| Bilder (hero, sponsorlogoer) | 📋 | Bevisst utsatt (brukerens valg). `hero_image_key`/`logo_key`-kolonner finnes allerede (inerte), men krever MinIO — ikke satt opp. Egen, senere runde. |
-| Del-metadata (Open Graph: og:title/og:image/og:description) | 📋 | Frontend-arbeid, ikke gjort ennå. |
+| Del-metadata (Open Graph: og:title/og:description) | ✅ | `generateMetadata()` på `/t/[id]`+`/clubs/[slug]`, ekte data fra API-et, verifisert mot produksjonsimaget. `og:image` gjenstår (MinIO). |
| Blind draw-skjuling på offentlig side | ✅ | Arves automatisk via delt `_fetch_sessions()`-hjelpefunksjon (ADR-018 Beslutning E) — ikke reimplementert. |
| Antall påmeldte / ledige plasser vist åpent | ✅ | `confirmed_count` i `GET /public/tournaments/{id}`. |
| Frontend: turnering-landingsside + påmeldingsskjema, LIVE | ✅ | `/t/[id]`. Tre bekreftelsestilstander (bekreftet/venteliste/godkjenning venter). Verifisert med ekte `POST`-registrering mot scratch. |
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 3b827aa..5b2dc19 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -19,6 +19,13 @@ RUN pnpm build
FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
+# ENV fra builder-steget arves IKKE til et nytt FROM -- må settes på nytt her.
+# Trengs av server-side kode som kjører ved REQUEST-tid, ikke build-tid, f.eks.
+# generateMetadata() sine egne fetch-kall (Open Graph-deling) -- disse går
+# IKKE gjennom next.config.mjs sin rewrites() (som kun gjelder nettleser-
+# trafikk inn til Next.js-serveren), så de trenger API-origin direkte.
+ARG TEECUP_API_ORIGIN=http://teecup_api:8000
+ENV TEECUP_API_ORIGIN=$TEECUP_API_ORIGIN
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
diff --git a/frontend/app/clubs/[slug]/page.tsx b/frontend/app/clubs/[slug]/page.tsx
index 82a1710..d0008bc 100644
--- a/frontend/app/clubs/[slug]/page.tsx
+++ b/frontend/app/clubs/[slug]/page.tsx
@@ -1,5 +1,39 @@
+import type { Metadata } from "next"
import { PublicClub } from "@/components/public-club"
+// Server-side -- går IKKE gjennom next.config.mjs sin rewrites() (kun for
+// nettleser-trafikk inn til Next.js-serveren). Peker direkte på API-et.
+const API_ORIGIN = process.env.TEECUP_API_ORIGIN || "http://localhost:8000"
+
+type PublicOrgInfo = {
+ name: string
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ slug: string }>
+}): Promise {
+ const { slug } = await params
+ try {
+ const res = await fetch(`${API_ORIGIN}/public/orgs/${slug}`, { cache: "no-store" })
+ if (!res.ok) {
+ return { title: "Klubb | TeeCup" }
+ }
+ const data: PublicOrgInfo = await res.json()
+ const title = `${data.name} | TeeCup`
+ const description = `Se ${data.name} sine turneringer på TeeCup.`
+ return {
+ title,
+ description,
+ openGraph: { title, description, siteName: "TeeCup", type: "website" },
+ twitter: { card: "summary", title, description },
+ }
+ } catch {
+ return { title: "TeeCup" }
+ }
+}
+
export default async function ClubPage({
params,
}: {
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 140bca5..27ce342 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -9,6 +9,10 @@ const nunito = Nunito({
})
export const metadata: Metadata = {
+ // Trengs for at relative og:image-URL-er (MinIO-runden, ikke bygget ennå)
+ // skal løses til en fullstendig URL -- uten denne advarer Next.js ved
+ // build og faller tilbake til en gjettet verdi.
+ metadataBase: new URL('https://teecup.teeoff.no'),
title: 'TeeCup — Logg inn',
description: 'Logg inn på TeeCup, din golfturnerings-app. Passordløs innlogging med magisk lenke.',
generator: 'v0.app',
diff --git a/frontend/app/t/[id]/page.tsx b/frontend/app/t/[id]/page.tsx
index 5bd7006..46a0b94 100644
--- a/frontend/app/t/[id]/page.tsx
+++ b/frontend/app/t/[id]/page.tsx
@@ -1,5 +1,45 @@
+import type { Metadata } from "next"
import { PublicTournament } from "@/components/public-tournament"
+// Server-side -- IKKE nettleser-fetch, går derfor IKKE gjennom
+// next.config.mjs sin rewrites() (kun for trafikk inn til Next.js-serveren
+// fra en klient). Peker direkte på API-et.
+const API_ORIGIN = process.env.TEECUP_API_ORIGIN || "http://localhost:8000"
+
+type PublicTournamentInfo = {
+ name: string
+ organization_name: string
+ description: string | null
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ id: string }>
+}): Promise {
+ const { id } = await params
+ try {
+ const res = await fetch(`${API_ORIGIN}/public/tournaments/${id}`, { cache: "no-store" })
+ if (!res.ok) {
+ return { title: "Turnering | TeeCup" }
+ }
+ const data: PublicTournamentInfo = await res.json()
+ const title = `${data.name} | ${data.organization_name}`
+ const description =
+ data.description?.trim() || `${data.name} arrangeres av ${data.organization_name} på TeeCup.`
+ return {
+ title,
+ description,
+ openGraph: { title, description, siteName: "TeeCup", type: "website" },
+ twitter: { card: "summary", title, description },
+ }
+ } catch {
+ // Feil her skal ALDRI hindre selve siden i å laste -- kun forringe
+ // delingsforhåndsvisningen, som fortsatt får en trygg standardtittel.
+ return { title: "TeeCup" }
+ }
+}
+
export default async function PublicTournamentPage({
params,
}: {