diff --git a/src/app/alternatives/[slug]/page.tsx b/src/app/alternatives/[slug]/page.tsx index f58b3563..03726fe1 100644 --- a/src/app/alternatives/[slug]/page.tsx +++ b/src/app/alternatives/[slug]/page.tsx @@ -128,7 +128,19 @@ export default async function AlternativePage({ datePublished: getBenchCreatedAt(bench.slug).toISOString(), ...(citableAsOf(bench) ? { dateModified: bench.lastRunAt } : {}), variableMeasured: bench.metric, - isBasedOn: benchUrl, + // Cross-doc @id-only refs get validated as standalone + // incomplete Datasets by Google (same failure mode as PR #1442's + // isBasedOn fix). Inline the required fields per bench. + isBasedOn: { + "@type": "Dataset" as const, + "@id": `${benchUrl}#dataset`, + name: bench.title, + description: bench.subtitle, + url: benchUrl, + creator: CREATOR_PUBLISHER, + license: DATASET_LICENSE, + isAccessibleForFree: true, + }, distribution: [ { "@type": "DataDownload", diff --git a/src/app/answers/[slug]/page.tsx b/src/app/answers/[slug]/page.tsx index f53b22bc..ecf424c5 100644 --- a/src/app/answers/[slug]/page.tsx +++ b/src/app/answers/[slug]/page.tsx @@ -182,6 +182,9 @@ export default async function AnswerPage({ text: ans.question, acceptedAnswer: { "@type": "Answer", + // Google's Rich Results validator flags Answer nodes without a + // `name` field (parity with the compare-page FAQPage builder). + name: ans.question, text: capDescription(shortAnswer, 990), url, author: { "@id": `${SITE.url}/#org` }, @@ -190,6 +193,7 @@ export default async function AnswerPage({ if (expertTake) { questionNode.suggestedAnswer = { "@type": "Answer", + name: ans.question, text: capDescription(expertTake, 990), url, author: { "@id": PERSON_ID }, @@ -218,7 +222,16 @@ export default async function AnswerPage({ author: { "@id": `${SITE.url}/#org` }, publisher: { "@id": `${SITE.url}/#org` }, image: `${SITE.url}/api/og/${bench.slug}`, - isBasedOn: benchUrl, + // Inline the referenced bench Dataset (bare URL ref gets + // validated as a standalone incomplete Dataset by Google, same + // failure mode as PR #1442's isBasedOn fix). + isBasedOn: { + "@type": "Dataset", + "@id": `${benchUrl}#dataset`, + name: bench.title, + description: bench.subtitle, + url: benchUrl, + }, }, buildBreadcrumbJsonLd([ { name: "Home", item: SITE.url }, diff --git a/src/app/compare/[slug]/page.tsx b/src/app/compare/[slug]/page.tsx index 6db7cb9c..12659efd 100644 --- a/src/app/compare/[slug]/page.tsx +++ b/src/app/compare/[slug]/page.tsx @@ -717,12 +717,36 @@ export default async function ComparePage({ isAccessibleForFree: true, license: DATASET_LICENSE, measurementTechnique: `${SITE.url}/methodology`, - variableMeasured: shared.map((s) => ({ - "@type": "PropertyValue", + // PropertyValue in a Dataset variableMeasured needs a numeric `value` + // for Google Dataset Search + academic LLM tools to extract the + // structured fact. Fallback to the aggregate winner's p50 in the + // shared unit; skip PropertyValue.value entirely when neither side + // returned data so we never publish a fabricated zero. + variableMeasured: shared.map((s) => { + const winner = s.aggregateWinner === "a" ? s.aResult : s.bResult; + const v = winner?.p50; + return { + "@type": "PropertyValue" as const, + name: s.title, + unitText: s.unit, + ...(typeof v === "number" && v > 0 ? { value: v } : {}), + }; + }), + // isBasedOn is a cross-doc reference to each bench Dataset. Google + // validates each Dataset in isolation and does not stitch bare-URL + // refs, so previously flagged them as standalone Datasets without + // name/description (same failure mode as PR #1442's isBasedOn fix + // in dataset-jsonld.ts). Inline the required fields per bench. + isBasedOn: shared.map((s) => ({ + "@type": "Dataset" as const, + "@id": `${SITE.url}/benchmarks/${s.slug}#dataset`, name: s.title, - unitText: s.unit, + description: `${s.metric} (${s.unit}) benchmark on OpenChainBench: live measurements for ${a.name} and ${b.name}.`, + url: `${SITE.url}/benchmarks/${s.slug}`, + creator: CREATOR_PUBLISHER, + license: DATASET_LICENSE, + isAccessibleForFree: true, })), - isBasedOn: shared.map((s) => `${SITE.url}/benchmarks/${s.slug}`), distribution: shared.map((s) => ({ "@type": "DataDownload", encodingFormat: "application/json", @@ -772,6 +796,9 @@ export default async function ComparePage({ const faqJsonLd = { "@context": "https://schema.org", "@type": "FAQPage", + // GSC's Rich Results tester flags FAQPage nodes without an `@id` + // (every other FAQPage on the site emits one via the shared builder). + "@id": `${url}#faq`, // Google's Rich Results validator flags missing `name` on the // FAQPage parent even when Question.name is set. Add here + on // Answer nodes so the compare page matches the shared FAQ builder diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx index 793c4fdc..55dc11f6 100644 --- a/src/app/products/[slug]/page.tsx +++ b/src/app/products/[slug]/page.tsx @@ -16,6 +16,7 @@ import { import { Breadcrumb } from "@/components/breadcrumb"; import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld"; import { CREATOR_PUBLISHER, CITABLE_JSON_URL, DATASET_LICENSE } from "@/lib/dataset-jsonld"; +import { getBenchCreatedAt } from "@/lib/seo/bench-dates"; import { fetchHlBuilderStats, isHlBuilderSlug, @@ -446,9 +447,18 @@ export default async function ProviderPage({ ...(sameAs.length > 0 ? { sameAs } : {}), subjectOf: sorted.map((a) => ({ "@type": "Dataset", + // GSC + Google Dataset Search flag anonymous Datasets + // ("Unnamed item" with recommended fields missing) when the + // @id + datePublished are absent (audit 2026-07-26). Match + // the shape used by the bench page's own Dataset node so + // cross-page identifiers align. + "@id": `${SITE.url}/benchmarks/${a.benchmark.slug}#dataset`, name: a.benchmark.title, description: capDescription(a.benchmark.subtitle, 990), url: `${SITE.url}/benchmarks/${a.benchmark.slug}`, + identifier: a.benchmark.slug, + variableMeasured: a.benchmark.metric, + datePublished: getBenchCreatedAt(a.benchmark.slug).toISOString(), creator: CREATOR_PUBLISHER, publisher: CREATOR_PUBLISHER, isAccessibleForFree: true,