From d9b047582f86a4f32c36fddf1e3ae54268e82729 Mon Sep 17 00:00:00 2001
From: Oseltamivir <58582368+Oseltamivir@users.noreply.github.com>
Date: Tue, 26 May 2026 10:36:10 -0700
Subject: [PATCH 1/2] fix(blog): remove generated backticks from inline code
---
packages/app/cypress/e2e/blog.cy.ts | 16 ++++++++++++++++
packages/app/src/app/globals.css | 6 ++++++
2 files changed, 22 insertions(+)
diff --git a/packages/app/cypress/e2e/blog.cy.ts b/packages/app/cypress/e2e/blog.cy.ts
index f8f8a523..4c70fecf 100644
--- a/packages/app/cypress/e2e/blog.cy.ts
+++ b/packages/app/cypress/e2e/blog.cy.ts
@@ -49,4 +49,20 @@ describe('Blog', () => {
cy.get('a[href="/blog"]').should('exist');
});
});
+
+ describe('Inline code styling', () => {
+ before(() => {
+ cy.visit('/blog/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar');
+ });
+
+ it('does not render generated backticks around inline code', () => {
+ cy.contains('article.prose code', 'zai-org/GLM-5-FP8')
+ .first()
+ .should(($code) => {
+ expect($code.text()).to.equal('zai-org/GLM-5-FP8');
+ expect(getComputedStyle($code[0], '::before').content).to.equal('none');
+ expect(getComputedStyle($code[0], '::after').content).to.equal('none');
+ });
+ });
+ });
});
diff --git a/packages/app/src/app/globals.css b/packages/app/src/app/globals.css
index 2f885148..01b78fe8 100644
--- a/packages/app/src/app/globals.css
+++ b/packages/app/src/app/globals.css
@@ -28,6 +28,12 @@
min-width: 100%;
}
+/* Inline code is already distinguished visually; omit Typography's generated backticks. */
+.blog-prose code::before,
+.blog-prose code::after {
+ content: none;
+}
+
/* Remove auto-inserted curly quotes from blockquotes in blog prose */
.blog-prose blockquote p:first-of-type::before,
.blog-prose blockquote p:last-of-type::after {
From 38bcb30496f89877dbbe319c654c01a3736e5d0d Mon Sep 17 00:00:00 2001
From: Oseltamivir <58582368+Oseltamivir@users.noreply.github.com>
Date: Tue, 26 May 2026 13:04:44 -0700
Subject: [PATCH 2/2] feat(compare): add indexed per-dollar chart images
---
.../e2e/compare-per-dollar-table.cy.ts | 13 +
.../compare-per-dollar/[slug]/page-client.tsx | 29 +-
.../app/compare-per-dollar/[slug]/page.tsx | 3 +
.../performance-per-dollar.png/route.tsx | 394 ++++++++++++++++++
packages/app/src/app/sitemap.ts | 21 +-
packages/app/src/lib/compare-ssr.ts | 47 +++
6 files changed, 494 insertions(+), 13 deletions(-)
create mode 100644 packages/app/src/app/compare-per-dollar/[slug]/performance-per-dollar.png/route.tsx
diff --git a/packages/app/cypress/e2e/compare-per-dollar-table.cy.ts b/packages/app/cypress/e2e/compare-per-dollar-table.cy.ts
index db48142d..e21a0baa 100644
--- a/packages/app/cypress/e2e/compare-per-dollar-table.cy.ts
+++ b/packages/app/cypress/e2e/compare-per-dollar-table.cy.ts
@@ -58,6 +58,19 @@ describe('Compare-per-dollar slug page — slimmed table + cross-link', () => {
it('uses "Performance per Dollar" framing in the page header', () => {
cy.contains('Performance per Dollar').should('be.visible');
});
+
+ it('renders an indexable comparison PNG with descriptive alt text', () => {
+ cy.get('[data-testid="compare-per-dollar-indexed-image"] img')
+ .should('be.visible')
+ .and('have.attr', 'src')
+ .and(
+ 'match',
+ /\/compare-per-dollar\/deepseek-r1-gb200-vs-h100\/performance-per-dollar\.png$/u,
+ );
+ cy.get('[data-testid="compare-per-dollar-indexed-image"] img')
+ .should('have.attr', 'alt')
+ .and('contain', 'cost per million tokens at matched interactivity levels');
+ });
});
describe('Compare slug page — cross-link to per-dollar view', () => {
diff --git a/packages/app/src/app/compare-per-dollar/[slug]/page-client.tsx b/packages/app/src/app/compare-per-dollar/[slug]/page-client.tsx
index f3c9a27b..b6ee7550 100644
--- a/packages/app/src/app/compare-per-dollar/[slug]/page-client.tsx
+++ b/packages/app/src/app/compare-per-dollar/[slug]/page-client.tsx
@@ -48,6 +48,8 @@ interface ComparePerDollarPageClientProps {
* header so readers can audit the pricing assumptions. */
aCostPerGpuHr: number;
bCostPerGpuHr: number;
+ /** Crawlable data graphic generated for the canonical default comparison. */
+ heroImageSrc: string;
}
/** Only show Cost + Concurrency in the interpolated table — the rest of the
@@ -98,6 +100,7 @@ export default function ComparePerDollarPageClient({
bArch,
aCostPerGpuHr,
bCostPerGpuHr,
+ heroImageSrc,
}: ComparePerDollarPageClientProps) {
useEffect(() => {
track('compare_per_dollar_page_view', { gpu_a: a, gpu_b: b, default_model: defaultModel });
@@ -121,7 +124,7 @@ export default function ComparePerDollarPageClient({
initialYAxisMetric={PER_DOLLAR_DEFAULT_Y_AXIS}
>
-
+
{modelLabel} · Performance per Dollar
@@ -129,7 +132,7 @@ export default function ComparePerDollarPageClient({
{label} Performance per Dollar
-
+
Cost per million tokens of {aLabel} ({aVendor} {aArch}) versus{' '}
{bLabel} ({bVendor} {bArch}) on {modelLabel} .
Owning-hyperscaler TCO normalized by output tokens — performance per dollar across
@@ -143,7 +146,7 @@ export default function ComparePerDollarPageClient({
{narrative.length > 0 && (
{narrative.map((para, i) => (
@@ -166,7 +169,7 @@ export default function ComparePerDollarPageClient({
)}
{(aCostPerGpuHr > 0 || bCostPerGpuHr > 0) && (
GPU pricing (owning hyperscaler): {aLabel} {' '}
@@ -195,6 +198,24 @@ export default function ComparePerDollarPageClient({
+
+
+
+ {aLabel} versus {bLabel} cost per million tokens for this comparison's canonical
+ default workload. Lower cost indicates better performance per dollar.
+
+
>
);
diff --git a/packages/app/src/app/compare-per-dollar/[slug]/performance-per-dollar.png/route.tsx b/packages/app/src/app/compare-per-dollar/[slug]/performance-per-dollar.png/route.tsx
new file mode 100644
index 00000000..5a303ac2
--- /dev/null
+++ b/packages/app/src/app/compare-per-dollar/[slug]/performance-per-dollar.png/route.tsx
@@ -0,0 +1,394 @@
+import { ImageResponse } from 'next/og';
+
+import { HW_REGISTRY } from '@semianalysisai/inferencex-constants';
+
+import { pickPairDefaults } from '@/lib/compare-pair-defaults';
+import { canonicalCompareSlug, parseCompareSlug } from '@/lib/compare-slug';
+import {
+ computeCompareImageRows,
+ computeCompareTableData,
+ getCachedBenchmarks,
+} from '@/lib/compare-ssr';
+
+export const dynamic = 'force-dynamic';
+export const runtime = 'nodejs';
+
+const DISPLAY_SIZE = { width: 1200, height: 675 };
+const IMAGE_SCALE = 2;
+const SIZE = {
+ width: DISPLAY_SIZE.width * IMAGE_SCALE,
+ height: DISPLAY_SIZE.height * IMAGE_SCALE,
+};
+const CHART_FRAME = { left: 0, top: 18, width: 746, height: 382 };
+const CHART = { left: 96, top: 42, width: 630, height: 272 };
+const COLORS = {
+ background: '#0d1117',
+ panel: '#121a23',
+ border: '#23303d',
+ muted: '#9aa7b5',
+ text: '#f3f7fb',
+ a: '#38d9a9',
+ b: '#f7b041',
+ grid: '#263544',
+ blue: '#0b86d1',
+};
+
+interface Point {
+ x: number;
+ y: number;
+}
+
+function money(value: number): string {
+ if (value >= 10) return `$${value.toFixed(1)}`;
+ if (value >= 1) return `$${value.toFixed(2)}`;
+ return `$${value.toFixed(3)}`;
+}
+
+function pointsPath(points: Point[]): string {
+ return points.map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.x} ${point.y}`).join(' ');
+}
+
+export async function GET(
+ _request: Request,
+ { params }: { params: Promise<{ slug: string }> },
+): Promise
{
+ const { slug } = await params;
+ const parsed = parseCompareSlug(slug);
+ if (!parsed || canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b) !== slug) {
+ return new Response('Not found', { status: 404 });
+ }
+
+ const rows = await getCachedBenchmarks(parsed.model.dbKeys);
+ const { sequence, precision } = pickPairDefaults(rows, parsed.a, parsed.b);
+ const { ssrRows, interactivityRange } = computeCompareTableData(
+ rows,
+ parsed.a,
+ parsed.b,
+ sequence,
+ precision,
+ );
+ const plottedRows = ssrRows.filter((row) => row.a || row.b);
+ const imageRows = computeCompareImageRows(
+ rows,
+ parsed.a,
+ parsed.b,
+ sequence,
+ precision,
+ interactivityRange,
+ ).filter((row) => row.a || row.b);
+ const curveRows = imageRows.length > 0 ? imageRows : plottedRows;
+
+ const aLabel = HW_REGISTRY[parsed.a]?.label ?? parsed.a.toUpperCase();
+ const bLabel = HW_REGISTRY[parsed.b]?.label ?? parsed.b.toUpperCase();
+ const costs = curveRows
+ .flatMap((row) => [row.a?.cost, row.b?.cost])
+ .filter((cost): cost is number => typeof cost === 'number' && Number.isFinite(cost));
+ const costMin = costs.length > 0 ? Math.min(...costs) : 0;
+ const costMax = costs.length > 0 ? Math.max(...costs) : 1;
+ const costPadding = Math.max((costMax - costMin) * 0.18, costMax * 0.08, 0.02);
+ const yMin = Math.max(0, costMin - costPadding);
+ const yMax = costMax + costPadding;
+ const xMin = curveRows.at(0)?.target ?? 0;
+ const xMax = curveRows.at(-1)?.target ?? 100;
+ const scaleX = (value: number) =>
+ CHART.left + (xMax === xMin ? CHART.width / 2 : ((value - xMin) / (xMax - xMin)) * CHART.width);
+ const scaleY = (value: number) =>
+ CHART.top +
+ CHART.height -
+ (yMax === yMin ? CHART.height / 2 : ((value - yMin) / (yMax - yMin)) * CHART.height);
+
+ const aPoints = curveRows
+ .filter((row) => row.a)
+ .map((row) => ({ x: scaleX(row.target), y: scaleY(row.a!.cost) }));
+ const bPoints = curveRows
+ .filter((row) => row.b)
+ .map((row) => ({ x: scaleX(row.target), y: scaleY(row.b!.cost) }));
+ const aHighlightPoints = plottedRows
+ .filter((row) => row.a)
+ .map((row) => ({ x: scaleX(row.target), y: scaleY(row.a!.cost) }));
+ const bHighlightPoints = plottedRows
+ .filter((row) => row.b)
+ .map((row) => ({ x: scaleX(row.target), y: scaleY(row.b!.cost) }));
+ const yTicks = Array.from({ length: 4 }, (_, index) => yMin + ((yMax - yMin) * index) / 3);
+ const workload = [sequence, precision?.toUpperCase()].filter(Boolean).join(' / ');
+
+ return new ImageResponse(
+
+
+
+
+ InferenceX Performance per Dollar
+
+
{parsed.model.label}
+
+ {aLabel} vs {bLabel} | Cost per Million Tokens
+
+
+
+
DEFAULT WORKLOAD
+
+ {workload || 'Default comparison'}
+
+
+ Lower cost is better
+
+
+
+
+
+
+
+
+ {yTicks.map((tick) => {
+ const y = scaleY(tick);
+ return (
+
+ );
+ })}
+ {aPoints.length > 1 && (
+
+ )}
+ {bPoints.length > 1 && (
+
+ )}
+ {aHighlightPoints.map((point, index) => (
+
+ ))}
+ {bHighlightPoints.map((point, index) => (
+
+ ))}
+
+ {yTicks.map((tick) => (
+
+ {money(tick)}
+
+ ))}
+ {plottedRows.map((row) => (
+
+ {row.target}
+
+ ))}
+
+ Interactivity (tok/s/user)
+
+
+
+
+
+ Matched Interactivity
+
+
+
+
+ {aLabel}
+
+
+
+ {bLabel}
+
+
+ {plottedRows.length > 0 ? (
+ plottedRows.map((row) => (
+
+
+ {row.target} tok/s/user
+
+
+
+ {row.a ? money(row.a.cost) : 'N/A'}
+
+
+ {row.b ? money(row.b.cost) : 'N/A'}
+
+
+
+ ))
+ ) : (
+
+ No matched cost data available.
+
+ )}
+
+
+
+
+
+ Owning-hyperscaler TCO | interpolated from benchmark results
+
+
+ inferencex.semianalysis.com
+
+
+
,
+ {
+ ...SIZE,
+ headers: {
+ 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
+ },
+ },
+ );
+}
diff --git a/packages/app/src/app/sitemap.ts b/packages/app/src/app/sitemap.ts
index dfda779b..d1717aa3 100644
--- a/packages/app/src/app/sitemap.ts
+++ b/packages/app/src/app/sitemap.ts
@@ -75,14 +75,17 @@ export default async function sitemap(): Promise {
changeFrequency: 'daily' as const,
priority: 0.7,
})),
- // Per-dollar variant URLs — same (model, pair) availability filter as the
- // /compare set, so the count matches exactly. Each is a distinct canonical
- // URL with its own SSR metadata, JSON-LD, and OG image.
- ...compareSlugs.map(({ modelSlug, a, b }) => ({
- url: `${BASE_URL}/compare-per-dollar/${canonicalCompareSlug(modelSlug, a, b)}`,
- lastModified: now,
- changeFrequency: 'daily' as const,
- priority: 0.7,
- })),
+ // Every indexed per-dollar landing page has a stable data graphic so image
+ // crawlers discover the PNG alongside the canonical comparison URL.
+ ...compareSlugs.map(({ modelSlug, a, b }) => {
+ const url = `${BASE_URL}/compare-per-dollar/${canonicalCompareSlug(modelSlug, a, b)}`;
+ return {
+ url,
+ images: [`${url}/performance-per-dollar.png`],
+ lastModified: now,
+ changeFrequency: 'daily' as const,
+ priority: 0.7,
+ };
+ }),
];
}
diff --git a/packages/app/src/lib/compare-ssr.ts b/packages/app/src/lib/compare-ssr.ts
index cc5adcc8..829a2985 100644
--- a/packages/app/src/lib/compare-ssr.ts
+++ b/packages/app/src/lib/compare-ssr.ts
@@ -258,6 +258,44 @@ export function computeCompareTableData(
return { defaultTargets, ssrRows, interactivityRange };
}
+/** Sample the same interpolated cost curve used for the comparison table for
+ * server-rendered image assets. More samples make the static PNG read like the
+ * interactive roofline without requiring browser-based chart capture. */
+export function computeCompareImageRows(
+ rows: BenchmarkRow[],
+ a: string,
+ b: string,
+ sequence: string | null,
+ precision: string | null,
+ interactivityRange: { min: number; max: number },
+): SsrInterpolatedRow[] {
+ if (!sequence || !precision || interactivityRange.max <= interactivityRange.min) return [];
+
+ const islOsl = sequenceToIslOsl(sequence);
+ if (!islOsl) return [];
+
+ const pointsA = buildGpuDataPoints(rows, a, islOsl.isl, islOsl.osl, precision);
+ const pointsB = buildGpuDataPoints(rows, b, islOsl.isl, islOsl.osl, precision);
+ if (pointsA.length === 0 && pointsB.length === 0) return [];
+
+ const sampleCount = 17;
+ const span = interactivityRange.max - interactivityRange.min;
+ return Array.from({ length: sampleCount }, (_, index) => {
+ const target = interactivityRange.min + (span * index) / (sampleCount - 1);
+ return {
+ target,
+ a:
+ pointsA.length > 0
+ ? interpolateForGPU(pointsA, target, 'interactivity_to_throughput', 'costh')
+ : null,
+ b:
+ pointsB.length > 0
+ ? interpolateForGPU(pointsB, target, 'interactivity_to_throughput', 'costh')
+ : null,
+ };
+ });
+}
+
// ---------------------------------------------------------------------------
// JSON-LD graph
// ---------------------------------------------------------------------------
@@ -729,6 +767,7 @@ export function buildJsonLd(
summaryA: PairSummary,
summaryB: PairSummary,
ssrRows: SsrInterpolatedRow[],
+ imageUrl?: string,
) {
const aLabel = HW_REGISTRY[a]?.label ?? a.toUpperCase();
const bLabel = HW_REGISTRY[b]?.label ?? b.toUpperCase();
@@ -793,6 +832,7 @@ export function buildJsonLd(
name: itemListName,
description: itemListDescription,
url,
+ ...(imageUrl && { image: imageUrl }),
itemListOrder: 'https://schema.org/ItemListOrderAscending',
numberOfItems: 2,
itemListElement: [jsonLdEntryFor(a, summaryA, 1), jsonLdEntryFor(b, summaryB, 2)],
@@ -804,6 +844,13 @@ export function buildJsonLd(
name: datasetName,
description: datasetDescription,
url,
+ ...(imageUrl && {
+ image: {
+ '@type': 'ImageObject',
+ contentUrl: imageUrl,
+ caption: datasetName,
+ },
+ }),
hasPart: comparisonRows,
},
]