Skip to content

feat(vite): dev-time overlay handle (@svelte-vitals/vite/hooks) - #26

Merged
oekazuma merged 10 commits into
mainfrom
feat/dev-overlay-handle
Jun 22, 2026
Merged

oekazuma merged 10 commits into
mainfrom
feat/dev-overlay-handle

Conversation

@oekazuma

@oekazuma oekazuma commented Jun 22, 2026 •

Copy link
Copy Markdown
Owner

Closes #9.

What

A dev-time overlay: a SvelteKit server handle that, in dev only, analyzes each page's rendered <head> as you navigate and prints SEO warnings for the current route to the dev-server terminal. Because it sees the rendered output, dynamic routes ({data.title}) are checked against what actually renders — the gap static mode and the build plugin can't cover per-navigation.

// src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks';
import { svelteVitalsHandle } from '@svelte-vitals/vite/hooks';

export const handle = sequence(svelteVitalsHandle());

How

  • New subpath export @svelte-vitals/vite/hooks → svelteVitalsHandle(options?): Handle.
  • Reuses the existing parseHtmlHead and the core pipeline (selectRules/runRules/applyRuleSeverities) — core is untouched. New code is the Handle wrapper plus two pure helpers (formatDevReport, findingSignature).
  • transformPageChunk accumulates the page HTML and returns each chunk unchanged (observe-only, never mutates the response). Analysis runs on the final chunk fire-and-forget, so the dev response is never blocked on parsing/rule execution.
  • A synthetic Project marks robots/sitemap present (SEO006/007 aren't page-scoped) and feeds the document's real htmlLang (SEO009). Only penalized findings are printed; clean routes are silent. Per-route dedup re-warns only when findings change.
  • Dev-only guard: DEV from esm-env. It resolves statically to false in production builds (tree-shaken out, not a runtime NODE_ENV read) and its fallback reads no bare process, so non-Node runtimes (edge adapters) pass through without a ReferenceError. The guard sits at the factory level, so the rule set isn't built outside dev. This is the same DEV/PROD signal Svelte itself uses, replacing the earlier process.env.NODE_ENV check.
  • Analysis is wrapped in error-swallowing try/catch so a tool bug never breaks a request; set SVELTE_VITALS_DEBUG to surface those swallowed errors while debugging.
  • esm-env added as a runtime dependency. @sveltejs/kit added as a type-only optional peer + dev dependency (Handle type only; no runtime import).

Scope (intentionally out)

Browser overlay UI, per-request robots/sitemap detection, exit codes / CI gating, cross-route session summary.

Testing

Built via brainstorming → writing-plans → subagent-driven-development (3 tasks, per-task spec+quality review, final whole-branch review = ready to merge). Full suite green: 181 tests (core 65 / vite 31 / cli 85), typecheck clean, prettier + eslint pass, check:publish (publint) covers the new ./hooks export. New tests cover the formatter (penalized-only, ordering, dedup signature) and the handle (missing-title warning, clean-route silence, chunk passthrough, dedup, not-in-dev no-op via an esm-env mock, options.rules plumbing, SVELTE_VITALS_DEBUG debug hatch, unparseable-HTML resilience). Where assertions depend on the now-detached fire-and-forget analysis, tests flush a macrotask first.

Changeset: @svelte-vitals/vite minor. Core unchanged.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a dev-time SEO analysis feature that displays warnings in the terminal for improved development feedback
    • Warnings deduplicate and only re-appear when findings change
  • Documentation

    • Updated README with setup and usage instructions for the new SEO analysis feature
  • Tests

    • Added test coverage for the new dev overlay functionality

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new @svelte-vitals/vite/hooks subpath export is introduced, providing a svelteVitalsHandle SvelteKit handle. The handle buffers rendered HTML via transformPageChunk, parses the <head>, runs configured SEO rules, and emits deduplicated console.warn messages per route during development. It is a no-op in production and never mutates the response.

Changes

Dev-time SEO diagnostics handle

Layer / File(s) Summary
Public contract, package exports, and build wiring
packages/vite/src/hooks/options.ts, packages/vite/src/hooks/index.ts, packages/vite/package.json, packages/vite/tsup.config.ts, pnpm-workspace.yaml
Defines SvelteVitalsHookOptions with optional metaComponents and rules fields, re-exports it alongside svelteVitalsHandle from the hooks index, adds the ./hooks subpath to package.json exports, expands the tsup entry array to src/hooks/index.ts, adds @sveltejs/kit as a peer/dev dependency, and registers it in the workspace catalog.
Formatting utilities: formatDevReport and findingSignature
packages/vite/src/hooks/format.ts, packages/vite/test/dev-format.test.ts
Adds severity glyph/rank tables, a penalized() filter, formatDevReport() (sorts and formats penalized findings as a terminal string), and findingSignature() (deterministic sorted signature for change detection). Tests verify report ordering, empty output for clean pages, signature stability regardless of input order, and that passing findings are ignored.
svelteVitalsHandle implementation and tests
packages/vite/src/hooks/handle.ts, packages/vite/test/dev-handle.test.ts
Implements the handle: buffers HTML chunks, parses the completed <head>, constructs ResolvedHead/Project models, applies rule severities, computes per-route signatures, and emits console.warn only when findings change. Swallows all errors; passes through without analysis in production. Tests cover missing-title warnings, clean-page silence, chunk passthrough, deduplication, production bypass, and error resilience.
README and changeset
README.md, .changeset/dev-overlay-handle.md
Adds a README section documenting svelteVitalsHandle wiring via sequence in src/hooks.server.ts, with notes on dev-only execution, non-mutating behavior, and navigation-triggered re-emission. Adds a changeset entry for the new minor feature.

Sequence Diagram

sequenceDiagram
  participant Browser
  participant SvelteKit
  participant svelteVitalsHandle
  participant formatDevReport
  participant Terminal

  Browser->>SvelteKit: navigate to route
  SvelteKit->>svelteVitalsHandle: handle(event, resolve)
  Note over svelteVitalsHandle: dev mode only
  svelteVitalsHandle->>SvelteKit: resolve(event, { transformPageChunk })
  SvelteKit-->>svelteVitalsHandle: stream HTML chunks
  svelteVitalsHandle->>svelteVitalsHandle: buffer chunks until done=true
  svelteVitalsHandle->>svelteVitalsHandle: parse <head>, run SEO rules
  svelteVitalsHandle->>svelteVitalsHandle: compute findingSignature
  alt signature differs from last visit
    svelteVitalsHandle->>formatDevReport: formatDevReport(route, results, config)
    formatDevReport-->>svelteVitalsHandle: formatted multi-line report
    svelteVitalsHandle->>Terminal: console.warn(report)
  end
  svelteVitalsHandle->>SvelteKit: return unmodified Response
  SvelteKit->>Browser: serve page
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Hop hop, a new hook is here today,
It watches your <head> as pages come to play,
No title? A warning hops out with a glow,
But only in dev — in prod it stays low,
Deduplicated hops, no noise to repeat,
Your SEO garden stays tidy and neat! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(vite): dev-time overlay handle (@svelte-vitals/vite/hooks)' accurately summarizes the main change—adding a new dev-time overlay handle feature via @svelte-vitals/vite/hooks.
Linked Issues check ✅ Passed All objectives from #9 are met: handle intercepts rendered HTML via transformPageChunk (#9), warns during development (#9), enables request-driven testing on dynamic routes (#9), provides real-time terminal feedback (#9), and operates without mutating responses (#9).
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the dev-time overlay feature. No unrelated changes detected in the changeset, package exports, tests, or dependencies.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/dev-overlay-handle

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 and usage tips.

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
README.md (1)

143-145: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move "Dev overlay" from "Upcoming" to "Shipped" in the roadmap.

The "Dev overlay" feature is being shipped in this PR; the roadmap incorrectly lists it under "Upcoming". Update the roadmap to reflect that it is now available.

📝 Proposed roadmap update

Move this line from the "Upcoming" section (line 145):

- **Dev overlay** ([`#9`](https://github.com/oekazuma/svelte-vitals/issues/9)) — warn in-place while developing via `transformPageChunk`, so dynamic routes are seen with real values as pages are visited.

Add it to the "Shipped" section (after line 141):

- **Dev overlay** ([`#9`](https://github.com/oekazuma/svelte-vitals/issues/9)) — SvelteKit handle (`svelteVitalsHandle`, exported from `@svelte-vitals/vite/hooks`) that intercepts rendered `<head>` as pages are visited and prints deduplicated terminal warnings — dev-only, request-driven, never mutates responses.

Then remove the duplicate line from "Upcoming".

🤖 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 `@README.md` around lines 143 - 145, The README.md roadmap incorrectly lists
the "Dev overlay" feature under the "Upcoming" section when it is being shipped
in this PR. Move the "Dev overlay" item from the "Upcoming" section (which
mentions `transformPageChunk`) to the "Shipped" section and update its
description to reflect the actual implementation using the SvelteKit handle
(`svelteVitalsHandle`) exported from `@svelte-vitals/vite/hooks`. Remove the old
"Dev overlay" entry from the "Upcoming" section to avoid duplication.
🤖 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/vite/package.json`:
- Around line 50-56: The package.json currently lists `@sveltejs/kit` as a regular
dependency, but since only the ./hooks subpath requires it at runtime and the
main svelteVitals plugin has no runtime dependency on it, you should make it an
optional peer dependency. Move `@sveltejs/kit` from the dependencies section to a
new peerDependencies section, then add a peerDependenciesMeta section to mark
`@sveltejs/kit` as optional (set optional to true). Keep `@sveltejs/kit` in
devDependencies for local development and testing purposes. This approach
reduces install requirements for users who only use the main plugin without the
hooks functionality.

In `@packages/vite/src/hooks/format.ts`:
- Around line 24-27: The findingSignature function creates a signature using
only the result id and effective severity, which can cause signature collisions
when a finding changes state but retains the same id and severity, leading to
incorrect deduplication. Modify the map operation within findingSignature to
include additional differentiating information from each Result object (such as
state or message) in the signature string alongside the id and
effectiveSeverity, ensuring that findings which change state produce distinct
signatures even when their id and severity remain the same.

---

Outside diff comments:
In `@README.md`:
- Around line 143-145: The README.md roadmap incorrectly lists the "Dev overlay"
feature under the "Upcoming" section when it is being shipped in this PR. Move
the "Dev overlay" item from the "Upcoming" section (which mentions
`transformPageChunk`) to the "Shipped" section and update its description to
reflect the actual implementation using the SvelteKit handle
(`svelteVitalsHandle`) exported from `@svelte-vitals/vite/hooks`. Remove the old
"Dev overlay" entry from the "Upcoming" section to avoid duplication.
🪄 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: 6e17babb-e2de-4095-a6df-6ce01a94da75

📥 Commits

Reviewing files that changed from the base of the PR and between 6774167 and f14abdb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • .changeset/dev-overlay-handle.md
  • README.md
  • packages/vite/package.json
  • packages/vite/src/hooks/format.ts
  • packages/vite/src/hooks/handle.ts
  • packages/vite/src/hooks/index.ts
  • packages/vite/src/hooks/options.ts
  • packages/vite/test/dev-format.test.ts
  • packages/vite/test/dev-handle.test.ts
  • packages/vite/tsup.config.ts
  • pnpm-workspace.yaml

Comment thread packages/vite/package.json
Comment thread packages/vite/src/hooks/format.ts
oekazuma and others added 2 commits June 22, 2026 12:04
Only the ./hooks subpath (svelteVitalsHandle) needs SvelteKit; the main Vite
plugin does not. Marking it optional avoids widening install requirements and
spurious peer warnings for plugin-only consumers. Addresses CodeRabbit review on #26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A penalized finding can change state while keeping the same rule id and
severity (e.g. a tag going missing -> present-but-empty). Keying the dedup
signature only on id+severity suppressed the legitimate re-warning; include
detection.presence/value so changed findings re-print. Addresses CodeRabbit review on #26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

This PR adds a dev-only SvelteKit handle export from @svelte-vitals/vite/hooks that analyzes each visited page’s rendered <head> via transformPageChunk and prints SEO warnings to the dev-server terminal, complementing the existing build-time (prerendered) analysis.

Changes:

  • Add @svelte-vitals/vite/hooks subpath export providing svelteVitalsHandle(options?): Handle plus formatter/dedup helpers.
  • Add dev overlay tests covering warnings, clean-route silence, chunk pass-through, dedup, production no-op, and bad-HTML resilience.
  • Update package exports/build config and docs; add @sveltejs/kit as an optional peer (type-only) and dev dependency.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Documents how to add the dev-time handle in src/hooks.server.ts.
pnpm-workspace.yaml Adds @sveltejs/kit to the workspace catalog for consistent versioning.
pnpm-lock.yaml Locks the new dependency graph after adding @sveltejs/kit (and transitive deps).
packages/vite/tsup.config.ts Builds an additional entrypoint for the new hooks subpath export.
packages/vite/test/dev-handle.test.ts Adds tests for the SvelteKit handle behavior (warnings, dedup, prod pass-through, etc.).
packages/vite/test/dev-format.test.ts Adds tests for dev report formatting and stable dedup signatures.
packages/vite/src/hooks/options.ts Introduces SvelteVitalsHookOptions for the handle’s supported configuration subset.
packages/vite/src/hooks/index.ts Exports svelteVitalsHandle and hook option types for the ./hooks subpath.
packages/vite/src/hooks/handle.ts Implements the dev-only SvelteKit handle using transformPageChunk to observe rendered HTML.
packages/vite/src/hooks/format.ts Adds terminal formatting and signature computation for per-route deduping.
packages/vite/package.json Adds ./hooks export and @sveltejs/kit as optional peer + dev dependency.
.changeset/dev-overlay-handle.md Declares a minor bump for @svelte-vitals/vite for the new feature.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/vite/src/hooks/handle.ts Outdated
Comment thread packages/vite/test/dev-handle.test.ts
oekazuma and others added 3 commits June 22, 2026 12:16
process.env.NODE_ENV threw a ReferenceError where process is undefined (edge
adapters), crashing every request instead of no-op'ing. Guard typeof process
and pass through when unavailable. Also restore NODE_ENV by deleting when it was
originally unset (avoids the string 'undefined' polluting later tests).
Addresses Copilot review on #26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… response path

Replace the hand-rolled `typeof process` / `NODE_ENV` dev guard with esm-env's
`DEV`. It resolves statically to `false` in production builds (so it's tree-shaken
out, not a runtime read) and its fallback reads no bare `process`, covering edge
runtimes too. Move the guard to the factory so the rule set isn't built outside dev.

Run analysis fire-and-forget on the final chunk so the dev response is never blocked
on parsing/rule execution; the chunk is still returned unchanged (observe-only).

Drop the redundant `treatDynamicAs`/`failOn` config (defaults; both moot here) and
add an opt-in `SVELTE_VITALS_DEBUG` escape hatch to surface swallowed tool errors.

Tests: mock esm-env to exercise the not-in-dev no-op (DEV is a static import, so
toggling env vars wouldn't flip it) and flush a macrotask where assertions depend
on the now-detached analysis.

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

Add tests that `options.rules` overrides flow into the handle's config and that
`SVELTE_VITALS_DEBUG` surfaces otherwise-swallowed analysis errors. Document the
handle's `metaComponents`/`rules` options and the debug env var in the README, and
include @svelte-vitals/vite in `check:publish` so the new `./hooks` export map is
validated by publint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

feat: dev-time overlay (transformPageChunk warnings)

2 participants