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<Metadata> { + 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<Metadata> { + 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, }: {