Skip to content

fix(schema): mirror title_tag instead of sidebar .Title in JSON-LD names - #20630

Merged
CamSoper merged 2 commits into
masterfrom
seo/webpage-name-title-tag
Aug 3, 2026
Merged

fix(schema): mirror title_tag instead of sidebar .Title in JSON-LD names#20630
CamSoper merged 2 commits into
masterfrom
seo/webpage-name-title-tag

Conversation

@workprentice

@workprentice workprentice Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Technical SEO fix: JSON-LD schema fields that are supposed to carry the real page title (WebPage.name, HowTo.name, CollectionPage.name, tutorial Course.name/ItemList.name) were built from Hugo's .Title, which is the short left-nav sidebar label, not the actual page title. layouts/partials/head.html (lines 279–281) already sets the real <title> tag from .Params.title_tag when present, falling back to .Title — this PR mirrors that same precedence in the schema graph so structured data matches what the browser tab and search snippet actually show.

This is the third and final instance of a recurring bug class this week: SoftwareApplication.name (PR #20535/#20552 line of work) and TechArticle.headline (PR #20580) both had the identical title_tag vs .Title confusion. This PR closes the class by sweeping every schema collector for the same leak.

Why it matters

  • 668 content files declare title_tag:. Before this fix, every one of those pages emitted the sidebar label (not the real title) into WebPage.name.
  • Those 668 pages collapsed to only 584 distinct WebPage.name values — 133 pages carried a name duplicated at least once elsewhere on the site. Worst offenders: "Azure" ×5, "AWS" ×5, "Operations" ×4, "Next steps" ×4, "Install Pulumi" ×4, "Google Cloud" ×4, "Terraform" ×3, "Kubernetes" ×3.
  • After this fix: 667 of 668 distinct — ambiguity essentially eliminated.
  • Concrete harm this fixes:
    • content/docs/iac/comparisons/cloudformation.md — a Pulumi comparison page was graph-named after the competitor ("AWS CloudFormation" instead of "Pulumi vs. AWS CloudFormation").
    • content/docs/iac/comparisons/_index.md and content/docs/esc/vs/_index.md both emitted the identical WebPage.name "Comparisons" — two different hub pages, indistinguishable in the graph.
    • content/tutorials/glossary/aws-eks.md — "Elastic Kubernetes Service" instead of "What is AWS Elastic Kubernetes Service (EKS)?" — glossary pages are prime AEO assets and were losing the question framing LLMs pattern-match on.
    • Four identically-titled create-project.md pages across aws/azure/gcp/kubernetes get-started tracks were indistinguishable by name.

Changes

All follow the existing (or .Params.title_tag .Title) guard pattern already used in this codebase (article-entity.html, faq-entity.html, breadcrumb-entity.html, itemlist-entity.html):

  • layouts/partials/schema/graph-builder.htmlWebPage.name (the primary, highest blast-radius fix — every one of the 668 pages goes through this).
  • layouts/partials/schema/collectors/howto-entity.htmlHowTo.name, plus the description fallback that referenced the unguarded title.
  • layouts/partials/schema/collectors/collection-entity.htmlCollectionPage.name, plus the child ListItem.name in its embedded ItemList (hub pages listing other pages).
  • layouts/partials/schema/content/course-list.html — tutorial Course.name (per listed tutorial) and the tutorials-hub ItemList.name, plus the description fallback. Found during the sweep; not in the original three known offenders but the identical bug.

Deliberately left unchanged

  • collectors/blog-entity.html BlogPosting.headline (.Title) — only 4 blog posts have title_tag, and in all 4, .Title is the genuine article headline/h1 while title_tag is a plain-text SEO variant (e.g. "Pulumi 💜's Open Source" vs "Pulumi Loves Open Source"). headline should mirror the h1, not the <title> tag — divergence there is correct semantics, not a bug.
  • collectors/video-entity.html — zero video pages currently declare title_tag, so there's no live exposure; left alone to avoid pointless churn.
  • collectors/product-entity.html $productName := .Titleintentionally not touched. Open PR Add real Offer/AggregateOffer pricing schema to /pricing/ #20552 (seo/pricing-offer-schema) already modifies this exact file; editing it here would create a merge conflict. Follow-up in a future PR once Add real Offer/AggregateOffer pricing schema to /pricing/ #20552 lands.
  • collectors/main-entity.html — uses .Title only for FAQ page detection (string match against the title), never emits it into schema. No action needed.
  • utils/content-aggregator.html — uses .Title only as content-analysis input for word-count estimation, never emitted as a schema name. No action needed.
  • Already guarded prior to this PR (verified, no action): article-entity.html, faq-entity.html, breadcrumb-entity.html, itemlist-entity.html, event-entity.html.

Verification

  • Hugo template delimiter balance ({{ vs }}) checked on every edited file against the pre-edit baseline — each edit added exactly one balanced pair, no pre-existing imbalance introduced by this change (one file, howto-entity.html, has a pre-existing 1-delimiter mismatch unrelated to this edit, present before and after at the same offset).
  • CI's Hugo build is authoritative for full-site rendering; a local hugo --renderToMemory run fails on an unrelated pre-existing environment gap (missing generated OpenAPI spec / CSS asset pipeline), not a regression from this change.

This PR was created by workprentice on behalf of the Pulumi SEO/AEO content team — no get_me-equivalent tool was available in this session to resolve a specific requester's username.

…names

WebPage.name, HowTo.name, CollectionPage.name/ItemList children, and the
tutorials Course/ItemList names were all built from Hugo's .Title, which
is the SHORT left-nav sidebar label, not the real page title. head.html
already builds the actual <title> tag from title_tag when present
(falling back to .Title) — this brings the schema graph in line with
that same precedence so the structured data matches what the browser
tab and search snippet actually show.

668 content files declare title_tag. Before this fix those pages
collapsed to only 584 distinct WebPage.name values (133 pages emitting
a name duplicated elsewhere); after, 667 of 668 are distinct.

Changed:
- layouts/partials/schema/graph-builder.html: WebPage.name
- layouts/partials/schema/collectors/howto-entity.html: HowTo.name and
  its description fallback
- layouts/partials/schema/collectors/collection-entity.html:
  CollectionPage.name and the child ListItem.name in its ItemList
- layouts/partials/schema/content/course-list.html: Course.name (per
  tutorial listed) and the tutorials-hub ItemList.name

Deliberately left unchanged (documented in PR body): blog-entity.html
headline, video-entity.html (zero exposure), product-entity.html
(conflicts with open PR #20552).
@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 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Pre-merge Review — Last updated 2026-08-03T22:54:35Z

Tip

Summary: This is a Hugo template change, not content: four JSON-LD schema partials now build their name fields from (or .Params.title_tag .Title) instead of the bare .Title, so structured data carries the real page title rather than the short left-nav sidebar label. The precedence matches what layouts/partials/head.html already uses for the <title> tag, and the guard pattern matches the partials that were already doing this (article-entity.html, faq-entity.html, breadcrumb-entity.html, itemlist-entity.html). The wrongness that would matter here is a partial that fails to render, or one that changes a user-visible string for the worse; I read each edited partial in full at the head commit plus head.html's title block and found no rendering hazard — only two places where the new value flows into a printf-built fallback sentence and one code comment that overstates the mirroring. No prose, fenced code, or frontmatter changed, so the style, claim-verification, and editorial-balance passes had nothing to examine.

Review confidence:

Dimension Level Notes
mechanics MEDIUM The pre-review Hugo build was skipped as "content-only PR; templating paths untouched" — the opposite of what this PR is — so nothing here was render-verified. The build-and-deploy.yml Hugo build is the authoritative check.
facts HIGH
templates MEDIUM Reviewed by reading each edited partial and head.html at the head commit; no rendered-output diff was available.
Investigation log
  • Cross-sibling reads: not run (not in a templated section)
  • External claim verification: not run (no claims in this diff)
  • Cited-claim spot-checks: not run (no cited claims)
  • Frontmatter sweep: not run (no frontmatter in diff)
  • Temporal-trigger sweep: not run (no trigger words)
  • 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
0 0 0 3

🔍 Verification trail

No prose claims were extracted from this diff (it changes Hugo templates only). These entries record the template checks that produced the findings below:

  • L21 in layouts/partials/schema/collectors/howto-entity.html "description fallback sentence still reads grammatically with the new name value" → 🤷 unverifiable (fallback only fires when a page has neither meta_desc nor a Hugo .Summary; whether any live tutorial hits that branch can't be determined without a rendered site) — fixed in 318d695
  • L18 in layouts/partials/schema/content/course-list.html "course description fallback sentence still reads grammatically with the new name value" → 🤷 unverifiable (same fallback conditions; not determinable from the diff) — fixed in 318d695
  • L5 in layouts/partials/schema/graph-builder.html "new comment's description of head.html's <title> logic" → 🌀 framing-drift (layouts/partials/head.html lines 249–327: $title is overridden for blog term/category/series pages before the title_tag check, and every <title> gets a | Pulumi … suffix appended) — fixed in 318d695

🚨 Outstanding in this PR

No outstanding findings in this PR.

⚠️ Low-confidence

No low-confidence findings in this PR.

💡 Pre-existing issues in touched files (optional)

No pre-existing issues in touched files.

✅ Resolved since last review

  • [L21] layouts/partials/schema/collectors/howto-entity.html — the description fallback now interpolates .Title instead of $pageName, so a sentence-style title_tag no longer produces a broken fallback sentence (resolved in 318d695).

  • [L18] layouts/partials/schema/content/course-list.html — same fix applied to Course.description: the fallback now interpolates $course.Title instead of $courseName (resolved in 318d695).

  • [L5] layouts/partials/schema/graph-builder.html — the code comment was narrowed to note the <title> precedence excludes head.html's blog term/category/series overrides and its " | Pulumi …" suffix (resolved in 318d695).

📜 Review history

  • 2026-08-01T12:11:24Z — Reviewed the four schema partials: the title_tag-then-.Title precedence is correct and consistent with head.html; flagged two printf description fallbacks that now interpolate a sentence-style title and one over-broad code comment. No blockers. (bc110f5)
  • 2026-08-03T22:54:35Z — Re-reviewed after fix push (1 new commit, 318d695): all three previously-low-confidence findings applied verbatim as suggested; moved to ✅ Resolved. No new findings introduced by the fix commit. (318d695)

  • 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:no-blockers Claude review completed cleanly; outstanding is empty and removed review:in-progress Claude review is currently running labels Aug 1, 2026
@pulumi-bot

pulumi-bot commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

@pulumi-bot

pulumi-bot commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Lighthouse Performance Report

Commit: 318d695 | Metric definitions

Page Device Score FCP LCP TBT CLS SI
Homepage Mobile 🟡 68 3.0s 3.9s 534ms 0.072 3.6s
Homepage Desktop 🟡 84 0.8s 1.1s 206ms 0.037 2.7s
Install Pulumi Mobile 🟡 59 6.1s 9.4s 62ms 0.053 6.1s
Install Pulumi Desktop 🟡 79 1.3s 2.1s 0ms 0.006 2.9s
AWS Get Started Mobile 🟡 56 5.9s 8.6s 10ms 0.091 7.9s
AWS Get Started Desktop 🟡 81 1.3s 1.9s 0ms 0.053 2.8s

@CamSoper

CamSoper commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@claude Implement the suggested low-confidence findings and #update-review

@github-actions github-actions Bot added review:in-progress Claude review is currently running and removed review:no-blockers Claude review completed cleanly; outstanding is empty labels Aug 3, 2026
…ow comment

Addresses low-confidence review findings: the meta_desc/description
printf fallbacks in howto-entity.html and course-list.html now
interpolate .Title (the short page title) rather than the
title_tag-preferring name, avoiding broken sentences when title_tag is
phrased as a full sentence or question. Also narrows graph-builder.html's
new comment, since head.html's title precedence isn't quite what it
described (blog term/category/series overrides, and a site-name suffix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Review updated on @CamSoper's request.

@github-actions github-actions Bot added review:no-blockers Claude review completed cleanly; outstanding is empty and removed review:in-progress Claude review is currently running labels Aug 3, 2026
@CamSoper
CamSoper enabled auto-merge (squash) August 3, 2026 23:05
@CamSoper
CamSoper merged commit 474abaf into master Aug 3, 2026
13 checks passed
@CamSoper
CamSoper deleted the seo/webpage-name-title-tag branch August 3, 2026 23:10
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:no-blockers Claude review completed cleanly; outstanding is empty

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants