Skip to content

Add required thumbnailUrl and TechArticle headline to schema - #123

Merged
vpetersson merged 3 commits into
sbomify:masterfrom
vpetersson-bot:seo-schema-required-fields
Aug 4, 2026
Merged

Add required thumbnailUrl and TechArticle headline to schema#123
vpetersson merged 3 commits into
sbomify:masterfrom
vpetersson-bot:seo-schema-required-fields

Conversation

@vpetersson-bot

Copy link
Copy Markdown
Contributor

Follow-up to #122. Rather than reading the Semrush columns at face value, I parsed the production build and checked every structured-data entity against the properties Google requires per rich-result type. Two gaps came out of it.

1. VideoObject.thumbnailUrl missing on 22 entities

thumbnailUrl is required — without it Google drops the entity entirely. video-embed-native.html only emitted it when a poster param was passed, and no call site passes one (0 of 22).

Now falls back to the page's social image, the same source head.html already uses for og:image. video-embed.html (YouTube) already derives a thumbnail from the video ID and is unchanged.

2. TechArticle missing headline on 19 guides

The guides block emitted name but not headline, which is the property Google actually reads for Article-family types. Added from the page title.

The date fields are guarded on .Lastmod.IsZero. Guides carry no date in frontmatter, and .GitInfo does not resolve in this repo despite enableGitInfo = true — I probed both and confirmed .Lastmod is the zero value and .GitInfo is nil. So nothing is emitted today; a zero date is worse than no date. The guard activates automatically if a guide ever gains one.

Verification

Production build (hugo --minify), all 479 pages parsed:

Result
Invalid JSON-LD blocks 0
Entities missing a required property 0

Covering Organization (339), BreadcrumbList (172), BlogPosting (99), TechArticle (64), FAQPage (62), VideoObject (32), Article (2), Product (1). bun run lint passes.

Known remaining gap — not fixed here

29 of 32 VideoObject entities emit uploadDate: "0001-01-01T00:00:00Z". Their pages (FAQ, compliance, /resources/) carry no date, so .Page.Date falls through to the zero value. Only the 3 on dated blog posts are correct.

uploadDate is also required, so those 29 entities remain invalid even with the thumbnail fix. I did not paper over it: emitting a synthesised date would be inaccurate structured data, which is its own problem. The real fix is either passing upload_date at the call sites or giving those pages a real date — both need actual values, not invented ones.

Deliberately not changed

  • Logo items = 0 in the report — the Organization schema already has a valid logo ImageObject. Semrush quirk.
  • Product snippet items = 1–2 — the build contains exactly one Product entity, on the homepage, which is correct.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FbqYj6ztzG6yautDpRWDXS

vpetersson-bot and others added 2 commits August 4, 2026 10:21
Two gaps found by parsing the production build for the properties
Google requires per rich-result type.

VideoObject.thumbnailUrl is required, and video-embed-native only
emitted it when a "poster" param was passed. No call site passes one,
so all 22 native-video entities omitted it. Falls back to the page's
social image -- the same source head.html already uses for og:image --
rather than emitting an entity Google will drop. video-embed (YouTube)
already derived a thumbnail from the video ID and is unchanged.

The guides TechArticle block emitted "name" but no "headline", which
is the property Google actually reads for Article-family types. Adds
it from the page title across 19 guides.

The date fields are guarded on .Lastmod.IsZero. Guides carry no date
in frontmatter and .GitInfo does not resolve in this repo despite
enableGitInfo = true, so nothing is emitted today -- a zero date is
worse than no date. The guard activates automatically if a guide ever
gains a date.

Verified against a production build (hugo --minify), all 479 pages:
no invalid JSON-LD, and no entity missing a required property for
Organization, BreadcrumbList, BlogPosting, TechArticle, FAQPage,
Article or Product.

Known remaining gap, not addressed here: 29 of 32 VideoObject entities
emit uploadDate "0001-01-01T00:00:00Z" because their pages (FAQ,
compliance, resources) carry no date. uploadDate is also required, so
those entities stay invalid until real dates are supplied. Fixing that
means either passing upload_date at the call sites or giving those
pages a date -- both need real values rather than a synthesised one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbqYj6ztzG6yautDpRWDXS
uploadDate is required for VideoObject, and 29 of 32 entities emitted
"0001-01-01T00:00:00Z" because their pages (FAQ, compliance,
resources) carry no frontmatter date.

Asset mtime is not a usable source. The Last-Modified on
marketing-assets.sbomify.com is the "Record Screencasts" workflow run
time -- run 30824548590 finished at 2026-08-03T15:10:39Z and all 18
screencasts report last-modified 15:10:11-12 the same day. Every
dispatch re-stamps all of them at once, so uploadDate would churn on
each run and tell Google each video was published the moment CI last
ran. HTTP has no creation-date header, and S3/R2 objects store only a
last-modified time that an overwrite resets, so there is nothing else
to read at build time.

