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/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({

+
+ {`${modelLabel}: +
+ {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/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 { 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, }, ]