Repository navigation
feat(core): SEO024–SEO027 — charset, image alt, hreflang, heading hierarchy - #59
Conversation
…rarchy Phase D closes the remaining statically-analyzable SEO gaps vs claude-seo: - SEO024 Character encoding: rendered-only <meta charset> presence (lives in app.html, like the viewport rule). Both parsers now model charset as name:'charset'. - SEO025 Image alt text: flags <img> with no alt (empty alt="" passes; static/CLI mode only, like the perf image rules). ImageInfo gains hasAlt; imageRule takes an optional category so this reports under 'seo'. - SEO026 hreflang validity: opt-in check of rel="alternate" hreflang alternates — malformed codes, or 2+ alternates without x-default. HeadTag gains hreflang, captured in both parsers. - SEO027 Heading hierarchy: flags zero/multiple <h1> per page (layout-chain headings count). Adds a page-body headings channel (ctx.headings) collected by both providers. Docs (en+ja) for all four, changeset (core/cli/vite/mcp minor), spec, and tests (27 new). pnpm -r test 420 green; typecheck, lint, docs build (79 pages) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (14)
✅ Files skipped from review due to trivial changes (4)
🚧 Files skipped from review as they are similar to previous changes (7)
📝 WalkthroughWalkthroughAdds SEO024–SEO027 checks and the plumbing to capture charset, hreflang, image alt presence, and page headings in static and rendered analysis. The PR also registers the new rules, updates tests, and adds English and Japanese documentation. ChangesSEO Phase D static gaps
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/vite/test/parse-html.test.ts (1)
128-133: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: heading test doesn't distinguish document order from level-grouped order.
The rendered
parseHtmlHeadcollects headings by iterating level 1→6, whereas the staticcollectHeadingswalks the AST in document order. The<h1><h2><h2>input here yields[1,2,2]under both strategies, so this case can't catch the divergence. SEO027 only counts<h1>, so it's not a current defect — but if heading-hierarchy order ever matters, consider a case like<h2>…<h1>to lock the intended ordering.🤖 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/vite/test/parse-html.test.ts` around lines 128 - 133, The heading test in parseHtmlHead currently uses an input whose result is the same whether headings are collected in document order or grouped by level, so it does not catch the ordering difference between parseHtmlHead and collectHeadings. Update the SEO027 test case in the parse-html test to use a heading sequence where order and level grouping diverge, such as a lower-level heading appearing before a higher-level one, so the expectation clearly locks the intended document-order behavior.packages/vite/src/providers/rendered/parse-html.ts (1)
95-101: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winPreserve document order when collecting headings.
This loop groups headings by level, so
<h2>…</h2><h1>…</h1>becomes[1, 2]instead of[2, 1]. The source parser keeps document order, so this creates a provider-parity mismatch and can skew future hierarchy/skip-level checks. Traverse the DOM once in order instead of switching to a comma-separated selector;node-html-parserdoesn’t reliably preserve order for grouped selectors.🤖 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/vite/src/providers/rendered/parse-html.ts` around lines 95 - 101, Preserve heading document order in parseHtml’s heading collection logic: the current loop in the headings scan groups by level and loses the source sequence, causing provider-parity mismatches. Update the heading collection in parse-html.ts to traverse the DOM once in order (rather than iterating level-by-level with querySelectorAll), using the existing headings handling around the Page-body headings SEO027 block so the collected array matches the original document order.docs/superpowers/specs/2026-06-26-seo-static-gaps-phase-d-design.md (1)
218-221: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the empty fenced code block at the end.
The trailing empty
```block triggers markdownlint (MD040) and serves no purpose.## Suggested delivery Two PRs to keep review tractable, or one if preferred: - **PR1 (head/image rules):** SEO024 charset, SEO025 image alt, SEO026 hreflang — small, additive capture extensions to existing channels. - **PR2 (body channel):** SEO027 heading hierarchy — introduces the new `ctx.headings` channel in both providers. - -```🤖 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 `@docs/superpowers/specs/2026-06-26-seo-static-gaps-phase-d-design.md` around lines 218 - 221, Remove the trailing empty fenced code block from the document so markdownlint MD040 no longer flags the file. Locate the stray fence at the end of the spec content and delete the opening/closing backticks, leaving only the intended prose in the section.
🤖 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 `@packages/core/src/rules/seo/seo026-hreflang.ts`:
- Around line 34-52: The SEO026 rule is losing the original offending <link>
tag, so failures are attributed to head.file instead of the real source. Update
seo026-hreflang’s tag collection and validation flow to retain the tag/file
metadata alongside each hreflang value, then set the result location from the
offending tag (for example, tag.file ?? head.file) when building the warning.
Keep the current detection/status behavior unchanged; only improve source
attribution in the SEO026 result object.
- Around line 9-15: Tighten SEO026 hreflang validation and preserve the
originating file for invalid alternates. Update isValidHreflang/HREFLANG_RE in
seo026-hreflang.ts so it only accepts lang, lang-REGION, or x-default, and
rejects script/extra-subtag forms like zh-Hant-TW. Also adjust the
alternate-validation path in SEO026’s rule logic to report the matched tag’s own
file instead of always defaulting to head.file, using the existing
alternate/matched tag symbols to locate the source correctly.
---
Nitpick comments:
In `@docs/superpowers/specs/2026-06-26-seo-static-gaps-phase-d-design.md`:
- Around line 218-221: Remove the trailing empty fenced code block from the
document so markdownlint MD040 no longer flags the file. Locate the stray fence
at the end of the spec content and delete the opening/closing backticks, leaving
only the intended prose in the section.
In `@packages/vite/src/providers/rendered/parse-html.ts`:
- Around line 95-101: Preserve heading document order in parseHtml’s heading
collection logic: the current loop in the headings scan groups by level and
loses the source sequence, causing provider-parity mismatches. Update the
heading collection in parse-html.ts to traverse the DOM once in order (rather
than iterating level-by-level with querySelectorAll), using the existing
headings handling around the Page-body headings SEO027 block so the collected
array matches the original document order.
In `@packages/vite/test/parse-html.test.ts`:
- Around line 128-133: The heading test in parseHtmlHead currently uses an input
whose result is the same whether headings are collected in document order or
grouped by level, so it does not catch the ordering difference between
parseHtmlHead and collectHeadings. Update the SEO027 test case in the parse-html
test to use a heading sequence where order and level grouping diverge, such as a
lower-level heading appearing before a higher-level one, so the expectation
clearly locks the intended document-order 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: 6ed44a6a-30cb-4b0e-9dac-0d96712b25f3
📒 Files selected for processing (32)
.changeset/seo-static-gaps-phase-d.mddocs/src/content/docs/ja/rules/seo024.mddocs/src/content/docs/ja/rules/seo025.mddocs/src/content/docs/ja/rules/seo026.mddocs/src/content/docs/ja/rules/seo027.mddocs/src/content/docs/rules/seo024.mddocs/src/content/docs/rules/seo025.mddocs/src/content/docs/rules/seo026.mddocs/src/content/docs/rules/seo027.mddocs/superpowers/specs/2026-06-26-seo-static-gaps-phase-d-design.mdpackages/cli/src/index.tspackages/cli/src/providers/source/parse.tspackages/cli/src/providers/source/routes.tspackages/cli/test/parse-static-gaps.test.tspackages/core/src/head.tspackages/core/src/headings.tspackages/core/src/images.tspackages/core/src/index.tspackages/core/src/rule.tspackages/core/src/rules/index.tspackages/core/src/rules/perf/image-rule.tspackages/core/src/rules/seo/seo024-charset.tspackages/core/src/rules/seo/seo025-image-alt.tspackages/core/src/rules/seo/seo026-hreflang.tspackages/core/src/rules/seo/seo027-heading.tspackages/core/test/perf-rules.test.tspackages/core/test/seo-static-gaps-rules.test.tspackages/vite/src/analyze.tspackages/vite/src/providers/rendered/collect.tspackages/vite/src/providers/rendered/parse-html.tspackages/vite/test/dev-handle.test.tspackages/vite/test/parse-html.test.ts
…g order - SEO026: broaden hreflang regex to a sound BCP-47 subset (language + optional script + optional region incl. 3-digit UN M49 like es-419) instead of tightening — the prior pattern false-positived valid es-419 and the docs under-described accepted forms. Reconciles regex with docs/spec. - SEO026: attribute a malformed-hreflang finding to the offending <link>'s own file (tag.file ?? head.file), matching the SEO022/023 convention, instead of always the route head file. - parse-html (rendered): collect page-body headings via an ordered DOM walk so levels match the static provider's document order (level-grouping diverged for inputs like <h2>…<h1>). - Tests: lock document order in the rendered heading test; add a BCP-47 script/numeric-region acceptance test. Docs (en+ja) + spec wording aligned; drop a stray empty code fence in the spec. pnpm -r test 421 green; typecheck, lint, docs build (79 pages) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mode The dev `handle` discarded the `headings` parsed from the rendered HTML and never passed them to runRules, so SEO027 (heading hierarchy) silently no-op'd in the dev server even though the data was available. Wire the parsed heading levels into the RuleContext, mirroring analyze.ts / collect.ts. Add a dev-handle test that drives a two-<h1> page through the hook and asserts SEO027 fires; document the static vs rendered heading-collection differences in the SEO027 reference pages (en + ja). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds SEO Phase D coverage by introducing four new statically-analyzable SEO rules (SEO024–SEO027) and extending both static (CLI) and rendered (vite) analysis pipelines to capture the additional signals (charset, hreflang, image alt presence, and heading hierarchy), along with tests and documentation updates.
Changes:
- Introduce new core SEO rules: charset presence (rendered-only), image alt presence (static-only), hreflang validation (both), and single-
<h1>heading hierarchy (both). - Extend parsers/providers to capture
<meta charset>,hreflang,<img>alt presence, and page-body heading levels; thread a newctx.headingschannel through analysis entrypoints. - Add/adjust unit tests, docs reference pages (EN/JA), and a changeset for minor releases.
Reviewed changes
Copilot reviewed 33 out of 33 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/vite/test/parse-html.test.ts | Adds rendered-parser tests for charset/hreflang/headings capture. |
| packages/vite/test/dev-handle.test.ts | Adds dev-hook coverage ensuring SEO027 can read rendered body headings. |
| packages/vite/src/providers/rendered/parse-html.ts | Captures <meta charset>, hreflang, and collects heading levels from rendered HTML. |
| packages/vite/src/providers/rendered/collect.ts | Threads rendered headings into collected analysis data. |
| packages/vite/src/hooks/handle.ts | Threads rendered headings into runRules context for dev warnings. |
| packages/vite/src/analyze.ts | Threads rendered headings into runRules context for prerender analysis. |
| packages/core/test/seo-static-gaps-rules.test.ts | Adds unit tests for SEO024–SEO027 rule behavior and mode applicability. |
| packages/core/test/perf-rules.test.ts | Updates perf test fixtures for new ImageInfo.hasAlt field. |
| packages/core/src/rules/seo/seo027-heading.ts | Implements the single-<h1> route-scoped heading hierarchy rule. |
| packages/core/src/rules/seo/seo026-hreflang.ts | Implements route-scoped hreflang validation with opt-in behavior. |
| packages/core/src/rules/seo/seo025-image-alt.ts | Implements static-only image alt presence rule via imageRule. |
| packages/core/src/rules/seo/seo024-charset.ts | Implements rendered-only charset presence rule via headTagRule. |
| packages/core/src/rules/perf/image-rule.ts | Generalizes image-rule factory to allow non-performance categories (SEO025). |
| packages/core/src/rules/index.ts | Registers and re-exports SEO024–SEO027 in allRules. |
| packages/core/src/rule.ts | Extends RuleContext with optional headings channel. |
| packages/core/src/index.ts | Exports new heading types and new SEO rules from the package entrypoint. |
| packages/core/src/images.ts | Adds ImageInfo.hasAlt to support SEO025. |
| packages/core/src/headings.ts | Introduces normalized heading types (HeadingInfo, ResolvedHeadings). |
| packages/core/src/head.ts | Adds HeadTag.hreflang to support SEO026. |
| packages/cli/test/parse-static-gaps.test.ts | Adds static-parser tests for charset/hreflang/alt/headings capture. |
| packages/cli/src/providers/source/routes.ts | Threads per-route headings through the static provider (layout chain included). |
| packages/cli/src/providers/source/parse.ts | Captures charset/hreflang/alt/headings from Svelte AST. |
| packages/cli/src/index.ts | Threads static headings into runRules context. |
| docs/superpowers/specs/2026-06-26-seo-static-gaps-phase-d-design.md | Adds design spec for Phase D rules and capture model changes. |
| docs/src/content/docs/rules/seo024.md | Adds SEO024 rule documentation (EN). |
| docs/src/content/docs/rules/seo025.md | Adds SEO025 rule documentation (EN). |
| docs/src/content/docs/rules/seo026.md | Adds SEO026 rule documentation (EN). |
| docs/src/content/docs/rules/seo027.md | Adds SEO027 rule documentation (EN). |
| docs/src/content/docs/ja/rules/seo024.md | Adds SEO024 rule documentation (JA). |
| docs/src/content/docs/ja/rules/seo025.md | Adds SEO025 rule documentation (JA). |
| docs/src/content/docs/ja/rules/seo026.md | Adds SEO026 rule documentation (JA). |
| docs/src/content/docs/ja/rules/seo027.md | Adds SEO027 rule documentation (JA). |
| .changeset/seo-static-gaps-phase-d.md | Declares minor bumps for affected packages and summarizes additions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…g scope - SEO026: accept "x-default" case-insensitively (both the validity check and the multiple-alternates x-default presence check). - hreflang: keep a literal empty hreflang="" (present-but-invalid) in both parsers instead of dropping it, so SEO026 flags it. - headings: collect body headings only — the static collector now skips <svelte:head>, and the rendered collector scopes to <body> (fallback root for fragments) — so a stray <h1> in the head is not counted. - SEO027: include the line number on a "Missing <h1>" finding when known (static mode), matching the multiple-<h1> branch. Tests added for each (empty/uppercase hreflang, head-scoped headings). pnpm -r test 428 green; typecheck, lint, docs build (79 pages) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SEO Phase D (after Phase B length, #58): close the remaining statically-analyzable SEO checks covered by claude-seo that fit svelte-vitals' design. claude-seo is a live, API/crawl/agent-driven audit suite — its CrUX/Lighthouse/GSC/GA4/backlinks/local/E-E-A-T coverage is out of scope for a dependency-free static linter. The overlap that does fit is four rules.
New rules
<meta charset>)warningwarningwarning<h1>)warningEach emits no signal when its subject is invisible to a mode (no false positives), following the existing
appliesTo(viewport) and image-collection conventions.How it works
app.html, so rendered-only (appliesTo: rendered, like SEO014 viewport). Both parsers now model<meta charset>asname:'charset'(previously dropped — it has neither name nor property).ImageInfogainshasAlt;imageRulegains an optionalcategoryso this reports underseo. Emptyalt=""passes (decorative); spread{...rest}is not flagged. CLI-only, like the perf image rules.HeadTaggainshreflang, captured in both parsers. Opt-in: a route with norel="alternate" hreflangemits nothing. Flags malformed codes and 2+ alternates without anx-default.ctx.headings) collected by both providers (CLI walks the AST; rendered queries the parsed body). Flags zero/multiple<h1>; layout-chain headings count.Surfaces & docs
@svelte-vitals/core+svelte-vitals+@svelte-vitals/vite+@svelte-vitals/mcpminor (MCP surfaces them viaallRules).docs/superpowers/specs/.Validation
pnpm -r test(420: core 192 / vite 65 / cli 154 / mcp 9; +27),pnpm -r typecheck,pnpm lint,pnpm --filter docs build(79 pages incl. 8 new) — all green.Two clean-page fixtures gained a valid
<h1>+<meta charset>(they were genuinely flagged under the new rules); existing perf-image test fixtures gainedhasAlt— additive, no assertions loosened.🤖 Generated with Claude Code
Summary by CodeRabbit
<meta charset>, missing imagealt, invalidhreflang(withx-defaultguidance), and heading hierarchy (exactly one<h1>, including layout chain).hreflang, and headings are checked in the correct mode with fewer false/misleading results.