Skip to content

Commit ac9b5d7

Browse files
committed
feat(compare): enrich JSON-LD for Google Dataset Search and add breadcrumbs
Three SEO-only additions to the compare slug pages: 1. Dataset metadata polish — datePublished / dateModified derived from the benchmark row dates for this pair, plus keywords, isAccessibleForFree, and measurementTechnique. These are the fields Google Dataset Search actually surfaces. 2. distribution: DataDownload — points at /api/v1/benchmarks?model=… so the Dataset isn't just described, it's downloadable in machine-readable form. 3. BreadcrumbList JSON-LD on every /compare/[slug] and /compare-per-dollar/ [slug] page (Home → GPU Comparisons / GPU Performance per Dollar → A vs B (Model)). Drives the Google SERP breadcrumb trail. No behavioral changes to the UI — only the SSR JSON-LD payload grows.
1 parent 28fb15b commit ac9b5d7

3 files changed

Lines changed: 99 additions & 0 deletions

File tree

packages/app/src/app/compare-per-dollar/[slug]/page.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
import { getAllComparableCompareSlugs } from '@/lib/compare-availability';
1515
import { getGpuSpecs } from '@/lib/constants';
1616
import {
17+
buildBreadcrumbJsonLd,
1718
buildJsonLd,
1819
compareTableNarrative,
1920
computeCompareTableData,
21+
dateRangeForPair,
2022
getCachedBenchmarks,
2123
KNOWN_MODELS,
2224
KNOWN_PRECISIONS,
@@ -121,6 +123,7 @@ export default async function ComparePerDollarPage({ params, searchParams }: Pro
121123

122124
const url = `${SITE_URL}/compare-per-dollar/${canonical}`;
123125
const imageUrl = `${url}/performance-per-dollar.png`;
126+
const { oldest, newest } = dateRangeForPair(rows, parsed.a, parsed.b);
124127
const jsonLd = buildJsonLd(
125128
'per-dollar',
126129
parsed.model,
@@ -131,6 +134,14 @@ export default async function ComparePerDollarPage({ params, searchParams }: Pro
131134
summaryB,
132135
ssrRows,
133136
imageUrl,
137+
oldest,
138+
newest,
139+
parsed.model.displayName,
140+
);
141+
const breadcrumbJsonLd = buildBreadcrumbJsonLd(
142+
'per-dollar',
143+
compareModelDisplayLabel(parsed.model, parsed.a, parsed.b),
144+
url,
134145
);
135146
const label = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b);
136147
const aMeta = HW_REGISTRY[parsed.a];
@@ -155,6 +166,7 @@ export default async function ComparePerDollarPage({ params, searchParams }: Pro
155166
return (
156167
<>
157168
<JsonLd data={jsonLd} />
169+
<JsonLd data={breadcrumbJsonLd} />
158170
<ComparePerDollarPageClient
159171
a={parsed.a}
160172
b={parsed.b}

packages/app/src/app/compare/[slug]/page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import {
1313
} from '@/lib/compare-slug';
1414
import { getAllComparableCompareSlugs } from '@/lib/compare-availability';
1515
import {
16+
buildBreadcrumbJsonLd,
1617
buildJsonLd,
1718
compareTableNarrative,
1819
computeCompareTableData,
20+
dateRangeForPair,
1921
getCachedBenchmarks,
2022
KNOWN_MODELS,
2123
KNOWN_PRECISIONS,
@@ -134,6 +136,7 @@ export default async function ComparePage({ params, searchParams }: Props) {
134136
);
135137

136138
const url = `${SITE_URL}/compare/${canonical}`;
139+
const { oldest, newest } = dateRangeForPair(rows, parsed.a, parsed.b);
137140
const jsonLd = buildJsonLd(
138141
'full',
139142
parsed.model,
@@ -143,6 +146,15 @@ export default async function ComparePage({ params, searchParams }: Props) {
143146
summaryA,
144147
summaryB,
145148
ssrRows,
149+
undefined,
150+
oldest,
151+
newest,
152+
parsed.model.displayName,
153+
);
154+
const breadcrumbJsonLd = buildBreadcrumbJsonLd(
155+
'full',
156+
compareModelDisplayLabel(parsed.model, parsed.a, parsed.b),
157+
url,
146158
);
147159
const label = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b);
148160
const aMeta = HW_REGISTRY[parsed.a];
@@ -161,6 +173,7 @@ export default async function ComparePage({ params, searchParams }: Props) {
161173
return (
162174
<>
163175
<JsonLd data={jsonLd} />
176+
<JsonLd data={breadcrumbJsonLd} />
164177
<ComparePageClient
165178
a={parsed.a}
166179
b={parsed.b}

packages/app/src/lib/compare-ssr.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
AUTHOR_NAME,
1515
AUTHOR_URL,
1616
HW_REGISTRY,
17+
SITE_URL,
1718
sequenceToIslOsl,
1819
} from '@semianalysisai/inferencex-constants';
1920
import { FIXTURES_MODE, JSON_MODE, getDb } from '@semianalysisai/inferencex-db/connection';
@@ -773,6 +774,46 @@ export function bucketComparePairsByVendor(modelSlug: string, pairs: ComparePair
773774
return { cross, nvidia, amd };
774775
}
775776

777+
/** Breadcrumb trail for a compare slug page. Emitted alongside the main
778+
* Dataset/ItemList JSON-LD so Google can render the Home → Compare → A vs B
779+
* trail in search results. Variant chooses /compare vs /compare-per-dollar. */
780+
export function buildBreadcrumbJsonLd(
781+
variant: CompareJsonLdVariant,
782+
pairLabel: string,
783+
url: string,
784+
) {
785+
const indexUrl =
786+
variant === 'per-dollar' ? `${SITE_URL}/compare-per-dollar` : `${SITE_URL}/compare`;
787+
const indexName = variant === 'per-dollar' ? 'GPU Performance per Dollar' : 'GPU Comparisons';
788+
return {
789+
'@context': 'https://schema.org',
790+
'@type': 'BreadcrumbList',
791+
itemListElement: [
792+
{ '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL },
793+
{ '@type': 'ListItem', position: 2, name: indexName, item: indexUrl },
794+
{ '@type': 'ListItem', position: 3, name: pairLabel, item: url },
795+
],
796+
};
797+
}
798+
799+
/** Pick the oldest and newest benchmark dates among rows whose hardware matches
800+
* the compared pair — used to populate Dataset.datePublished / dateModified. */
801+
export function dateRangeForPair(
802+
rows: BenchmarkRow[],
803+
a: string,
804+
b: string,
805+
): { oldest?: string; newest?: string } {
806+
let oldest: string | undefined;
807+
let newest: string | undefined;
808+
for (const row of rows) {
809+
if (row.hardware !== a && row.hardware !== b) continue;
810+
if (!row.date) continue;
811+
if (oldest === undefined || row.date < oldest) oldest = row.date;
812+
if (newest === undefined || row.date > newest) newest = row.date;
813+
}
814+
return { oldest, newest };
815+
}
816+
776817
export function buildJsonLd(
777818
variant: CompareJsonLdVariant,
778819
model: CompareModelSlug,
@@ -783,6 +824,13 @@ export function buildJsonLd(
783824
summaryB: PairSummary,
784825
ssrRows: SsrInterpolatedRow[],
785826
imageUrl?: string,
827+
/** ISO date of oldest benchmark row contributing to this dataset. */
828+
datePublished?: string,
829+
/** ISO date of newest benchmark row — drives Google Dataset Search freshness. */
830+
dateModified?: string,
831+
/** Display model name accepted by /api/v1/benchmarks?model=…, used to wire the
832+
* Dataset's `distribution: DataDownload` to a real machine-readable export. */
833+
modelApiKey?: string,
786834
) {
787835
const aLabel = HW_REGISTRY[a]?.label ?? a.toUpperCase();
788836
const bLabel = HW_REGISTRY[b]?.label ?? b.toUpperCase();
@@ -860,11 +908,37 @@ export function buildJsonLd(
860908
description: datasetDescription,
861909
url,
862910
license: 'https://www.apache.org/licenses/LICENSE-2.0',
911+
isAccessibleForFree: true,
912+
measurementTechnique:
913+
'Open-source automated GPU CI/CD inference benchmark (github.com/SemiAnalysisAI/InferenceX)',
914+
keywords: [
915+
'AI inference benchmark',
916+
'GPU comparison',
917+
variant === 'per-dollar' ? 'cost per million tokens' : 'inference latency',
918+
variant === 'per-dollar' ? 'performance per dollar' : 'tokens per second',
919+
model.label,
920+
aLabel,
921+
bLabel,
922+
HW_REGISTRY[a]?.vendor,
923+
HW_REGISTRY[b]?.vendor,
924+
]
925+
.filter(Boolean)
926+
.join(', '),
927+
...(datePublished && { datePublished }),
928+
...(dateModified && { dateModified }),
863929
creator: {
864930
'@type': 'Organization',
865931
name: AUTHOR_NAME,
866932
url: AUTHOR_URL,
867933
},
934+
...(modelApiKey && {
935+
distribution: {
936+
'@type': 'DataDownload',
937+
encodingFormat: 'application/json',
938+
contentUrl: `${SITE_URL}/api/v1/benchmarks?model=${encodeURIComponent(modelApiKey)}`,
939+
name: `${model.label} latest benchmark rows (JSON)`,
940+
},
941+
}),
868942
...(imageUrl && {
869943
image: {
870944
'@type': 'ImageObject',

0 commit comments

Comments
 (0)