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
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(/
+ indexHtml.replace(
+ /[^<]*<\/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');
});
}