ADR-018 er nå helt ferdig — backend, begge frontend-skjermer, og nå Open Graph-metadata, alt live samme dag. Oppsummert:

Én reell driftsfeil funnet før den nådde produksjon: generateMetadata() kjører server-side ved forespørselstid — går derfor ikke gjennom next.config.mjs sin rewrites() (som bare gjelder nettleser-trafikk). Den trengte TEECUP_API_ORIGIN direkte, men den variabelen fantes kun i Dockerfile sitt build-steg — ENV fra ett steg arves ikke til det neste i en multi-stage build. Rettet ved å sette den på nytt i runner-steget.

Verifisert presist, ikke bare at det kompilerte: bygget det ekte produksjonsimaget (ikke dev-server), pekte det mot en scratch-backend med ekte data, og hentet den faktiske server-rendrede HTML-en — bekreftet ekte og:title/og:description, og en trygg fallback-tittel for en ukjent turnering.

teeoff.no upåvirket gjennom hele runden.
This commit is contained in:
Erol Haagenrud 2026-07-18 10:58:36 +02:00
parent 8445b137ec
commit 6d84aafef8
7 changed files with 118 additions and 9 deletions

View file

@ -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",

View file

@ -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 `<title>`/`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

View file

@ -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()``/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. |

View file

@ -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 ./

View file

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

View file

@ -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',

View file

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