Skip to content

fix(core): reduce JSON-LD rule false positives (SEO018/019/021) + rendered entity handling - #57

Merged
oekazuma merged 2 commits into
mainfrom
fix/seo-jsonld-false-positives
Jun 26, 2026
Merged

oekazuma merged 2 commits into
mainfrom
fix/seo-jsonld-false-positives

Conversation

@oekazuma

@oekazuma oekazuma commented Jun 26, 2026 •

Copy link
Copy Markdown
Owner

Follow-up to #55. These refinements were reviewed and committed locally but never pushed before #55 merged, so they're re-submitted here on their own branch.

Why

The JSON-LD rules from #55 produced false positives on valid, idiomatic structured data, and rendered-mode capture could corrupt JSON before validation.

Changes

Rule / area Problem Fix
SEO018 @id (a node identifier, often a relative fragment like #organization) flagged as a relative URL; data:/mailto:/urn:/protocol-relative values also flagged Drop @id from URL_KEYS; broaden isAbsoluteUrl to accept any URI scheme or // protocol-relative — only scheme-less paths flag
SEO019 schema.org reduced-precision dates (2026, 2026-06) wrongly flagged isIso8601 accepts year / year-month precision (calendar + range validation kept)
SEO021 "headline": "" / null / [] counted as present (false negative) New hasNonEmpty helper treats empty/blank/null/empty-array required values as missing
Rendered capture script.text HTML-entity-decodes (&quot; → "), corrupting JSON before SEO016 parses it Read via script.rawText — <script> is a raw-text element crawlers don't decode

Docs (en/ja for SEO018, SEO019) updated to match. Tests added for each case (core +10, vite +1).

Validation

pnpm -r test (368: core 165 / vite 58 / cli 136 / mcp 9), pnpm -r typecheck, pnpm lint — all green.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved JSON-LD validation to reduce false positives for URLs, dates, and required fields.
    • Accepted more valid URL forms, including protocol-relative links and non-HTTP schemes.
    • Allowed reduced-precision dates such as year-only and year-month formats.
    • Treated empty or blank required values as missing.
    • Preserved JSON-LD script content exactly as written when capturing rendered pages.
  • Documentation

    • Clarified JSON-LD rule behavior in the docs.

oekazuma and others added 2 commits June 26, 2026 11:44
…dered entity handling

SEO018: drop @id from URL_KEYS (it's a node identifier, often a relative
fragment like "#organization"); broaden isAbsoluteUrl to accept any URI scheme
(data:/mailto:/urn:) and protocol-relative URLs, so only scheme-less paths flag.

SEO019: accept schema.org reduced-precision dates (year, year-month) in
isIso8601, keeping calendar/range validation.

SEO021: treat empty/blank/null/[] required values as missing via new hasNonEmpty
helper instead of a presence-only `in` check.

Rendered mode: read JSON-LD via script.rawText, not script.text — `<script>` is
a raw-text element whose entities crawlers don't decode; .text decoded `&quot;`
and corrupted the JSON before SEO016 parsed it.

Docs (en/ja seo018, seo019) and tests updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR updates JSON-LD validation to accept broader URL and ISO date forms, treats blank required values as missing, and preserves raw <script type="application/ld+json"> contents during rendered HTML parsing. Matching docs, tests, and a release note were updated.

Changes

JSON-LD validation updates

Layer / File(s) Summary
URL and date predicates
.changeset/seo-jsonld-false-positives.md, packages/core/src/rules/seo/jsonld-engine.ts, docs/src/content/docs/rules/seo018.md, docs/src/content/docs/ja/rules/seo018.md, docs/src/content/docs/rules/seo019.md, docs/src/content/docs/ja/rules/seo019.md, packages/core/test/jsonld-engine.test.ts, packages/core/test/seo-jsonld-rules.test.ts
isAbsoluteUrl accepts any URI scheme and protocol-relative URLs, URL_KEYS no longer includes @id, isIso8601 accepts reduced-precision dates, and the SEO018/SEO019 docs and tests reflect those checks.
Non-empty required values
packages/core/src/rules/seo/jsonld-engine.ts, packages/core/src/rules/seo/seo016-021.ts, packages/core/test/jsonld-engine.test.ts, packages/core/test/seo-jsonld-rules.test.ts
hasNonEmpty(node, key) is added and SEO021 now reports required properties that are missing, blank, or empty rather than only absent keys.
Rendered JSON-LD capture
packages/vite/src/providers/rendered/parse-html.ts, packages/vite/test/parse-html.test.ts
Rendered-mode JSON-LD capture switches to rawText, and the test asserts HTML entities are preserved verbatim.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

A bunny hopped through JSON-LD dew,
Sniffed out false alarms and bade them adieu.
Raw script crumbs stayed crisp in the air,
While dates and URLs leapt everywhere. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fewer JSON-LD false positives across SEO018/019/021 and the rendered JSON-LD entity handling fix.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/seo-jsonld-false-positives

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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/rules/seo/jsonld-engine.ts (1)

92-106: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject out-of-range timezone offsets in isIso8601.

Line 92 accepts any ±dd:dd offset, and Lines 103-106 only range-check the local hh:mm:ss. That means values like 2026-06-26T10:00:00+24:00 or 2026-06-26T10:00:00+00:60 currently return true, so SEO019 will silently accept invalid DateTime values.

Suggested fix
 export function isIso8601(s: string): boolean {
   const str = s.trim();
   if (!/^\d{4}(-\d{2}(-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})?)?)?)?$/.test(str)) return false;
   const y = Number(str.slice(0, 4));
   const hasMonth = str.length >= 7;
   const hasDay = str.length >= 10;
   const m = hasMonth ? Number(str.slice(5, 7)) : 1;
   if (m < 1 || m > 12) return false;
   if (hasDay) {
     const d = Number(str.slice(8, 10));
     const dt = new Date(Date.UTC(y, m - 1, d));
     if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d) return false;
   }
-  const tm = /^\d{4}-\d{2}-\d{2}T(\d{2}):(\d{2})(?::(\d{2}))?/.exec(str);
+  const tm = /^\d{4}-\d{2}-\d{2}T(\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))?$/.exec(str);
   if (tm) {
     const [hh, mm, ss] = [Number(tm[1]), Number(tm[2]), tm[3] !== undefined ? Number(tm[3]) : 0];
     if (hh > 23 || mm > 59 || ss > 59) return false;
+    if (tm[5] !== undefined) {
+      const offH = Number(tm[5]);
+      const offM = Number(tm[6]);
+      if (offH > 23 || offM > 59) return false;
+    }
   }
   return true;
 }
🤖 Prompt for 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.

In `@packages/core/src/rules/seo/jsonld-engine.ts` around lines 92 - 106,
`isIso8601` currently validates the local time parts but does not reject invalid
timezone offsets, so out-of-range values like +24:00 or +00:60 can still pass.
Update the offset validation in `isIso8601` to parse and range-check the
timezone hours and minutes explicitly, alongside the existing `tm` local-time
checks, and return false when the offset is outside valid ISO 8601 bounds.
🤖 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.

Outside diff comments:
In `@packages/core/src/rules/seo/jsonld-engine.ts`:
- Around line 92-106: `isIso8601` currently validates the local time parts but
does not reject invalid timezone offsets, so out-of-range values like +24:00 or
+00:60 can still pass. Update the offset validation in `isIso8601` to parse and
range-check the timezone hours and minutes explicitly, alongside the existing
`tm` local-time checks, and return false when the offset is outside valid ISO
8601 bounds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5aadd58d-e85e-4cbd-acef-2e901523dd9b

📥 Commits

Reviewing files that changed from the base of the PR and between 99cdfbe and 67a5a0e.

📒 Files selected for processing (11)
  • .changeset/seo-jsonld-false-positives.md
  • docs/src/content/docs/ja/rules/seo018.md
  • docs/src/content/docs/ja/rules/seo019.md
  • docs/src/content/docs/rules/seo018.md
  • docs/src/content/docs/rules/seo019.md
  • packages/core/src/rules/seo/jsonld-engine.ts
  • packages/core/src/rules/seo/seo016-021.ts
  • packages/core/test/jsonld-engine.test.ts
  • packages/core/test/seo-jsonld-rules.test.ts
  • packages/vite/src/providers/rendered/parse-html.ts
  • packages/vite/test/parse-html.test.ts

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