Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 3 additions & 14 deletions src/app/benchmarks/[slug]/[chain]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ImageResponse } from "next/og";
import { getBenchmark } from "@/data/benchmarks";
import { headlineSentence, leader } from "@/lib/citation";
import { leader } from "@/lib/citation";
import { OgClaimSentence } from "@/lib/og-claim";
import { fmtUnit } from "@/lib/format";
import { CATEGORY_COLOR } from "@/lib/category-colors";
import { loadBenchmark } from "@/lib/spec";
Expand Down Expand Up @@ -33,7 +34,6 @@ export default async function OG({
const headline = top
? `${top.name} leads at ${fmtUnit(top.value, b.unit)}`
: "Awaiting first run";
const sentence = headlineSentence(b);
const catColor = CATEGORY_COLOR[b.category] ?? "#7a2e1f";
const titleText = `${b.title} on ${chainLabel}`;

Expand Down Expand Up @@ -94,18 +94,7 @@ export default async function OG({
>
{titleText}
</div>
<div
style={{
display: "flex",
fontSize: 28,
fontStyle: "italic",
color: "#4a443c",
marginTop: 18,
maxWidth: 1080,
}}
>
{top ? sentence : b.subtitle}
</div>
<OgClaimSentence benchmark={b} fallback={b.subtitle} />
</div>

<div
Expand Down
17 changes: 3 additions & 14 deletions src/app/benchmarks/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ImageResponse } from "next/og";
import { getBenchmark } from "@/data/benchmarks";
import { headlineSentence, leader } from "@/lib/citation";
import { leader } from "@/lib/citation";
import { OgClaimSentence } from "@/lib/og-claim";
import { fmtUnit } from "@/lib/format";
import { CATEGORY_COLOR } from "@/lib/category-colors";
import { matchesChainSlug } from "@/lib/chain-aliases";
Expand Down Expand Up @@ -57,7 +58,6 @@ export default async function OG({

const top = leader(b);
const headline = top ? `${top.name} leads at ${fmtUnit(top.value, b.unit)}` : "Awaiting first run";
const sentence = headlineSentence(b);
const catColor = CATEGORY_COLOR[b.category] ?? "#7a2e1f";
const chainLabel = chainId
? b.dimensions?.chain?.find((c) => matchesChainSlug(c.value, chainId))
Expand Down Expand Up @@ -123,18 +123,7 @@ export default async function OG({
>
{titleText}
</div>
<div
style={{
display: "flex",
fontSize: 28,
fontStyle: "italic",
color: "#4a443c",
marginTop: 18,
maxWidth: 1080,
}}
>
{top ? sentence : b.subtitle}
</div>
<OgClaimSentence benchmark={b} fallback={b.subtitle} />
</div>

<div
Expand Down
17 changes: 3 additions & 14 deletions src/app/benchmarks/[slug]/twitter-image.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ImageResponse } from "next/og";
import { getBenchmark } from "@/data/benchmarks";
import { headlineSentence, leader } from "@/lib/citation";
import { leader } from "@/lib/citation";
import { OgClaimSentence } from "@/lib/og-claim";
import { fmtUnit } from "@/lib/format";
import { CATEGORY_COLOR } from "@/lib/category-colors";
import { matchesChainSlug } from "@/lib/chain-aliases";
Expand Down Expand Up @@ -68,7 +69,6 @@ export default async function TwitterImage({

const top = leader(b);
const headline = top ? `${top.name} leads at ${fmtUnit(top.value, b.unit)}` : "Awaiting first run";
const sentence = headlineSentence(b);
const catColor = CATEGORY_COLOR[b.category] ?? "#7a2e1f";
const chainLabel = chainId
? b.dimensions?.chain?.find((c) => matchesChainSlug(c.value, chainId))
Expand Down Expand Up @@ -133,18 +133,7 @@ export default async function TwitterImage({
>
{titleText}
</div>
<div
style={{
display: "flex",
fontSize: 28,
fontStyle: "italic",
color: "#4a443c",
marginTop: 18,
maxWidth: 1080,
}}
>
{top ? sentence : b.subtitle}
</div>
<OgClaimSentence benchmark={b} fallback={b.subtitle} />
</div>
<div
style={{
Expand Down
21 changes: 9 additions & 12 deletions src/app/products/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ImageResponse } from "next/og";
import { getProvider } from "@/lib/providers";
import { OgHighlight } from "@/lib/og-claim";

export const runtime = "nodejs";
// Share cards change slowly (title, leader, headline value); crawlers
Expand Down Expand Up @@ -90,18 +91,14 @@ export default async function OG({
>
{p.name}
</div>
<div
style={{
display: "flex",
fontSize: 28,
fontStyle: "italic",
color: "#4a443c",
marginTop: 18,
maxWidth: 1080,
}}
>
{appearancesLine} on OpenChainBench
</div>
<OgHighlight
lead={
p.wins > 0
? `#1 on ${p.wins} of ${p.appearances.length} benchmark${p.appearances.length === 1 ? "" : "s"}`
: appearancesLine
}
rest={p.wins > 0 ? "measured live on OpenChainBench" : "on OpenChainBench"}
/>
</div>

<div
Expand Down
27 changes: 12 additions & 15 deletions src/app/reports/[category]/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ImageResponse } from "next/og";
import { getReport } from "@/lib/reports/loader";
import { OgHighlight, splitLead } from "@/lib/og-claim";

export const runtime = "nodejs";
// Share cards change slowly (title, leader, headline value); crawlers
Expand All @@ -24,6 +25,16 @@ export default async function OG({
if (!report) return new ImageResponse(<div />, { ...size });

const title = report.title;
// Lead clause of the hero finding is the card's highlighted line; the
// remainder keeps the previous ~140 character budget.
const finding = (() => {
const { lead, rest } = splitLead(report.heroFinding);
const room = Math.max(0, 140 - lead.length);
return {
lead,
rest: rest.length > room ? rest.slice(0, Math.max(0, room - 3)).trimEnd() + "..." : rest,
};
})();
const fontSize = title.length > 60 ? 58 : title.length > 40 ? 68 : 80;

return new ImageResponse(
Expand Down Expand Up @@ -83,21 +94,7 @@ export default async function OG({
>
{title}
</div>
<div
style={{
display: "flex",
fontSize: 26,
fontStyle: "italic",
color: "#4a443c",
marginTop: 20,
maxWidth: 1080,
lineHeight: 1.4,
}}
>
{report.heroFinding.length > 140
? report.heroFinding.slice(0, 137) + "..."
: report.heroFinding}
</div>
<OgHighlight lead={finding.lead} rest={finding.rest} />
</div>

<div
Expand Down
19 changes: 16 additions & 3 deletions src/lib/citation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,27 @@ function windowSuffix(unit: string): string {

/** Short factual sentence ready to paste into an article. Templated, no LLM. */
export function headlineSentence(b: Benchmark): string {
const parts = headlineParts(b);
return parts.claim ? `${parts.claim} ${parts.rest}` : parts.rest;
}

/** The headline sentence split in two: the leader claim ("GNS posts the
* lowest p/f ratio at 1.559x"), which the social cards highlight, and
* the qualifier that follows ("(p50, 24h) on <title>."). `claim` is
* empty when there is no leader to assert. Joined with one space they
* are exactly `headlineSentence`. */
export function headlineParts(b: Benchmark): { claim: string; rest: string } {
if (b.dataConfidence === "insufficient") {
return `${b.title}. Insufficient data to assert a leader.`;
return { claim: "", rest: `${b.title}. Insufficient data to assert a leader.` };
}
const top = leader(b);
if (!top) return `${b.title}. Awaiting first run.`;
if (!top) return { claim: "", rest: `${b.title}. Awaiting first run.` };
const value = fmtUnit(top.value, b.unit);
const verb = b.higherIsBetter ? "leads" : "posts the lowest";
return `${top.name} ${verb} ${b.metric.toLowerCase()} at ${value} ${windowSuffix(b.unit)} on ${b.title}.`;
return {
claim: `${top.name} ${verb} ${b.metric.toLowerCase()} at ${value}`,
rest: `${windowSuffix(b.unit)} on ${b.title}.`,
};
}

/** Pasteable attribution string. Standard convention: "<sentence> Source: OpenChainBench (url)". */
Expand Down
152 changes: 152 additions & 0 deletions src/lib/og-claim.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import type { Benchmark } from "@/types/benchmark";
import { headlineParts } from "@/lib/citation";

/** Marker colour behind the leader claim on the social cards. Warm
* orange on the cream card ground: readable at thumbnail size, and not
* a category colour, so it means "the number" on every bench alike. */
export const OG_CLAIM_MARKER = "#ffb257";

/**
* The headline sentence for OG / Twitter cards, with the leader claim
* ("GNS posts the lowest p/f ratio at 1.559x") run through a highlighter
* and the qualifier ("(p50, 24h) on <title>.") underneath in the muted
* italic the cards already use. Satori lays each child span out as a
* flex item, so the claim sits on its own line and wraps within itself
* when it is long. Falls back to the subtitle when there is no leader.
*/
export function OgClaimSentence({
benchmark,
fallback,
}: {
benchmark: Benchmark;
fallback: string;
}) {
const { claim, rest } = headlineParts(benchmark);
// The title is already the card's headline; keep only the window
// qualifier ("(p50, 24h)") under the claim instead of repeating it.
const qualifier = rest.replace(` on ${benchmark.title}.`, "");
if (!claim) {
return (
<div
style={{
display: "flex",
fontSize: 28,
fontStyle: "italic",
color: "#4a443c",
marginTop: 18,
maxWidth: 1080,
}}
>
{fallback}
</div>
);
}
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
marginTop: 18,
maxWidth: 1080,
}}
>
<span
style={{
fontSize: 30,
fontWeight: 700,
color: "#1c1a17",
backgroundColor: OG_CLAIM_MARKER,
padding: "4px 12px",
borderRadius: 6,
}}
>
{claim}
</span>
<span
style={{
fontSize: 26,
fontStyle: "italic",
color: "#4a443c",
marginTop: 10,
}}
>
{qualifier}
</span>
</div>
);
}

/**
* Generic form for cards whose body is free text (report hero finding,
* product line): `lead` goes through the highlighter, `rest` follows in
* the muted italic. Same marker, same sizes as the bench claim.
*/
export function OgHighlight({ lead, rest }: { lead: string; rest?: string }) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
marginTop: 18,
maxWidth: 1080,
}}
>
<span
style={{
fontSize: 30,
fontWeight: 700,
color: "#1c1a17",
backgroundColor: OG_CLAIM_MARKER,
padding: "4px 12px",
borderRadius: 6,
}}
>
{lead}
</span>
{rest ? (
<span
style={{
fontSize: 26,
fontStyle: "italic",
color: "#4a443c",
marginTop: 10,
lineHeight: 1.4,
}}
>
{rest}
</span>
) : null}
</div>
);
}

/**
* Split a finding into a short lead clause and the remainder: cut at the
* first ", " / ". " / "; " / " — " boundary at or after `min` chars, and
* never past `max`. Text with no usable boundary is all lead when short,
* else cut at the last space before `max`.
*/
export function splitLead(
text: string,
{ min = 24, max = 84 }: { min?: number; max?: number } = {},
): { lead: string; rest: string } {
const t = text.trim();
const re = /,\s|\.\s|;\s|\s[—–-]\s/g;
let m: RegExpExecArray | null;
while ((m = re.exec(t))) {
if (m.index > max) break;
if (m.index >= min) {
return {
lead: t.slice(0, m.index).trim(),
rest: t.slice(m.index + m[0].length).trim(),
};
}
}
if (t.length <= max) return { lead: t, rest: "" };
const cut = t.lastIndexOf(" ", max);
return cut > min
? { lead: t.slice(0, cut).trim(), rest: t.slice(cut).trim() }
: { lead: t.slice(0, max).trim(), rest: t.slice(max).trim() };
}
Loading