Python 51.6%
TypeScript 46.7%
CSS 1.7%
1import { ImageResponse } from "next/og";2import { ENGINE_URL, type Forecast } from "@/lib/api";3import { PARTIES } from "@/lib/parties";45export const runtime = "nodejs";6export const alt = "QC26 — Projection de l'élection québécoise 2026";7export const size = { width: 1200, height: 630 };8export const contentType = "image/png";910export default async function Image() {11 let f: Forecast | null = null;12 try { f = await fetch(`${ENGINE_URL}/api/forecast/latest`, { next: { revalidate: 300 } }).then((r) => r.json()); } catch { f = null; }13 const rows = f ? Object.entries(f.parties).filter(([id]) => id !== "aut").sort((a, b) => b[1].seatsMean - a[1].seatsMean) : [];14 const color = (id: string) => PARTIES.find((p) => p.id === id)?.color ?? "#999";15 const date = f?.completedAt ? new Date(f.completedAt).toLocaleDateString("fr-CA", { day: "numeric", month: "long", timeZone: "America/Toronto" }) : "";16 return new ImageResponse(17 (18 <div style={{ display: "flex", width: "100%", height: "100%", flexDirection: "column", background: "#F7F8FA", color: "#111318", padding: 56, fontFamily: "Inter, sans-serif" }}>19 <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>20 <div style={{ display: "flex", alignItems: "baseline", fontSize: 44, fontWeight: 800, letterSpacing: -2 }}><span>QC</span><span style={{ width: 4, height: 36, background: "#2563EB", margin: "0 6px", borderRadius: 2 }} /><span style={{ color: "#2563EB" }}>26</span></div>21 <div style={{ display: "flex", fontSize: 20, color: "#656B76", letterSpacing: 3, textTransform: "uppercase" }}>Projection · {date}</div>22 </div>23 <div style={{ display: "flex", flex: 1, marginTop: 30, gap: 40 }}>24 <div style={{ display: "flex", flexDirection: "column", justifyContent: "center", flex: 1 }}>25 {rows.map(([id, v]) => (26 <div key={id} style={{ display: "flex", alignItems: "center", gap: 18, marginBottom: 14 }}>27 <div style={{ display: "flex", width: 90, fontSize: 30, fontWeight: 700, color: color(id) }}>{id.toUpperCase()}</div>28 <div style={{ display: "flex", flex: 1, height: 26, background: "#E9ECF0", borderRadius: 6, position: "relative" }}>29 <div style={{ display: "flex", width: `${(v.seatsMedian / (f?.summary.seats_total ?? 127)) * 100}%`, height: 26, background: color(id), borderRadius: 6 }} />30 <div style={{ display: "flex", position: "absolute", left: `${((f?.summary.majority ?? 64) / (f?.summary.seats_total ?? 127)) * 100}%`, top: -6, width: 3, height: 38, background: "#111318" }} />31 </div>32 <div style={{ display: "flex", width: 150, fontSize: 34, fontWeight: 800, textAlign: "right" }}>{v.seatsMedian}<span style={{ fontSize: 18, color: "#656B76", fontWeight: 500 }}> {v.seats80[0]}–{v.seats80[1]}</span></div>33 </div>34 ))}35 </div>36 </div>37 <div style={{ display: "flex", justifyContent: "space-between", fontSize: 20, color: "#656B76" }}><span>Québec 2026 · 5 octobre · {f?.summary.seats_total ?? 127} sièges · majorité {f?.summary.majority ?? 64}</span><span>qc26.ca</span></div>38 </div>39 ),40 { ...size },41 );42}43