Skip to content

Add schema.org/Dataset support to the JSON-LD @graph - #21084

Open
workprentice[bot] wants to merge 1 commit into
masterfrom
seo/dataset-schema-support
Open

Add schema.org/Dataset support to the JSON-LD @graph#21084
workprentice[bot] wants to merge 1 commit into
masterfrom
seo/dataset-schema-support

Conversation

@workprentice

@workprentice workprentice Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What

Adds schema.org/Dataset support to the site-wide JSON-LD @graph, following the same opt-in collector pattern already used for ItemList and FAQPage.

A page opts in with a dataset object in frontmatter:

dataset:
  name: "State of IaC 2026 Survey Dataset"
  description: "Aggregated results from Pulumi's 2026 State of Infrastructure as Code survey."
  license: "https://creativecommons.org/licenses/by/4.0/"
  date_published: "2026-09-15"
  keywords:
    - "infrastructure as code"
    - "survey"
  variable_measured:
    - "IaC tool adoption"
    - "team size"
  temporal_coverage: "2026"
  is_accessible_for_free: true
  distribution:
    - content_url: "https://www.pulumi.com/state-of-iac/2026/data.csv"
      encoding_format: "text/csv"

Every field beyond name is optional; the collector only emits what a page actually declares, and returns an empty dict (a no-op) for every page that hasn't opted in — no existing page's markup changes. creator defaults to the shared Pulumi Organization @id already defined on every page, with an optional creator_name override for data attributed to a named person or team.

Why

None of the site's existing structured-data types (WebSite, BreadcrumbList, SoftwareSourceCode, HowTo, FAQPage, TechArticle/APIReference, ItemList) tell search engines or AI systems "this page publishes a dataset." Pulumi's upcoming State of IaC 2026 survey report is exactly that kind of page — original survey data, not just narrative content — and without this, it would render with only Article/FAQPage markup, which describes the write-up but not the underlying data. Dataset is the schema.org type built for exactly this case, and gives search/AI systems machine-readable access to license, coverage, variables measured, and the actual data files.

Implementation

  • New layouts/partials/schema/collectors/dataset-entity.html, mirroring itemlist-entity.html's structure and gating.
  • Wired into layouts/partials/schema/graph-builder.html immediately before the ItemList block, with hasPart set on the page's WebPage node and the Dataset node appended to the @graph under its own @id.

Verification

Hugo template changes to layouts/partials/schema/ don't get meaningfully checked by hugo build alone against this repo's asset pipeline in a sandboxed environment, so I verified with an isolated Hugo harness: a minimal site that copies layouts/partials/schema/ (plus its actual dependencies) verbatim, renders a page with a full dataset frontmatter block, a page with a minimal one (name + creator_name only), and a control page with no dataset frontmatter at all, then parses every emitted ld+json block with a JSON parser. Results:

  • The full and minimal cases both produce valid, correctly nested Dataset JSON-LD with the right @id resolution and hasPart wiring, and only emit the optional fields actually supplied.
  • The control page's @graph is unchanged — no Dataset node appears, and every previously-existing node type on that page still renders.

CI will additionally validate the template compiles across the full site.


🧠 This PR was created by workprentice.

Adds a dataset-entity.html collector, following the existing
itemlist-entity.html opt-in pattern: a page opts in with a "dataset"
object in frontmatter (name, description, license, datePublished,
keywords, variableMeasured, temporalCoverage, isAccessibleForFree,
distribution), and the collector returns an empty dict (a no-op) for
every page that hasn't opted in.

Wires into graph-builder.html the same way itemlist and the comparison
table do: collected before the WebPage entity is finalized so hasPart
can point at it, then appended to the graph under its own @id.

creator defaults to the shared Pulumi Organization @id already defined
on every page, with an optional creator_name override for data
attributed to a named person or team.

This is prep for the State of IaC 2026 survey report, an upcoming page
publishing original survey statistics: Dataset markup is what makes
that data machine-extractable for search and AI systems, distinct from
the Article/FAQPage markup a page like that would otherwise fall back
to.
@github-actions github-actions Bot added review:triaging Claude Triage is currently classifying the PR domain:infra PR touches workflows, scripts, infra, Makefile, or build config review:in-progress Claude review is currently running and removed review:triaging Claude Triage is currently classifying the PR labels Aug 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Pre-merge Review — Last updated 2026-08-24T12:40:56Z

