Skip to content

fix(blog): emit valid schema.org JSON-LD in blog SEO sections#1643

Merged
aka-sacci-ccr merged 5 commits into
mainfrom
fix-blog-seo-jsonld
Jul 16, 2026
Merged

fix(blog): emit valid schema.org JSON-LD in blog SEO sections#1643
aka-sacci-ccr merged 5 commits into
mainfrom
fix-blog-seo-jsonld

Conversation

@aka-sacci-ccr

@aka-sacci-ccr aka-sacci-ccr commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Problem

blog/sections/Seo/SeoBlogPost.tsx and blog/sections/Seo/SeoBlogPostListing.tsx were serializing the raw page object straight into the <script type="application/ld+json"> tag:

  • SeoBlogPost emitted {"@type": "BlogPostPage", post: {...}, seo: {...}}BlogPostPage is not a schema.org type and none of the nested fields (title, excerpt, slug, ...) are recognized properties, so Google ignored the markup entirely.
  • SeoBlogPostListing was worse: no @type at all, plus a { ...jsonLD?.posts } spread of an array into an object, producing garbage like {"0": {...}, "1": {...}}.

Fix

Following Google's Article structured data guidelines:

  • New blog/utils/jsonLD.ts with a toBlogPosting helper mapping a BlogPost to a BlogPosting node: headline, description, image, datePublished, dateModified, author (as Person with jobTitle/worksFor when available), publisher, articleSection, timeRequired (ISO 8601 duration), url/mainEntityOfPage, and aggregateRating/interactionStatistic when present. Post content (raw HTML) is excluded by construction, so the old strip-content workaround is gone.
  • SeoBlogPost now emits a BlogPosting plus a BreadcrumbList, using the canonical URL (falling back to the request URL) as mainEntityOfPage.
  • SeoBlogPostListing now emits a Blog node with name, description, url, publisher and a blogPost array of BlogPosting items, plus a BreadcrumbList.

The top-level "@context": "https://schema.org" is added by website/components/Seo.tsx at serialization time, as before.

Validity guards

AggregateRating requires ratingValue + ratingCount/reviewCount, and InteractionCounter requires userInteractionCount. Posts carrying empty {"@type": ...} stubs would fail the Rich Results Test, so these keys are only emitted when the required data is present.

New app/type fields

  • BlogPost.dateModified (optional, @format date) → emitted as dateModified to signal content freshness.
  • Blog app state publisher ({ name, logo?, url? }) → emitted as an Organization node on BlogPosting (post page) and Blog (listing).
  • Blog app state canonicalBaseUrl → overrides the origin of url/mainEntityOfPage, which are otherwise built from the request host (useful behind proxies/multiple domains).

BreadcrumbList

Derived from the canonical pathname. Intermediate segments resolve real category names when known (falling back to humanized slugs) and always carry absolute item URLs; the last item uses the real page name (post title / category name) and omits item, as allowed by Google:

{
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Inspira", "item": "https://loja.example.com.br/inspira" },
    { "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://loja.example.com.br/inspira/blog" },
    { "@type": "ListItem", "position": 3, "name": "Dicas", "item": "https://loja.example.com.br/inspira/blog/dicas" },
    { "@type": "ListItem", "position": 4, "name": "Mitos e verdades sobre o uso do micro-ondas no dia a dia" }
  ]
}

Sample BlogPosting output for a post page:

{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Mitos e verdades sobre o uso do micro-ondas no dia a dia",
  "description": "Resumo",
  "image": ["https://cdn.site.com/img.png"],
  "datePublished": "2026-06-01",
  "dateModified": "2026-07-10",
  "author": [{ "@type": "Person", "name": "Ana" }],
  "publisher": { "@type": "Organization", "name": "Acme", "url": "https://loja.example.com.br", "logo": { "@type": "ImageObject", "url": "https://cdn.site.com/logo.png" } },
  "articleSection": ["Dicas"],
  "url": "https://loja.example.com.br/inspira/blog/dicas/mitos-e-verdades",
  "mainEntityOfPage": { "@type": "WebPage", "@id": "https://loja.example.com.br/inspira/blog/dicas/mitos-e-verdades" }
}

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved blog post and listing SEO metadata with richer structured data, including authors, publishers, categories, dates, ratings, interactions, and breadcrumbs.
    • Added support for canonical URL configuration and publisher details.
    • Added author type and post modification date metadata.
  • Bug Fixes

    • Prevented unsafe characters in structured data from breaking or escaping SEO scripts.
    • Improved handling of incomplete rating and interaction data to maintain valid rich results.

…stListing

Both sections were serializing the raw page object into the ld+json
script: SeoBlogPost emitted a non-schema.org "BlogPostPage" node and
SeoBlogPostListing spread the posts array into an object, producing
invalid markup that Google ignores.

Add blog/utils/jsonLD.ts with a toBlogPosting helper following
https://developers.google.com/search/docs/appearance/structured-data/article
and emit a BlogPosting node on post pages and a Blog node with blogPost
items on listing pages.

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

Copy link
Copy Markdown
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.159.6 update
  • 🎉 for Minor 0.160.0 update
  • 🚀 for Major 1.0.0 update

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@aka-sacci-ccr, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ac14fdb-f8a2-4a2e-877b-4d05a413f9ae

📥 Commits

Reviewing files that changed from the base of the PR and between 495ff49 and 5cd9425.

📒 Files selected for processing (3)
  • blog/sections/Seo/SeoBlogPost.tsx
  • blog/sections/Seo/SeoBlogPostListing.tsx
  • blog/utils/jsonLD.ts
