spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import { ImageResponse } from "next/og";2import { api } from "@/lib/api";3import { fmtInt, fmtMw } from "@/lib/format";45/**6 * Shared Open Graph template (1200×630) — dark ink, thin world-grid texture, mark + wordmark,7 * title/tagline, stats strip. Entity pages reuse `renderOg({ title, subtitle, stats, eyebrow })`.8 * Satori subset of CSS only: flex layout, no grid, no CSS vars.9 */1011export const OG_SIZE = { width: 1200, height: 630 };12export const OG_CONTENT_TYPE = "image/png";1314const INK = "#0b0f14";15const PAPER = "#eceae5";16const MUTED = "#8a9099";17const ACCENT = "#e8763a";18const HAIR = "rgba(236,234,229,0.08)";1920export interface OgStat {21 label: string;22 value: string;23}2425export interface OgProps {26 eyebrow?: string;27 title: string;28 subtitle?: string;29 stats?: OgStat[];30 footer?: string;31}3233function Mark({ size }: { size: number }) {34 return (35 <svg width={size} height={size} viewBox="0 0 32 32" fill="none">36 <circle cx="15" cy="16" r="12" stroke={PAPER} strokeWidth="2.2" />37 <path d="M15 4c-3.2 2.9-4.8 6.9-4.8 12s1.6 9.1 4.8 12c3.2-2.9 4.8-6.9 4.8-12S18.2 6.9 15 4Z" stroke={PAPER} strokeWidth="1.4" />38 <path d="M4.6 10.6h20.8M4.6 21.4h20.8" stroke={PAPER} strokeWidth="2" strokeLinecap="round" />39 <path d="M3 16h28" stroke={ACCENT} strokeWidth="2.6" strokeLinecap="round" />40 </svg>41 );42}4344function Graticule() {45 const cols = Array.from({ length: 25 }, (_, i) => i * 50);46 const rows = Array.from({ length: 13 }, (_, i) => i * 50);47 return (48 <div style={{ position: "absolute", inset: 0, display: "flex" }}>49 {cols.map((x) => (50 <div key={`c${x}`} style={{ position: "absolute", left: x, top: 0, width: 1, height: 630, background: HAIR }} />51 ))}52 {rows.map((y) => (53 <div key={`r${y}`} style={{ position: "absolute", top: y, left: 0, height: 1, width: 1200, background: HAIR }} />54 ))}55 {/* meridian arcs as thin ellipses */}56 {[220, 420, 640].map((rx, i) => (57 <div key={i} style={{ position: "absolute", left: 900 - rx, top: 315 - 420, width: rx * 2, height: 840, borderRadius: "50%", border: `1px solid ${HAIR}` }} />58 ))}59 <div style={{ position: "absolute", inset: 0, background: "linear-gradient(90deg, rgba(11,15,20,0.9) 0%, rgba(11,15,20,0.4) 55%, rgba(11,15,20,0) 100%)" }} />60 </div>61 );62}6364export function OgTemplate({ eyebrow, title, subtitle, stats, footer }: OgProps) {65 return (66 <div style={{ width: 1200, height: 630, display: "flex", flexDirection: "column", background: INK, color: PAPER, fontFamily: "Inter, sans-serif", position: "relative", padding: 64 }}>67 <Graticule />68 <div style={{ display: "flex", alignItems: "center", gap: 18, position: "relative" }}>69 <Mark size={56} />70 <div style={{ display: "flex", alignItems: "baseline", fontSize: 40, letterSpacing: -1 }}>71 <span style={{ fontWeight: 600 }}>DataCenter</span>72 <span style={{ fontWeight: 400, color: "#b9b6ae" }}>Index</span>73 <div style={{ width: 4, height: 24, background: ACCENT, marginLeft: 12 }} />74 </div>75 </div>76 <div style={{ display: "flex", flexDirection: "column", flex: 1, justifyContent: "center", position: "relative", paddingRight: 240 }}>77 {eyebrow && <div style={{ fontSize: 20, letterSpacing: 3, textTransform: "uppercase", color: MUTED, marginBottom: 14 }}>{eyebrow}</div>}78 <div style={{ fontSize: title.length > 40 ? 52 : 64, fontWeight: 600, letterSpacing: -2, lineHeight: 1.05 }}>{title}</div>79 {subtitle && <div style={{ fontSize: 26, color: "#b9b6ae", marginTop: 18, lineHeight: 1.3 }}>{subtitle}</div>}80 </div>81 <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", position: "relative", borderTop: `1px solid rgba(236,234,229,0.18)`, paddingTop: 22 }}>82 <div style={{ display: "flex", gap: 40 }}>83 {(stats ?? []).map((s, i) => (84 <div key={i} style={{ display: "flex", flexDirection: "column" }}>85 <div style={{ fontSize: 32, fontWeight: 600, letterSpacing: -1 }}>{s.value}</div>86 <div style={{ fontSize: 13, letterSpacing: 1.5, textTransform: "uppercase", color: MUTED, marginTop: 3 }}>{s.label}</div>87 </div>88 ))}89 </div>90 <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", fontSize: 18, color: MUTED }}>91 <span>{footer ?? "www.datacenterindex.io"}</span>92 <span style={{ fontSize: 13, letterSpacing: 2, textTransform: "uppercase", marginTop: 4 }}>published figures · sourced · versioned</span>93 </div>94 </div>95 </div>96 );97}9899/** Live stats for the default share image, with a graceful fallback when the API is down. */100export async function ogDashboardStats(): Promise<OgStat[]> {101 const r = await api.dashboard();102 if (!r.ok || !r.data?.stats) return [{ label: "Global coverage", value: "Facilities · MW · Countries" }];103 const s = r.data.stats;104 return [105 { label: "Facilities indexed", value: fmtInt(s.facilities) },106 { label: "Known operational MW", value: fmtMw(s.knownOperationalMw) },107 { label: "Under construction (≈)", value: fmtMw(s.constructionMw) },108 { label: "Countries", value: fmtInt(s.countries) },109 { label: "Events · 24 h", value: fmtInt(s.events24h) },110 ];111}112113export function renderOg(props: OgProps): ImageResponse {114 return new ImageResponse(<OgTemplate {...props} />, { ...OG_SIZE });115}116