Tip

Summary: This PR adds a schema.org/Dataset collector to the JSON-LD @graph — a new dataset-entity.html partial plus the wiring in graph-builder.html — modelled closely on the existing itemlist-entity.html opt-in (frontmatter-driven, empty dict when the page hasn't opted in, so it's a no-op everywhere else). No user-facing content changes. The failure mode that matters here is emitting structured data that's wrong or incomplete on the pages that do opt in, since nothing on the page surfaces it and a bad node is only visible in a rich-results test. Two such issues came out of a read of the new partial against its sibling collectors: the new hasPart write clobbers the ItemList one on any page using both, and the documented creator_name override emits the wrong @type for a person. The creator @id reference does resolve — graph-builder.html emits a matching Organization node on both home and non-home pages. Passes run: infra review of both changed templates, a read against the comparison-table / ItemList / Organization collectors in the same file, and external verification of the URLs in the new doc comment.

Review confidence:

Dimension Level Notes
mechanics MEDIUM Templates were read, not rendered — make build and the lint job are the authority on Hugo-syntax validity, and no page in the repo opts into dataset yet, so the new branch is unexercised.
facts HIGH
Investigation log
  • Cross-sibling reads: not run (not in a templated section)
  • External claim verification: 1 of 4 claims verified (0 unverifiable, 0 contradicted) · 4 specialists (numerical, cross-reference, capability, framing); 0 cross-specialist corroborations · routed: 0 inline, 2 Pass 1, 0 Pass 2, 2 Pass 3 (verified 1, contradicted 0, unverifiable 1).
  • Cited-claim spot-checks: not run (no cited claims)
  • Frontmatter sweep: not run (no frontmatter in diff)
  • Temporal-trigger sweep: ran (recency words present in diff; spot-check in-review)
  • Code execution: not run (no static/programs/ change)
  • Code-examples checks: not run (no fenced code blocks in content files)
  • Editorial-balance pass: not run (not under content/blog/)
🚨 Outstanding ⚠️ Low-confidence 💡 Pre-existing ✅ Resolved
2 0 0 0

🔍 Verification trail

4 claims extracted · 1 verified · 0 unverifiable · 0 contradicted
  • L9 in layouts/partials/schema/collectors/dataset-entity.html "license: 'https://creativecommons.org/licenses/by/4.0/'" → ✅ verified (evidence: The URL resolves to the live, canonical Creative Commons CC BY 4.0 deed page: "CC BY 4.0 · https://creativecommons.org/licenses/by/4.0/ See the legal code · Share — copy and redistribute the material in any medium or format for any…; source: https://creativecommons.org/licenses/by/4.0/)
  • L10 in layouts/partials/schema/collectors/dataset-entity.html "date_published: '2026-09-15'" → ➖ not-a-claim (evidence: This is a line in a schema.org structured-data partial template (dataset-entity.html) that emits a date_published field, presumably populated from front matter or a placeholder value for schema generation purposes — not a falsifiable…; source: layouts/partials/schema/collectors/dataset-entity.html)
  • L20 in layouts/partials/schema/collectors/dataset-entity.html "- content_url: 'https://www.pulumi.com/state-of-iac/2026/data.csv'" → ➖ not-a-claim (evidence: The URL appears only inside a template file's doc-comment example block illustrating how to populate dataset.distribution.content_url in frontmatter; it's sample/placeholder content for a future "State of IaC 2026" page, not an…; source: repo:layouts/partials/schema/collectors/dataset-entity.html (comment block, lines 1-27))
  • L87 in layouts/partials/schema/collectors/dataset-entity.html "{{ $entity = merge $entity (dict 'creator' (dict '@id' 'https://www.pulumi.com/#organization')) }}" → ➖ not-a-claim (evidence: This is a schema.org JSON-LD @id value used to link an entity to an Organization node defined elsewhere on the same page (a common pattern of using the page URL + fragment as a stable internal identifier). It is not an assertion about…; source: repo:layouts/partials/schema/collectors/dataset-entity.html)
  • L72 in layouts/partials/schema/graph-builder.html "the new hasPart merge is the third writer of the same single-valued WebPage key" → ⚔️ mismatch (evidence: lines 40, 56 and 72 each merge $webpage (dict "hasPart" …), so on a page declaring both itemlist and dataset the last write wins and the earlier hasPart reference is silently dropped from the emitted WebPage node; source: repo:layouts/partials/schema/graph-builder.html lines 35-74)
  • L85 in layouts/partials/schema/collectors/dataset-entity.html "creator_name override is documented for 'a named person or third party' but always emits @type Organization" → ⚔️ mismatch (evidence: the doc comment at lines 27-29 says the override is for data "produced by a named person or third party", while line 85 hardcodes "@type" "Organization"; schema.org/Dataset expects Person for an individual creator; source: repo:layouts/partials/schema/collectors/dataset-entity.html lines 27-29, 84-88)

🚨 Outstanding in this PR

These must be resolved or refuted before merging.

  • [L72] layouts/partials/schema/graph-builder.htmlhasPart is a single-valued key on $webpage, and this block is now the third writer of it (comparison table at L40, ItemList at L56, Dataset here). merge overwrites rather than accumulates, so a page that opts into both itemlist and dataset emits only "hasPart": {"@id": "#dataset"} — the ItemList node still lands in the @graph but is no longer referenced from the WebPage, which is the link crawlers follow. Collect the references and write them once instead:

    {{/* near the top, beside `$graph := slice` */}}
    {{ $hasParts := slice }}
    
    {{/* in each of the three blocks, replace the `merge $webpage …` line with: */}}
    {{ $hasParts = $hasParts | append (dict "@id" "#dataset") }}
    
    {{/* after the last collector block */}}
    {{ if gt (len $hasParts) 0 }}
      {{ $webpage = merge $webpage (dict "hasPart" $hasParts) }}
    {{ end }}
    

    schema.org accepts a hasPart array, so this is correct for the one-part case too. If you'd rather keep the change scoped to Dataset, guard it instead ({{ if not (isset $webpage "hasPart") }}) and say in the comment which entity wins — but the collision only grows with the next collector.

  • [L85] layouts/partials/schema/collectors/dataset-entity.html — the doc comment says a page can override creator "when the data was produced by a named person or third party", but the override branch hardcodes "@type" "Organization", so naming a person emits a person as an Organization. Either narrow the comment to third-party organizations, or let the page say which it is:

    {{ if $d.creator_name }}
      {{ $entity = merge $entity (dict "creator" (dict "@type" (default "Organization" $d.creator_type) "name" $d.creator_name)) }}
    {{ else }}
    

    If you take the creator_type route, document it in the frontmatter example block at the top of the file alongside creator_name.

⚠️ Low-confidence

No low-confidence findings.

💡 Pre-existing issues in touched files (optional)

No pre-existing issues in touched files.

✅ Resolved since last review

No items resolved since the last review.

📜 Review history

  • 2026-08-24T12:40:56Z — First review: two structured-data issues in the new Dataset collector — the hasPart write overwrites the ItemList reference, and the creator_name override emits @type: Organization for what the comment describes as a person. (1ff8748)

  • Refresh this review — comment @claude #update-review. Say what you fixed, or which finding you dispute and why; both work in the same mention.
  • Ask for anything else — comment @claude with no hashtag (questions, one-off fixes). Leaves this review untouched.

Important

Please don't hide, resolve, or delete this comment! It breaks things!

📖 How pre-merge review works — the full lifecycle, short-circuits, and escape hatches.

@github-actions github-actions Bot added review:outstanding-issues Claude review completed; outstanding has author-actionable findings and removed review:in-progress Claude review is currently running labels Aug 24, 2026

@CamSoper CamSoper left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@workprentice please work through the findings on this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain:infra PR touches workflows, scripts, infra, Makefile, or build config review:outstanding-issues Claude review completed; outstanding has author-actionable findings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants