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..28626486 100644 --- a/src/app/answers/[slug]/page.tsx +++ b/src/app/answers/[slug]/page.tsx @@ -75,14 +75,25 @@ export async function generateMetadata({ type: "article", url, siteName: SITE.name, - images: [{ url: `${SITE.url}/opengraph-image`, width: 1200, height: 630 }], + // Use the referenced bench's OG card instead of the site-wide + // default. Perplexity / ChatGPT / Claude Deep Research scrape the + // og:image alongside the answer text; showing the bench-specific + // leader card gives the AI a real datapoint next to the answer + // rather than the generic homepage tile. + images: [ + { + url: `${SITE.url}/api/og/${ans.bench.slug}`, + width: 1200, + height: 630, + }, + ], }, twitter: { card: "summary_large_image", site: SITE.twitter, title, description, - images: [`${SITE.url}/twitter-image`], + images: [`${SITE.url}/api/og/${ans.bench.slug}`], }, }; } @@ -182,6 +193,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 +204,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 +233,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/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index 6aa8ba96..f2f72fd7 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -165,7 +165,12 @@ export async function generateMetadata({ }, other: { citation_title: metaTitle, - citation_author: "OpenChainBench", + // Highwire Press citation_author expects a person name (Scholar + // routinely rejects records where author reads as a publisher + // organisation). The Person is also anchored in the JSON-LD + // Dataset creator + StatisticalReport contributor, so the two + // signals agree. + citation_author: "Florent Tapponnier", citation_publisher: "OpenChainBench", ...(isoPubDate ? { citation_publication_date: isoPubDate } : {}), citation_doi: "10.5281/zenodo.20800312", @@ -507,9 +512,24 @@ export default async function BenchmarkPage({

{benchmark.title}

-

- {benchmark.subtitle} -

+ {/* Grounding-line first-under-H1: Perplexity / ChatGPT search / + Claude Deep Research scrape the first prose sentence after the + H1 as the "citable claim." Previously the methodology-flavoured + subtitle sat here; the current-leader sentence carries a + concrete provider + number + unit that AI answer engines quote + verbatim ("Codex leads head lag at 0.8 s (p50, 24h)..."). Fall + back to the subtitle when no defensible leader exists (draft, + insufficient, cold start). */} + {sentence ? ( +

+ {sentence}{" "} + {benchmark.subtitle} +

+ ) : ( +

+ {benchmark.subtitle} +

+ )} {/* Compact "At a glance" card. Rendered as a native
element so it collapses to a one line summary on click while diff --git a/src/app/compare/[slug]/page.tsx b/src/app/compare/[slug]/page.tsx index 6db7cb9c..a3d28e45 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 @@ -885,6 +912,32 @@ export default async function ComparePage({ + {/* Visible FAQ parity with the FAQPage JSON-LD above. Google + requires visible Q&A text matching the schema entries or it + silently drops the rich result — the compare pages had FAQ + jsonld without any visible questions, so the FAQPage never + rendered in the SERP snippet. */} +
+

+ Frequently asked questions +

+
+ {faqEntries.map((e) => ( +
+ + {e.q} + +

+ {e.a} +

+
+ ))} +
+
+

How this pair was selected 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,