Skip to content

🍕 feat(seo): collapsible JSON-LD viewer + inline validator#8

Merged
jjpaulino merged 2 commits into
masterfrom
feat/seo-jsonld-collapsible
May 13, 2026
Merged

🍕 feat(seo): collapsible JSON-LD viewer + inline validator#8
jjpaulino merged 2 commits into
masterfrom
feat/seo-jsonld-collapsible

Conversation

@jjpaulino
Copy link
Copy Markdown
Member

@jjpaulino jjpaulino commented May 13, 2026

Why

The SEO tab's JSON-LD row used to be a one-liner that just said "2 block(s)". You had no way to see what was in the structured data, and no way to tell whether it was valid. This PR turns that line into a proper inspector and validator.

What's new

1. Collapsible JSON-LD viewer

A new "Structured data (JSON-LD)" section under the social previews with one card per <script type="application/ld+json"> block on the page.

  • Native <details> / <summary> for accessibility (keyboard, screen readers, browser find-in-page).
  • Collapsed header shows: block index, schema.org @type (or types joined with /), the most useful name field (`headline` → `name` → `title` → `url`), and a copy button.
  • Expanded body shows the full JSON pretty-printed with the same syntax highlighting as the JSON tab. The `
    ` is only rendered once the user opens the card so big `@graph` payloads don't run the highlighter regex if no one looks at them.
  • Smart `@graph` summaries: `@graph (Article, BreadcrumbList, WebSite) · 3 items` for Yoast/Rank-Math style blocks.
  • Invalid blocks get a red border, raw-text fallback view, and a clear "this isn't valid JSON" header.

2. Inline JSON-LD validator

A focused schema.org linter that runs over every block and surfaces issues directly inside each card. Goal: high signal for news/CMS pages, not a full schema.org validator. Catches the bugs that actually break Google rich results in the wild.

Checks

Category Code Severity What it flags
Universal `missing-context` error Block has no `@context`
`invalid-context` error Not a schema.org URL (e.g. `https://shema.org\` typo)
`missing-type` warn Entity has no `@type`
Article `article-missing-headline` error Required for Google rich results
`article-headline-long` warn > 110 chars (Google's documented limit)
`article-missing-image` error Required for Google rich results
`article-image-relative` warn Image URL must be absolute (handles string / ImageObject / array forms with precise sub-paths)
`article-missing-date-published` error Required
`article-bad-date-published` warn Not ISO 8601
`article-bad-date-modified` warn Not ISO 8601
`article-modified-before-published` warn Common copy-paste bug
`article-missing-author` warn Recommended
`article-missing-publisher` info Recommended
Breadcrumb `breadcrumb-empty` warn No `itemListElement`
`breadcrumb-missing-position` warn Per item
`breadcrumb-missing-name` warn Per item
`breadcrumb-missing-item` info Per item (final crumb may omit)
`breadcrumb-bad-positions` warn Positions aren't 1, 2, … N
Cross-block `duplicate-id` warn Same `@id` in 2+ blocks (Google may merge or drop entities)

The walker recurses into `@graph` (Yoast / Rank-Math style) and bare arrays so every inner entity gets its own checks. Each issue carries a precise dotted path like `@graph[1].headline` or `itemListElement[2].item`.

UI integration

  • Per-card severity badge in the collapsed summary: red `✕ N` for errors, yellow `! N` for warnings, blue `i N` for notes.
  • Card border turns red or yellow when issues are present.
  • Cards with errors auto-open on first render so the user sees problems without an extra click.
  • Issues list renders above the JSON pre when expanded, reusing the existing `.cs-seo-issue` tone classes from the page-level Issues section. Each row shows the message + the precise field path in monospace.
  • Section-level counters in the header: `3 blocks · ✕ 2 errors · ! 4 warnings`.

3. Refactor + bonus bug fix (carried over from earlier in this PR)

  • Extracted the JSON syntax-highlight regex from `JsonPreview.tsx` into a shared `src/lib/json-highlight.ts`.
  • Fixed a latent regex bug that was making `.cs-json-key` dead code — every key in the JSON tab was rendering with the string color instead of the accent color. Now keys correctly highlight in both tabs.

Test plan

  • `npm run validate` (typecheck + lint + format + 113 tests, +29 new across this PR)
  • `npm run build` clean
  • On a Clay page with multiple JSON-LD blocks: open SEO tab → confirm a card renders for each block → click a card → JSON expands with syntax highlighting → click copy → toast confirms
  • On a page with a JSON-LD block that's missing an Article `headline`: card auto-opens, summary shows red `✕` badge, expanded body lists the issue with path
  • On a page with a malformed JSON-LD block: card shows "Invalid JSON" with red border and raw script contents
  • On a page with no JSON-LD: "Page basics" still shows none, no JSON-LD section appears
  • Keyboard: Tab to a card summary → Enter toggles open/closed
  • JSON tab: keys (e.g. `"_ref":`) now render in the accent color, not green

Files

  • New: `src/lib/json-highlight.ts`, `tests/lib/json-highlight.test.ts`
  • Updated: `src/lib/seo.ts` (`summarizeJsonLd` + `lintJsonLd` + types), `src/content/panel/components/SeoTab.tsx`, `src/content/panel/components/JsonPreview.tsx`, `src/content/panel/styles.css`, `tests/lib/seo.test.ts` (+25 tests)

Commits

  • `f6a5f9a` 🍕 feat(seo): collapsible JSON-LD viewer in the SEO tab
  • `accaa51` 🍕 feat(seo): inline JSON-LD validator with per-block issues

jjpaulino and others added 2 commits May 13, 2026 12:12
The JSON-LD row used to be a one-liner that just said "N block(s)" — you
had no way to see what was actually in the structured data without
opening DevTools. This adds a proper viewer:

- A new "Structured data (JSON-LD)" section under the social previews,
  one card per <script type="application/ld+json"> block on the page.
- Each card uses a native <details>/<summary> for accessibility (works
  with keyboard, screen readers, and the browser's reveal-on-find).
- Collapsed header shows: block index, schema.org @type (or types
  joined with " / "), the most useful name field (headline / name /
  title / url), and a copy button.
- Expanded body shows the full JSON pretty-printed with the same
  syntax highlighting as the JSON tab. The <pre> is only rendered
  once the user opens the card so big @graph payloads don't run the
  highlighter regex if no one looks at them.
- Invalid/unparseable blocks (the ones the extractor flags with
  __invalid: true) get a red border + raw-text fallback view + a
  clear "this isn't valid JSON" header.
- New summarizeJsonLd() helper handles the common shapes:
  - bare @type: "Article" → "Article"
  - @type array: ["Article","NewsArticle"] → "Article / NewsArticle"
  - Yoast/Rank-Math @graph → "@graph (Article, BreadcrumbList, WebSite) · 3 items"
  - top-level array of entities → "Array (Person)"
  - missing @type → "Untyped object"
  - long names truncated with an ellipsis at 80 chars

Refactor:
- Extracted the syntax-highlight regex from JsonPreview.tsx into a new
  src/lib/json-highlight.ts module so the SEO tab and the JSON tab
  share one implementation. JsonPreview now imports from there.

Bug fix bundled with the extraction:
- The original highlightJson() had a dead-code branch for keys: it used
  a zero-width lookahead (?=\s*:) to detect them, then checked
  match.endsWith(':') in the callback — which never fired because the
  colon was outside the match. As a result, the .cs-json-key CSS class
  defined in styles.css was never actually applied; every key rendered
  with the same green as string values. Captured the colon inside the
  match so keys now correctly pick up the accent color in both the
  JSON tab and the new JSON-LD viewer.

Tests: 11 new (7 for summarizeJsonLd covering all the shapes above
+ 4 for highlightJson covering the key-detection fix and XSS escape).
Full suite: 95/95.

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a focused schema.org validator that runs over every JSON-LD block
on the page and surfaces issues directly inside the new collapsible
cards. The goal is "high signal for news/CMS pages" — not a complete
schema.org validator, but it catches the bugs that actually break
Google rich results in the wild.

What's checked

  Universal
  - missing-context              error: block has no @context
  - invalid-context              error: @context isn't a schema.org URL
                                 (catches typos like https://shema.org)
  - missing-type                 warn:  entity has no @type

  Article / NewsArticle / BlogPosting / etc.
  - article-missing-headline     error: required for Google rich results
  - article-headline-long        warn:  > 110 chars (Google's limit)
  - article-missing-image        error: required for Google rich results
  - article-image-relative       warn:  image URL must be absolute (works
                                 across string, ImageObject, and array
                                 forms; reports the precise sub-path)
  - article-missing-date-published   error
  - article-bad-date-published       warn: not ISO 8601
  - article-bad-date-modified        warn: not ISO 8601
  - article-modified-before-published warn: copy-paste bug
  - article-missing-author       warn:  recommended
  - article-missing-publisher    info:  recommended

  BreadcrumbList
  - breadcrumb-empty             warn:  no itemListElement
  - breadcrumb-missing-position  warn:  per item
  - breadcrumb-missing-name      warn:  per item
  - breadcrumb-missing-item      info:  per item (final crumb may omit)
  - breadcrumb-bad-positions     warn:  positions aren't 1, 2, … N

  Cross-block
  - duplicate-id                 warn:  same @id appears in 2+ blocks
                                 (Google may merge or drop entities)

The walker recurses into @graph (Yoast / Rank-Math style) and bare
arrays so every inner entity gets its own checks, with paths like
"@graph[1].headline" or "itemListElement[2]" attached to each issue.

UI

- Per-card severity badge in the collapsed summary (red ✕ N for
  errors, yellow ! N for warnings, blue i N for notes).
- Card border picks up red/yellow when issues are present.
- Cards with errors auto-open on first render so problems are visible
  without an extra click.
- Issues list rendered above the JSON pre when expanded, reusing the
  existing .cs-seo-issue tone classes for visual consistency. Each
  row shows the message + the precise dotted path to the offending
  field in monospace.
- Section header gains aggregate counters: "3 blocks · ✕ 2 errors · ! 4 warnings".

Tests: 18 new (every check above + @graph recursion + cross-block @id
dedup behavior + skip-invalid-blocks). Full suite: 113/113.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jjpaulino jjpaulino changed the title 🍕 feat(seo): collapsible JSON-LD viewer in the SEO tab 🍕 feat(seo): collapsible JSON-LD viewer + inline validator May 13, 2026
@jjpaulino jjpaulino self-assigned this May 13, 2026
@jjpaulino jjpaulino merged commit cd3c75e into master May 13, 2026
1 check passed
@jjpaulino jjpaulino deleted the feat/seo-jsonld-collapsible branch May 13, 2026 16:54
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