📝 Walkthrough

Walkthrough

Blog detail and listing SEO loaders now use shared schema utilities to generate BlogPosting, Blog, and breadcrumb JSON-LD. Canonical URLs can use request fallbacks or configured bases, publisher metadata is supported, and serialized JSON-LD is escaped before script embedding.

Changes

Blog JSON-LD generation

Layer / File(s) Summary
Schema contracts and mapping
blog/types.ts, blog/utils/jsonLD.ts
Adds author types, modification dates, publisher metadata, canonical URL handling, BlogPosting mapping, validation guards, and breadcrumb generation.
Blog state and loader integration
blog/mod.ts, blog/sections/Seo/SeoBlogPost.tsx, blog/sections/Seo/SeoBlogPostListing.tsx
Adds canonical and publisher state, maps detail posts into BlogPosting JSON-LD, and builds listing-page Blog JSON-LD with mapped posts and breadcrumbs.
JSON-LD script rendering
website/components/Seo.tsx
Escapes <, \u2028, and \u2029 after JSON serialization before embedding JSON-LD scripts.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant BlogLoader
  participant JSONLDUtilities
  participant Seo
  Request->>BlogLoader: provide request URL
  BlogLoader->>JSONLDUtilities: map posts and build breadcrumbs
  JSONLDUtilities-->>BlogLoader: return JSON-LD nodes
  BlogLoader->>Seo: pass jsonLDs
  Seo-->>Request: embed escaped JSON-LD scripts
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change, but it does not follow the required template and omits the issue, Loom, and demonstration sections. Rewrite it using the required headings: What is this Contribution About?, Issue Link, Loom Video, and Demonstration Link, and fill each section in.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: fixing blog SEO JSON-LD schema output.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-blog-seo-jsonld

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread blog/utils/jsonLD.ts Outdated
Comment thread blog/utils/jsonLD.ts Outdated
…Url in SEO JSON-LD

- Drop empty AggregateRating/InteractionCounter stubs: only emit
  aggregateRating when ratingValue + ratingCount/reviewCount are set and
  interactionStatistic when userInteractionCount is set, since bare
  {"@type": ...} objects fail the Rich Results Test.
- Emit publisher (Organization with optional logo/url) on BlogPosting and
  Blog nodes, configured app-wide via the new publisher state in mod.ts.
- Add optional dateModified to BlogPost and emit it when present.
- Add canonicalBaseUrl app state that overrides the origin of
  url/mainEntityOfPage, otherwise built from the request host.
- Emit a BreadcrumbList on post and listing pages, derived from the
  canonical pathname: intermediate items resolve real category names
  (falling back to humanized slugs) with absolute item URLs; the last
  item carries the real page name and omits item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread blog/utils/jsonLD.ts
Comment thread blog/sections/Seo/SeoBlogPostListing.tsx
Comment thread blog/sections/Seo/SeoBlogPostListing.tsx Outdated
Comment thread blog/sections/Seo/SeoBlogPost.tsx Outdated
decobot and others added 2 commits July 16, 2026 15:59
Google's Article guidelines accept Person or Organization as author.
Add an optional type field to Author (defaults to Person) and emit it as
the author @type; jobTitle/worksFor are Person-only properties and are
skipped for organizations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…JSON-LD serialization

- Apply canonicalBaseUrl to the canonical link as well, so <link
  rel=canonical> and the JSON-LD url/mainEntityOfPage always point at
  the same host.
- Escape "<" (plus U+2028/U+2029) when serializing jsonLDs in the Seo
  component: values containing "</script>" could break out of the
  ld+json script tag via dangerouslySetInnerHTML. The unicode escapes
  parse back to the same JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@blog/utils/jsonLD.ts`:
- Around line 27-34: Update withCanonicalBase to parse url against a dummy base
so relative paths do not throw, then replace only the origin using
canonicalBaseUrl while preserving the full path, query parameters, and hash.
Keep the existing behavior of returning url when canonicalBaseUrl is absent.
- Around line 89-122: Update toBreadcrumbList so relative url values are parsed
with a valid fallback base instead of allowing new URL(url) to throw. Ensure
generated non-final item links use the resolved absolute origin, while
preserving existing pathname, category-name, and final-item behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f170949-d3ba-4168-84b1-3c1d4a300e2f

📥 Commits

Reviewing files that changed from the base of the PR and between f8b62ed and 495ff49.

📒 Files selected for processing (6)
  • blog/mod.ts
  • blog/sections/Seo/SeoBlogPost.tsx
  • blog/sections/Seo/SeoBlogPostListing.tsx
  • blog/types.ts
  • blog/utils/jsonLD.ts
  • website/components/Seo.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • blog/sections/Seo/SeoBlogPostListing.tsx

Comment thread blog/utils/jsonLD.ts Outdated
Comment thread blog/utils/jsonLD.ts
…lative canonical URLs

- Only emit timeRequired for finite positive readTime values; negative
  or Infinity produced invalid ISO 8601 durations (PT-5M, PTInfinityM).
- Resolve configured canonicals against the request URL in both SEO
  sections: a relative canonical previously reached new URL() in
  toBreadcrumbList and threw, breaking the page render. It also makes
  the emitted canonical link absolute, as Google requires.
- withCanonicalBase now resolves relative URLs against the canonical
  base and preserves query/hash instead of dropping them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aka-sacci-ccr
aka-sacci-ccr merged commit 8b14f3f into main Jul 16, 2026
3 checks passed
@aka-sacci-ccr
aka-sacci-ccr deleted the fix-blog-seo-jsonld branch July 16, 2026 19:38
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.

1 participant