Dates come from stable sources instead:

  - sbomify screencasts (18): first-commit date of the generator in
    sbomify/sbomify screencasts/*.py -- the video cannot predate its
    generator, and these survive re-recording
  - YouTube (10): the real uploadDate from each video's watch page
  - FOSDEM talk (4): Last-Modified from the FOSDEM mirror
    (2026-02-03), an archived asset that is not regenerated

The two independent sources cross-validate: generator dates and the
content files' own first-commit dates agree to the day for
document_signatures, cra_compliance, vex_upload, plugin_enablement
and workspace_deletion. Generator dates win where a content file
predates the Jekyll migration and only carries the 2026-02-17
migration floor (e.g. compliance/fda-medical-device).

Verified against a production build, all 479 pages: 0 invalid JSON-LD,
0 entities missing a required property, 0 VideoObject with a zero
uploadDate, and 14 distinct dates spanning 2024-04 to 2026-05.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbqYj6ztzG6yautDpRWDXS
@vpetersson-bot

Copy link
Copy Markdown
Contributor Author

Pushed e99b419 — the remaining uploadDate gap is now closed, so this PR makes all 32 VideoObjects valid, not just the thumbnails.

Why asset mtime couldn't be the source

I tested reading Last-Modified at build time. It doesn't work, and the workflow run confirms why: Record Screencasts run 30824548590 finished at 2026-08-03T15:10:39Z, and all 18 screencasts report last-modified of 15:10:11–12 that same day — within one second of each other.

Every dispatch re-stamps all of them at once. Using that as uploadDate would tell Google each video was published the moment CI last ran, and it would change on every re-record.

There's also no alternative header to read: HTTP has no creation-date field, and S3/R2 objects store only a last-modified time that an overwrite resets. Nothing else is exposed.

Sources used instead

Source Count Why it's stable
Generator first-commit date (sbomify/sbomify screencasts/*.py) 18 A video cannot predate its generator; survives re-recording
YouTube watch-page uploadDate 10 The real publish date
FOSDEM mirror Last-Modified (2026-02-03) 4 Archived talk, not regenerated

The generator dates cross-validate against the content files' own first-commit dates, agreeing to the day for document_signatures, cra_compliance, vex_upload, plugin_enablement and workspace_deletion. Generator dates win where a content file predates the Jekyll migration and only carries the 2026-02-17 migration floor — compliance/fda-medical-device is the clear case (migration floor 2026-02-17 vs. true generator date 2026-04-24).

Result

Production build, all 479 pages:

  • 0 invalid JSON-LD
  • 0 entities missing a required property
  • 0 VideoObject with a zero uploadDate (was 29)
  • 14 distinct dates spanning 2024-04 → 2026-05

One judgement call worth flagging: the six plugin_enablement_*.webm assets all come from a single plugin_enablement.py (2026-04-24), so they share that date. For plugin_enablement_osv specifically, the FAQ page referencing it dates to 2026-03-02, suggesting an earlier differently-named asset that was later folded into the shared generator. 2026-04-24 is the defensible date for the current asset, but say the word if you'd rather it read 2026-03-02.

Diffing the "Record Screencasts" run (30824548590) against what the
site embeds turned up a mismatch in both directions: 21 assets
produced, 18 referenced, and one reference the run does not produce.

Removes the stale GitHub Attestation embed. That plugin was folded
into sbom-verification -- PLUGIN_SLUGS in screencasts/plugin_enablement.py
no longer lists it, and the comment there records the merge ("formerly
two separate plugins"). The asset only still resolves because the R2
sync runs without --delete: plugin_enablement_github-attestation.webm
reports last-modified 2026-04-24, while every current screencast
reports 2026-08-03. The page was showing a video of a plugin that no
longer exists, and describing it as current, directly above the video
that replaced it. Merges the two paragraphs into one describing the
unified plugin, keeping the announcement post as a historical link.

Adds two screencasts that were being recorded on every run and never
used:

  - oidc_trusted_publishing.webm -> how-do-i-set-up-oidc-trusted-publishing,
    which had no video at all despite the recording existing since May
  - vex_lifecycle.webm -> how-do-i-use-vex, as the fuller end-to-end
    journey (suppress, publish, and v2 superseding v1) ahead of the
    existing vex_upload clip, which only shows the artifact rendering

upload_date on both comes from the generator's first-commit date, the
same stable source used for the rest of the embeds.

Verified against a production build: 0 invalid JSON-LD, 0 entities
missing a required property, 0 VideoObject with a zero uploadDate, and
no referenced asset that CI does not produce.

Two of the 21 remain unused, deliberately:

  - vulnerability_scanning.webm is superseded. Its docstring describes
    "Plugins page -> enable OSV -> save", which is exactly what the
    parametrized plugin_enablement_osv.webm now shows, and that one is
    already embedded on the vulnerability-scanning FAQ.
  - security_advisories.webm has no home. Its docstring references an
    FAQ describing the advisory lifecycle, but no such page exists on
    the site yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbqYj6ztzG6yautDpRWDXS
@vpetersson
vpetersson merged commit a6a2d91 into sbomify:master Aug 4, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants