From 9aea49ba42f4a9a2f9c48bac319904223434b71d Mon Sep 17 00:00:00 2001 From: Nils Briggen Date: Fri, 12 Jun 2026 01:22:36 +0200 Subject: [PATCH] Shared links unfurl: per-route OG meta for match and team pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crawler-visible + og:title/description are injected into the SPA shell server-side: match links carry the score or the model's probability split, team links the current title odds — honest framing included. Plain shell for every other route. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- server/src/index.ts | 49 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index b685a6a..e2988ba 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { existsSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import Fastify from 'fastify'; import type { FastifyRequest } from 'fastify'; import fastifyWebsocket from '@fastify/websocket'; @@ -184,12 +184,53 @@ export function buildServer() { }, 24 * 60 * 60 * 1000); app.addHook('onClose', async () => clearInterval(refitTimer)); - // ---- static SPA (prod) with history fallback ---- + // ---- static SPA (prod) with history fallback + per-route OG meta ---- if (existsSync(STATIC_DIR)) { void app.register(fastifyStatic, { root: STATIC_DIR, wildcard: false }); + + // Shared links unfurl with the model's numbers: swap <title> and inject + // og: meta for /match/:num and /team/:name before serving the SPA shell. + const indexHtml = readFileSync(`${STATIC_DIR}/index.html`, 'utf8'); + const esc = (s: string): string => s.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"'); + const withMeta = (title: string, desc: string): string => + indexHtml.replace( + /<title>[^<]*<\/title>/, + `<title>${esc(title)}\n \n \n `, + ); + const ogFor = (url: string): string | null => { + try { + const mMatch = /^\/match\/(\d+)(?:\?|$)/.exec(url); + if (mMatch) { + const f = state.allFixtures().find((x) => x.num === Number(mMatch[1])); + if (!f) return null; + const pred = model.current().matches.find((m) => m.num === f.num); + const score = f.homeScore != null && f.status !== 'scheduled' ? ` ${f.homeScore}–${f.awayScore}` : ''; + const odds = pred ? ` · Model: ${Math.round(pred.probs.home * 100)}/${Math.round(pred.probs.draw * 100)}/${Math.round(pred.probs.away * 100)}` : ''; + return withMeta( + `${f.home.label}${score || ' vs'} ${f.away.label} — Cup26`, + `World Cup 2026 · ${f.venue}${odds} (model probabilities, not betting advice)`, + ); + } + const mTeam = /^\/team\/([^/?]+)(?:\?|$)/.exec(url); + if (mTeam) { + const team = decodeURIComponent(mTeam[1]!); + const odds = model.current().odds.find((o) => o.team === team); + if (!odds) return null; + return withMeta( + `${team} — Cup26`, + `World Cup 2026 · title odds ${(odds.champion * 100).toFixed(1)}% by the Cup26 model`, + ); + } + } catch { /* fall through to the plain shell */ } + return null; + }; + app.setNotFoundHandler((req, reply) => { - if (req.raw.url?.startsWith('/ws')) return reply.code(426).send('Upgrade Required'); - if (req.raw.url?.startsWith('/api')) return reply.code(404).send({ error: 'not found' }); + const url = req.raw.url ?? '/'; + if (url.startsWith('/ws')) return reply.code(426).send('Upgrade Required'); + if (url.startsWith('/api')) return reply.code(404).send({ error: 'not found' }); + const html = ogFor(url); + if (html) return reply.type('text/html; charset=utf-8').send(html); return reply.sendFile('index.html'); }); }