From bee3c8c32c17151db6b1701cf2310e8b4d905fef Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:35:55 +0200 Subject: [PATCH 1/2] seo: fix GSC Dataset warnings (embed creator/publisher + distribution on products, alternatives, compare) (#1291) Co-authored-by: Florent Tapponnier --- src/app/alternatives/[slug]/page.tsx | 20 +++++++++++++++++--- src/app/compare/[slug]/page.tsx | 7 +++++-- src/app/products/[slug]/page.tsx | 19 +++++++++++++++++-- src/lib/dataset-jsonld.ts | 14 +++++++++++++- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/app/alternatives/[slug]/page.tsx b/src/app/alternatives/[slug]/page.tsx index 3186c168..3809608d 100644 --- a/src/app/alternatives/[slug]/page.tsx +++ b/src/app/alternatives/[slug]/page.tsx @@ -12,6 +12,7 @@ import { getBenchCreatedAt } from "@/lib/seo/bench-dates"; import { capDescription } from "@/lib/seo-text"; import { SectionLabel, SummaryStat } from "@/components/summary-stat"; import { SITE } from "@/data/site"; +import { CREATOR_PUBLISHER, CITABLE_JSON_URL, DATASET_LICENSE, HF_DATASET_URL } from "@/lib/dataset-jsonld"; import { loadAlternative } from "@/lib/alternatives"; import { Breadcrumb } from "@/components/breadcrumb"; import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld"; @@ -120,14 +121,27 @@ export default async function AlternativePage({ bench.metric, ...bench.results.map((r) => r.name), ].join(", "), - creator: { "@id": `${SITE.url}/#org` }, - publisher: { "@id": `${SITE.url}/#org` }, + creator: CREATOR_PUBLISHER, + publisher: CREATOR_PUBLISHER, isAccessibleForFree: true, - license: "https://creativecommons.org/licenses/by/4.0/", + license: DATASET_LICENSE, datePublished: getBenchCreatedAt(bench.slug).toISOString(), ...(citableAsOf(bench) ? { dateModified: bench.lastRunAt } : {}), variableMeasured: bench.metric, isBasedOn: benchUrl, + distribution: [ + { + "@type": "DataDownload", + encodingFormat: "application/json", + contentUrl: `${SITE.url}/api/stat/${bench.slug}`, + }, + { + "@type": "DataDownload", + encodingFormat: "application/json", + contentUrl: CITABLE_JSON_URL, + }, + ], + sameAs: [HF_DATASET_URL], }, { "@type": "Article", diff --git a/src/app/compare/[slug]/page.tsx b/src/app/compare/[slug]/page.tsx index 302fc68b..aeab9e75 100644 --- a/src/app/compare/[slug]/page.tsx +++ b/src/app/compare/[slug]/page.tsx @@ -17,6 +17,7 @@ import { capDescription } from "@/lib/seo-text"; import { Breadcrumb } from "@/components/breadcrumb"; import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld"; import { SITE } from "@/data/site"; +import { CREATOR_PUBLISHER, DATASET_LICENSE } from "@/lib/dataset-jsonld"; import type { Benchmark } from "@/types/benchmark"; import { computeInputsHash, @@ -711,8 +712,10 @@ export default async function ComparePage({ name: `${a.name} vs ${b.name} OpenChainBench measurements`, description: `Side by side live measurements for ${a.name} and ${b.name} on ${shared.length} shared OpenChainBench benchmarks.`, url, - creator: { "@id": `${SITE.url}/#org` }, - license: "https://creativecommons.org/licenses/by/4.0/", + creator: CREATOR_PUBLISHER, + publisher: CREATOR_PUBLISHER, + isAccessibleForFree: true, + license: DATASET_LICENSE, measurementTechnique: `${SITE.url}/methodology`, variableMeasured: shared.map((s) => ({ "@type": "PropertyValue", diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx index 898678be..cbc63040 100644 --- a/src/app/products/[slug]/page.tsx +++ b/src/app/products/[slug]/page.tsx @@ -15,6 +15,7 @@ import { } from "@/data/provider-registry"; import { Breadcrumb } from "@/components/breadcrumb"; import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld"; +import { CREATOR_PUBLISHER, CITABLE_JSON_URL, DATASET_LICENSE } from "@/lib/dataset-jsonld"; import { fetchHlBuilderStats, isHlBuilderSlug, @@ -445,8 +446,22 @@ export default async function ProviderPage({ name: a.benchmark.title, description: capDescription(a.benchmark.subtitle, 990), url: `${SITE.url}/benchmarks/${a.benchmark.slug}`, - creator: { "@id": `${SITE.url}/#org` }, - license: "https://creativecommons.org/licenses/by/4.0/", + creator: CREATOR_PUBLISHER, + publisher: CREATOR_PUBLISHER, + isAccessibleForFree: true, + license: DATASET_LICENSE, + distribution: [ + { + "@type": "DataDownload", + encodingFormat: "application/json", + contentUrl: `${SITE.url}/api/stat/${a.benchmark.slug}`, + }, + { + "@type": "DataDownload", + encodingFormat: "application/json", + contentUrl: CITABLE_JSON_URL, + }, + ], })), }, // NB: previously emitted a SoftwareApplication node here, but Google's diff --git a/src/lib/dataset-jsonld.ts b/src/lib/dataset-jsonld.ts index 59a3c786..6db08662 100644 --- a/src/lib/dataset-jsonld.ts +++ b/src/lib/dataset-jsonld.ts @@ -60,13 +60,25 @@ const KEYWORDS = [ "infrastructure", ]; -const CREATOR_PUBLISHER = { +/** Embedded Org node used for Dataset `creator` / `publisher`. Exported + * so pages emitting a Dataset outside the home graph can inline the + * full node (Google Search Console flags an `@id`-only reference as + * "missing field creator" when the referenced Org isn't declared on + * the same page). */ +export const CREATOR_PUBLISHER = { "@type": "Organization", "@id": `${SITE.url}/#org`, name: SITE.name, url: SITE.url, } as const; +/** Full JSON download of the current benchmark corpus. Used as the + * default `distribution.contentUrl` for Dataset nodes emitted outside + * `/benchmarks/[slug]` (products, alternatives, compare) so every + * Dataset carries a machine-readable download link, satisfying the + * Google Dataset "encodingFormat" / "contentUrl" requirements. */ +export const CITABLE_JSON_URL = `${SITE.url}/api/citable`; + /** * Site-wide Dataset entry. Emitted on the home page so Google Dataset * Search and Perplexity have a single canonical record pointing at both From 3e0bd8b34812174ae0f0d294bd0cfbdebdcebace Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:56:20 +0200 Subject: [PATCH 2/2] seo: keep inline creator on bench Dataset (GSC flags @id-only refs as missing) (#1294) Co-authored-by: Florent Tapponnier --- src/app/benchmarks/[slug]/page.tsx | 58 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/app/benchmarks/[slug]/page.tsx b/src/app/benchmarks/[slug]/page.tsx index d0196e3d..d19103b6 100644 --- a/src/app/benchmarks/[slug]/page.tsx +++ b/src/app/benchmarks/[slug]/page.tsx @@ -325,36 +325,34 @@ export default async function BenchmarkPage({ } : null, }); - const datasetNode = { - ...buildBenchDatasetJsonLd({ - slug: benchmark.slug, - name: benchmark.seoTitle ?? benchmark.title, - alternateName: benchmark.title, - // Google Rich Results validator caps description at ~1000 chars even - // though schema.org Dataset allows up to 5000. Keep it under 990 to - // avoid the "Invalid string length" warning that strips rich snippets. - description: capDescription(benchmark.abstract, 990), - url: benchmarkUrl, - variableMeasured, - category: benchmark.category, - datePublished: getBenchCreatedAt(benchmark.slug).toISOString(), - // For drafts (no measurement history) skip dateModified so the - // structured-data channel does not spoof freshness. Same rule - // applied to /api/citable, /api/stat and the MCP surface. - ...(citableAsOf(benchmark) - ? { dateModified: benchmark.lastRunAt } - : {}), - measurementTechnique: benchmark.methodology.join(" "), - }), - // Re-bind creator + publisher to the global @id reference so the bench - // Dataset resolves to the same Organization node emitted by layout.tsx - // when crawlers stitch the site graph back together. The helper sets - // an inline Organization for standalone consumption; the page-level - // override is the more accurate shape on a site that already declares - // the Organization globally. - creator: { "@id": `${SITE.url}/#org` }, - publisher: { "@id": `${SITE.url}/#org` }, - }; + // Keep the inline creator/publisher from buildBenchDatasetJsonLd. An + // earlier override rebound them to `{ "@id": ... }` references + // pointing at the Organization emitted by layout.tsx, but Google + // Search Console validates each Dataset in isolation - it does not + // stitch cross-document @id refs - and flagged the reference-only + // shape as "missing field creator" on /benchmarks/*. The inline Org + // still shares the `@id` with the layout node so JSON-LD stitchers + // (Perplexity, StatisticalReport isBasedOn) resolve them as one. + const datasetNode = buildBenchDatasetJsonLd({ + slug: benchmark.slug, + name: benchmark.seoTitle ?? benchmark.title, + alternateName: benchmark.title, + // Google Rich Results validator caps description at ~1000 chars even + // though schema.org Dataset allows up to 5000. Keep it under 990 to + // avoid the "Invalid string length" warning that strips rich snippets. + description: capDescription(benchmark.abstract, 990), + url: benchmarkUrl, + variableMeasured, + category: benchmark.category, + datePublished: getBenchCreatedAt(benchmark.slug).toISOString(), + // For drafts (no measurement history) skip dateModified so the + // structured-data channel does not spoof freshness. Same rule + // applied to /api/citable, /api/stat and the MCP surface. + ...(citableAsOf(benchmark) + ? { dateModified: benchmark.lastRunAt } + : {}), + measurementTechnique: benchmark.methodology.join(" "), + }); // StatisticalReport companion. Only emitted when we have a defensible // leader so the shape never publishes an Observation with a fabricated // measured value. temporalCoverage uses the ISO 8601 interval form: