evijit HF Staff Claude Opus 4.7 (1M context) commited on
Commit
c315a26
·
1 Parent(s): d49f850

Add per-model + per-eval OpenGraph thumbnails; replace "EE/EC" mark with logo image

Browse files

Social/chat unfurls now identify the specific model or benchmark being
shared instead of showing the generic homepage card.

- /api/og/models/[...id] and /api/og/evals/[...id] render dynamic 1200x630
PNGs via next/og's ImageResponse, pulling display name / developer /
category / models-evaluated / top-model from the existing DuckDB-backed
summary getters. Cards include the EvalEval brand logo (logo-square.png)
in place of the previous text mark. The handlers live under /api/og/
rather than as `opengraph-image.tsx` siblings because Next.js 15 rejects
metadata files inside catch-all (`[...id]`) folders.
- app/models/[...id]/layout.tsx and app/evals/[...id]/layout.tsx own the
`generateMetadata` for those routes (the page components are client-side,
so they can't export metadata directly). The layouts also seed the
per-entity `title`/`description`/`og:image`/`twitter:image` so non-image
unfurls also identify the entity.
- app/opengraph-image.tsx swaps the inline "EE" text mark for the same
logo image used elsewhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

app/api/og/_shared.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Helpers shared between the dynamic OpenGraph image routes
3
+ * (/api/og/models/[...id] and /api/og/evals/[...id]). The route
4
+ * handlers run on the node runtime because they query the same
5
+ * DuckDB backend the rest of the app uses; this file just collects
6
+ * the common chrome (size, content-type, brand mark URL resolution)
7
+ * so the per-entity handlers can focus on the entity content.
8
+ */
9
+
10
+ export const OG_SIZE = { width: 1200, height: 630 } as const
11
+ export const OG_CONTENT_TYPE = "image/png"
12
+
13
+ /**
14
+ * Absolute URL for the brand-mark image used inside the generated card.
15
+ * `ImageResponse`'s `<img src="…" />` needs a fetchable URL because the
16
+ * renderer worker downloads the bytes at render time. In production the
17
+ * Space hosts the file at `<origin>/logo-square.png`; for local dev we
18
+ * fall back to localhost so headless preview hits something real.
19
+ */
20
+ export function resolveBrandLogoUrl(request: Request): string {
21
+ const fromEnv = process.env.NEXT_PUBLIC_SITE_URL?.trim().replace(/\/+$/, "")
22
+ const origin = fromEnv || new URL(request.url).origin
23
+ return `${origin}/logo-square.png`
24
+ }
25
+
26
+ /**
27
+ * Truncate a long display name so it doesn't overflow the 1200×630 card
28
+ * frame. Adds an ellipsis when truncated.
29
+ */
30
+ export function ellipsize(value: string, max: number): string {
31
+ if (value.length <= max) return value
32
+ return `${value.slice(0, max - 1).trimEnd()}…`
33
+ }
app/api/og/evals/[...id]/route.tsx ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ImageResponse } from "next/og"
2
+
3
+ import { OG_CONTENT_TYPE, OG_SIZE, ellipsize, resolveBrandLogoUrl } from "@/app/api/og/_shared"
4
+ import { getEvalSummaryById } from "@/lib/data-backend"
5
+ import { routeIdFromSegments } from "@/lib/utils"
6
+
7
+ export const runtime = "nodejs"
8
+
9
+ function formatScore(value: number, unit: string | null): string {
10
+ const isPercentish = !unit || /percent|proportion|accuracy|score|pass@|exact|f1|%/i.test(unit)
11
+ if (isPercentish) {
12
+ const v = Math.abs(value) <= 1 ? value * 100 : value
13
+ return `${v.toFixed(1)}%`
14
+ }
15
+ return value.toFixed(3).replace(/0+$/g, "").replace(/\.$/, "")
16
+ }
17
+
18
+ /**
19
+ * Dynamic OpenGraph image for an eval/benchmark page. Pulls the
20
+ * canonical benchmark name, category, model count, and top result out
21
+ * of the eval summary so the unfurl identifies the specific benchmark.
22
+ */
23
+ export async function GET(
24
+ request: Request,
25
+ context: { params: Promise<{ id: string | string[] }> },
26
+ ) {
27
+ const { id } = await context.params
28
+ const evalId = routeIdFromSegments(id)
29
+
30
+ let evalName = "Benchmark"
31
+ let category: string | null = null
32
+ let modelsCount: number | null = null
33
+ let topModelName: string | null = null
34
+ let topScore: number | null = null
35
+ let unit: string | null = null
36
+
37
+ try {
38
+ const summary = await getEvalSummaryById(evalId)
39
+ if (summary) {
40
+ evalName =
41
+ summary.canonical_display_name ??
42
+ summary.evaluation_name ??
43
+ summary.composite_display_name ??
44
+ evalName
45
+ category = summary.category ?? null
46
+ modelsCount = summary.models_count ?? null
47
+ topModelName = summary.best_model?.name ?? null
48
+ topScore = summary.best_model?.score ?? null
49
+ unit = summary.metric_config?.unit ?? null
50
+ }
51
+ } catch {
52
+ // Fall through to the generic card.
53
+ }
54
+
55
+ const logoUrl = resolveBrandLogoUrl(request)
56
+ const displayName = ellipsize(evalName, 64)
57
+
58
+ const response = new ImageResponse(
59
+ (
60
+ <div
61
+ style={{
62
+ width: "100%",
63
+ height: "100%",
64
+ display: "flex",
65
+ flexDirection: "column",
66
+ background: "#faf9f6",
67
+ color: "#1a1916",
68
+ fontFamily: "Inter, sans-serif",
69
+ padding: "72px",
70
+ }}
71
+ >
72
+ <div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
73
+ {/* eslint-disable-next-line @next/next/no-img-element */}
74
+ <img
75
+ src={logoUrl}
76
+ width={56}
77
+ height={56}
78
+ alt=""
79
+ style={{ borderRadius: "4px" }}
80
+ />
81
+ <div style={{ fontSize: "26px", fontWeight: 700, letterSpacing: "-0.01em" }}>
82
+ Evaluation Cards
83
+ </div>
84
+ <div
85
+ style={{
86
+ fontFamily: "monospace",
87
+ fontSize: "14px",
88
+ letterSpacing: "0.2em",
89
+ textTransform: "uppercase",
90
+ color: "#9c9a95",
91
+ paddingLeft: "20px",
92
+ marginLeft: "8px",
93
+ borderLeft: "1px solid #e8e6e1",
94
+ display: "flex",
95
+ alignItems: "center",
96
+ height: "32px",
97
+ }}
98
+ >
99
+ Benchmark
100
+ </div>
101
+ </div>
102
+
103
+ <div
104
+ style={{
105
+ display: "flex",
106
+ flexDirection: "column",
107
+ gap: "20px",
108
+ flex: 1,
109
+ justifyContent: "center",
110
+ paddingTop: "8px",
111
+ }}
112
+ >
113
+ {category && (
114
+ <div
115
+ style={{
116
+ fontFamily: "monospace",
117
+ fontSize: "16px",
118
+ letterSpacing: "0.2em",
119
+ textTransform: "uppercase",
120
+ color: "#5bacd1",
121
+ fontWeight: 600,
122
+ }}
123
+ >
124
+ {category}
125
+ </div>
126
+ )}
127
+ <div
128
+ style={{
129
+ fontSize: "82px",
130
+ lineHeight: 1.02,
131
+ letterSpacing: "-0.04em",
132
+ fontWeight: 700,
133
+ maxWidth: "1060px",
134
+ wordBreak: "break-word",
135
+ }}
136
+ >
137
+ {displayName}
138
+ </div>
139
+ {(modelsCount != null || topModelName) && (
140
+ <div
141
+ style={{
142
+ display: "flex",
143
+ flexDirection: "column",
144
+ gap: "8px",
145
+ fontSize: "24px",
146
+ color: "#5c5a55",
147
+ lineHeight: 1.4,
148
+ }}
149
+ >
150
+ {modelsCount != null && (
151
+ <div>
152
+ {`${modelsCount.toLocaleString("en-US")} ${modelsCount === 1 ? "model evaluated" : "models evaluated"}`}
153
+ </div>
154
+ )}
155
+ {topModelName && topScore != null && Number.isFinite(topScore) && (
156
+ <div style={{ display: "flex", alignItems: "baseline", gap: "10px" }}>
157
+ <span>Top:</span>
158
+ <span style={{ color: "#1a1916", fontWeight: 600 }}>{topModelName}</span>
159
+ <span style={{ color: "#5bacd1", fontWeight: 600 }}>
160
+ {formatScore(topScore, unit)}
161
+ </span>
162
+ </div>
163
+ )}
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ <div
169
+ style={{
170
+ display: "flex",
171
+ alignItems: "center",
172
+ justifyContent: "space-between",
173
+ paddingTop: "26px",
174
+ borderTop: "1px solid #1a1916",
175
+ fontFamily: "monospace",
176
+ fontSize: "15px",
177
+ letterSpacing: "0.18em",
178
+ textTransform: "uppercase",
179
+ color: "#5c5a55",
180
+ }}
181
+ >
182
+ <div style={{ display: "flex", alignItems: "center", gap: "14px" }}>
183
+ <span style={{ color: "#1a1916", fontWeight: 600 }}>Reproducibility</span>
184
+ <span style={{ color: "#9c9a95" }}>·</span>
185
+ <span style={{ color: "#1a1916", fontWeight: 600 }}>Completeness</span>
186
+ <span style={{ color: "#9c9a95" }}>·</span>
187
+ <span style={{ color: "#1a1916", fontWeight: 600 }}>Provenance</span>
188
+ <span style={{ color: "#9c9a95" }}>·</span>
189
+ <span style={{ color: "#1a1916", fontWeight: 600 }}>Comparability</span>
190
+ </div>
191
+ <div style={{ color: "#9c9a95" }}>Evaluation Cards</div>
192
+ </div>
193
+ </div>
194
+ ),
195
+ OG_SIZE,
196
+ )
197
+ response.headers.set("content-type", OG_CONTENT_TYPE)
198
+ response.headers.set("cache-control", "public, max-age=3600, s-maxage=86400")
199
+ return response
200
+ }
app/api/og/models/[...id]/route.tsx ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ImageResponse } from "next/og"
2
+
3
+ import { OG_CONTENT_TYPE, OG_SIZE, ellipsize, resolveBrandLogoUrl } from "@/app/api/og/_shared"
4
+ import { getModelSummaryById } from "@/lib/data-backend"
5
+ import { routeIdFromSegments } from "@/lib/utils"
6
+
7
+ export const runtime = "nodejs"
8
+
9
+ /**
10
+ * Dynamic OpenGraph image for a model page. Reaches into the same
11
+ * DuckDB-backed model summary the route handler uses so the unfurled
12
+ * card identifies the specific model rather than rendering the
13
+ * generic site card. Co-located under /api/og/ rather than as an
14
+ * `opengraph-image.tsx` sibling of the page because Next.js 15 rejects
15
+ * metadata files inside catch-all (`[...id]`) folders.
16
+ */
17
+ export async function GET(
18
+ request: Request,
19
+ context: { params: Promise<{ id: string | string[] }> },
20
+ ) {
21
+ const { id } = await context.params
22
+ const routeId = routeIdFromSegments(id)
23
+
24
+ let modelName = "Model"
25
+ let developer: string | null = null
26
+ let benchmarkCount: number | null = null
27
+
28
+ try {
29
+ const summary = await getModelSummaryById(routeId)
30
+ if (summary) {
31
+ modelName = summary.model_info?.name ?? modelName
32
+ developer = summary.model_info?.developer ?? null
33
+ const categories = summary.evaluations_by_category ?? {}
34
+ const seen = new Set<string>()
35
+ let total = 0
36
+ for (const items of Object.values(categories)) {
37
+ for (const item of items ?? []) {
38
+ total += 1
39
+ const key = (item as { benchmark?: string; evaluation_id?: string }).benchmark
40
+ ?? (item as { evaluation_id?: string }).evaluation_id
41
+ if (key) seen.add(key)
42
+ }
43
+ }
44
+ benchmarkCount = seen.size > 0 ? seen.size : total > 0 ? total : null
45
+ }
46
+ } catch {
47
+ // Fall through to the generic card.
48
+ }
49
+
50
+ const logoUrl = resolveBrandLogoUrl(request)
51
+ const displayName = ellipsize(modelName, 64)
52
+
53
+ const response = new ImageResponse(
54
+ (
55
+ <div
56
+ style={{
57
+ width: "100%",
58
+ height: "100%",
59
+ display: "flex",
60
+ flexDirection: "column",
61
+ background: "#faf9f6",
62
+ color: "#1a1916",
63
+ fontFamily: "Inter, sans-serif",
64
+ padding: "72px",
65
+ }}
66
+ >
67
+ <div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
68
+ {/* eslint-disable-next-line @next/next/no-img-element */}
69
+ <img
70
+ src={logoUrl}
71
+ width={56}
72
+ height={56}
73
+ alt=""
74
+ style={{ borderRadius: "4px" }}
75
+ />
76
+ <div style={{ fontSize: "26px", fontWeight: 700, letterSpacing: "-0.01em" }}>
77
+ Evaluation Cards
78
+ </div>
79
+ <div
80
+ style={{
81
+ fontFamily: "monospace",
82
+ fontSize: "14px",
83
+ letterSpacing: "0.2em",
84
+ textTransform: "uppercase",
85
+ color: "#9c9a95",
86
+ paddingLeft: "20px",
87
+ marginLeft: "8px",
88
+ borderLeft: "1px solid #e8e6e1",
89
+ display: "flex",
90
+ alignItems: "center",
91
+ height: "32px",
92
+ }}
93
+ >
94
+ Model card
95
+ </div>
96
+ </div>
97
+
98
+ <div
99
+ style={{
100
+ display: "flex",
101
+ flexDirection: "column",
102
+ gap: "20px",
103
+ flex: 1,
104
+ justifyContent: "center",
105
+ paddingTop: "8px",
106
+ }}
107
+ >
108
+ <div
109
+ style={{
110
+ fontFamily: "monospace",
111
+ fontSize: "16px",
112
+ letterSpacing: "0.2em",
113
+ textTransform: "uppercase",
114
+ color: "#5bacd1",
115
+ fontWeight: 600,
116
+ }}
117
+ >
118
+ {developer ?? "Model"}
119
+ </div>
120
+ <div
121
+ style={{
122
+ fontSize: "88px",
123
+ lineHeight: 1.02,
124
+ letterSpacing: "-0.04em",
125
+ fontWeight: 700,
126
+ maxWidth: "1060px",
127
+ wordBreak: "break-word",
128
+ }}
129
+ >
130
+ {displayName}
131
+ </div>
132
+ {benchmarkCount != null && (
133
+ <div style={{ fontSize: "26px", color: "#5c5a55", lineHeight: 1.4 }}>
134
+ {`Reported on ${benchmarkCount.toLocaleString("en-US")} ${benchmarkCount === 1 ? "benchmark" : "benchmarks"}.`}
135
+ </div>
136
+ )}
137
+ </div>
138
+
139
+ <div
140
+ style={{
141
+ display: "flex",
142
+ alignItems: "center",
143
+ justifyContent: "space-between",
144
+ paddingTop: "26px",
145
+ borderTop: "1px solid #1a1916",
146
+ fontFamily: "monospace",
147
+ fontSize: "15px",
148
+ letterSpacing: "0.18em",
149
+ textTransform: "uppercase",
150
+ color: "#5c5a55",
151
+ }}
152
+ >
153
+ <div style={{ display: "flex", alignItems: "center", gap: "14px" }}>
154
+ <span style={{ color: "#1a1916", fontWeight: 600 }}>Reproducibility</span>
155
+ <span style={{ color: "#9c9a95" }}>��</span>
156
+ <span style={{ color: "#1a1916", fontWeight: 600 }}>Completeness</span>
157
+ <span style={{ color: "#9c9a95" }}>·</span>
158
+ <span style={{ color: "#1a1916", fontWeight: 600 }}>Provenance</span>
159
+ <span style={{ color: "#9c9a95" }}>·</span>
160
+ <span style={{ color: "#1a1916", fontWeight: 600 }}>Comparability</span>
161
+ </div>
162
+ <div style={{ color: "#9c9a95" }}>Evaluation Cards</div>
163
+ </div>
164
+ </div>
165
+ ),
166
+ OG_SIZE,
167
+ )
168
+ response.headers.set("content-type", OG_CONTENT_TYPE)
169
+ response.headers.set("cache-control", "public, max-age=3600, s-maxage=86400")
170
+ return response
171
+ }
app/evals/[...id]/layout.tsx ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next"
2
+
3
+ import { getEvalSummaryById } from "@/lib/data-backend"
4
+ import { routeIdFromSegments, routeIdToPath } from "@/lib/utils"
5
+
6
+ /**
7
+ * Server-side metadata for the eval/benchmark detail page. Mirrors
8
+ * app/models/[...id]/layout.tsx — the page itself is a client
9
+ * component, so this sibling owns the OpenGraph payload so unfurls
10
+ * surface the specific benchmark's name + per-benchmark thumbnail
11
+ * (rendered by /api/og/evals/<id>).
12
+ */
13
+ export async function generateMetadata(props: {
14
+ params: Promise<{ id: string | string[] }>
15
+ }): Promise<Metadata> {
16
+ const { id } = await props.params
17
+ const routeId = routeIdFromSegments(id)
18
+ let evalName = "Benchmark"
19
+ let category: string | null = null
20
+ let modelsCount: number | null = null
21
+
22
+ try {
23
+ const summary = await getEvalSummaryById(routeId)
24
+ if (summary) {
25
+ evalName =
26
+ summary.canonical_display_name ??
27
+ summary.evaluation_name ??
28
+ summary.composite_display_name ??
29
+ evalName
30
+ category = summary.category ?? null
31
+ modelsCount = summary.models_count ?? null
32
+ }
33
+ } catch {
34
+ // Fall through to generic copy.
35
+ }
36
+
37
+ const idSlug = routeIdToPath(routeId)
38
+ const title = category ? `${evalName} — ${category}` : evalName
39
+ const description = modelsCount
40
+ ? `${evalName} — ${modelsCount.toLocaleString("en-US")} models evaluated, with reproducibility, completeness, provenance, and comparability signals computed over every result.`
41
+ : `${evalName} — reported model–benchmark results on Evaluation Cards.`
42
+ const imageUrl = `/api/og/evals/${idSlug}`
43
+
44
+ return {
45
+ title,
46
+ description,
47
+ openGraph: {
48
+ title,
49
+ description,
50
+ type: "article",
51
+ images: [
52
+ {
53
+ url: imageUrl,
54
+ width: 1200,
55
+ height: 630,
56
+ alt: `${evalName} — Evaluation Cards`,
57
+ },
58
+ ],
59
+ },
60
+ twitter: {
61
+ card: "summary_large_image",
62
+ title,
63
+ description,
64
+ images: [imageUrl],
65
+ },
66
+ }
67
+ }
68
+
69
+ export default function EvalDetailLayout({
70
+ children,
71
+ }: Readonly<{ children: React.ReactNode }>) {
72
+ return children
73
+ }
app/models/[...id]/layout.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next"
2
+
3
+ import { getModelSummaryById } from "@/lib/data-backend"
4
+ import { routeIdFromSegments, routeIdToPath } from "@/lib/utils"
5
+
6
+ /**
7
+ * Server-side metadata for the model detail page. The page itself is a
8
+ * client component so it can't export `generateMetadata` directly; this
9
+ * sibling layout owns the OpenGraph payload so social/chat unfurls
10
+ * carry the model's name + a per-model thumbnail (rendered by
11
+ * /api/og/models/<id>) instead of the site-wide card.
12
+ */
13
+ export async function generateMetadata(props: {
14
+ params: Promise<{ id: string | string[] }>
15
+ }): Promise<Metadata> {
16
+ const { id } = await props.params
17
+ const routeId = routeIdFromSegments(id)
18
+ let modelName = "Model"
19
+ let developer: string | null = null
20
+
21
+ try {
22
+ const summary = await getModelSummaryById(routeId)
23
+ if (summary) {
24
+ modelName = summary.model_info?.name ?? modelName
25
+ developer = summary.model_info?.developer ?? null
26
+ }
27
+ } catch {
28
+ // Fall through to generic copy.
29
+ }
30
+
31
+ const idSlug = routeIdToPath(routeId)
32
+ const title = developer ? `${modelName} — ${developer}` : modelName
33
+ const description = developer
34
+ ? `${modelName} (${developer}) — every reported model–benchmark result, organised under Evaluation Cards' five-level hierarchy and four interpretive signals.`
35
+ : `${modelName} — every reported model–benchmark result on Evaluation Cards.`
36
+ const imageUrl = `/api/og/models/${idSlug}`
37
+
38
+ return {
39
+ title,
40
+ description,
41
+ openGraph: {
42
+ title,
43
+ description,
44
+ type: "article",
45
+ images: [
46
+ {
47
+ url: imageUrl,
48
+ width: 1200,
49
+ height: 630,
50
+ alt: `${modelName} — Evaluation Cards`,
51
+ },
52
+ ],
53
+ },
54
+ twitter: {
55
+ card: "summary_large_image",
56
+ title,
57
+ description,
58
+ images: [imageUrl],
59
+ },
60
+ }
61
+ }
62
+
63
+ export default function ModelDetailLayout({
64
+ children,
65
+ }: Readonly<{ children: React.ReactNode }>) {
66
+ return children
67
+ }
app/opengraph-image.tsx CHANGED
@@ -5,7 +5,15 @@ export const alt = "Evaluation Cards — a reporting layer over evaluation infra
5
  export const size = { width: 1200, height: 630 }
6
  export const contentType = "image/png"
7
 
 
 
 
 
 
 
 
8
  export default function OpenGraphImage() {
 
9
  return new ImageResponse(
10
  (
11
  <div
@@ -28,24 +36,14 @@ export default function OpenGraphImage() {
28
  gap: "16px",
29
  }}
30
  >
31
- <div
32
- style={{
33
- width: "56px",
34
- height: "56px",
35
- background: "#5bacd1",
36
- color: "#ffffff",
37
- display: "flex",
38
- alignItems: "center",
39
- justifyContent: "center",
40
- fontFamily: "monospace",
41
- fontWeight: 600,
42
- fontSize: "22px",
43
- letterSpacing: "0.02em",
44
- borderRadius: "4px",
45
- }}
46
- >
47
- EE
48
- </div>
49
  <div
50
  style={{
51
  fontSize: "26px",
 
5
  export const size = { width: 1200, height: 630 }
6
  export const contentType = "image/png"
7
 
8
+ // Brand mark URL. `ImageResponse` fetches `<img src>` at render time,
9
+ // so it needs an absolute URL — prefer NEXT_PUBLIC_SITE_URL (set on
10
+ // the Space), else fall back to the canonical remote.
11
+ const LOGO_URL =
12
+ (process.env.NEXT_PUBLIC_SITE_URL?.trim().replace(/\/+$/, "") ?? "") + "/logo-square.png"
13
+ const FALLBACK_LOGO_URL = "https://evalevalai.com/assets/img/logo-square.png"
14
+
15
  export default function OpenGraphImage() {
16
+ const logoUrl = LOGO_URL.startsWith("http") ? LOGO_URL : FALLBACK_LOGO_URL
17
  return new ImageResponse(
18
  (
19
  <div
 
36
  gap: "16px",
37
  }}
38
  >
39
+ {/* eslint-disable-next-line @next/next/no-img-element */}
40
+ <img
41
+ src={logoUrl}
42
+ width={56}
43
+ height={56}
44
+ alt=""
45
+ style={{ borderRadius: "4px" }}
46
+ />
 
 
 
 
 
 
 
 
 
 
47
  <div
48
  style={{
49
  fontSize: "26px",