Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/app/alternatives/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 27 additions & 3 deletions src/app/answers/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`],
},
};
}
Expand Down Expand Up @@ -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` },
Expand All @@ -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 },
Expand Down Expand Up @@ -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 },
Expand Down
28 changes: 24 additions & 4 deletions src/app/benchmarks/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -507,9 +512,24 @@ export default async function BenchmarkPage({
<h1 className="mt-5 display text-3xl sm:text-4xl md:text-5xl tracking-tight text-ink break-words">
{benchmark.title}
</h1>
<p className="mt-4 max-w-3xl text-lg sm:text-xl text-ink-muted leading-snug break-words">
{benchmark.subtitle}
</p>
{/* 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 ? (
<p className="mt-4 max-w-3xl text-lg sm:text-xl text-ink leading-snug break-words">
<span className="font-medium">{sentence}</span>{" "}
<span className="text-ink-muted">{benchmark.subtitle}</span>
</p>
) : (
<p className="mt-4 max-w-3xl text-lg sm:text-xl text-ink-muted leading-snug break-words">
{benchmark.subtitle}
</p>
)}

{/* Compact "At a glance" card. Rendered as a native <details>
element so it collapses to a one line summary on click while
Expand Down
61 changes: 57 additions & 4 deletions src/app/compare/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -885,6 +912,32 @@ export default async function ComparePage({
</div>
</section>

{/* 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. */}
<section className="mt-12 max-w-3xl">
<h2 className="text-[11px] font-medium uppercase tracking-[0.18em] text-ink-muted">
Frequently asked questions
</h2>
<div className="mt-4 space-y-4">
{faqEntries.map((e) => (
<details
key={e.q}
className="border border-rule rounded-lg px-4 py-3"
>
<summary className="cursor-pointer text-sm font-medium text-ink leading-tight">
{e.q}
</summary>
<p className="mt-3 text-sm leading-relaxed text-ink-soft">
{e.a}
</p>
</details>
))}
</div>
</section>

<section className="mt-12 max-w-3xl">
<h2 className="text-[11px] font-medium uppercase tracking-[0.18em] text-ink-muted">
How this pair was selected
Expand Down
10 changes: 10 additions & 0 deletions src/app/products/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading