| name | svelte-vitals |
| description | Use when writing or reviewing SvelteKit routes/components — svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture, accessibility) and how to run the scanner. |
svelte-vitals
When to use
Use this whenever you are writing or reviewing SvelteKit route files (+page.svelte, +layout.svelte) or components in this project — svelte-vitals statically checks SEO, performance, correctness, security, architecture, and accessibility patterns.
Playbook
- After writing or editing code, run
npx svelte-vitals . --diff --reporter agent and fix any findings it reports.
- Before committing, run
npx svelte-vitals . --staged as a pre-commit gate.
- For a rule's full rationale, configurable options and fix examples, run
npx svelte-vitals explain <rule-id> (add --json for a structured object) or open its docs link below. The fix text for a specific finding comes from the run itself, not from explain — see the note above the rule digest.
- When a finding is correct by design — a reviewed
{@html} that is already sanitized, a deliberate $effect — clear it with an inline directive instead of changing the code: <!-- svelte-vitals-disable-next-line <rule-id> --> on the line above in markup, or // svelte-vitals-disable-next-line <rule-id> inside <script>. Review it first; never suppress a finding you have not read.
- Exit codes:
0 nothing failing, 1 a critical finding (or your --fail-on/--min-health threshold), 2 the run itself failed — a 2 says nothing about the code, so never read it as a clean or dirty result.
- For anything else — reporters, the config file, scoping to a change, CI, monorepos — run
npx svelte-vitals docs list and then npx svelte-vitals docs show <name>. Those guides ship inside the CLI, so they match the version installed here; prefer them over searching the web.
Rule digest
A Fix: below is the rule's canonical fix, the same for every occurrence. A line without one is not a rule without a fix — those rules word their fix per finding, so take it from the finding itself: recommendation on each issue in --reporter json, printed as Fix: by --reporter agent.
SEO
- seo/title-presence — Title presence (critical): A unique, non-empty is the single strongest on-page SEO signal and the text shown in search results and browser tabs. Fix: Add a <title> inside svelte:head (a dynamic title is fine). (docs)
- seo/description-presence — Description presence (warning): A meta description is the snippet search engines show under your title; without one they invent one from page text, often poorly. Fix: Add a inside svelte:head, or set description on your meta component. (docs)
- seo/canonical-url — Canonical URL (warning): A canonical URL tells search engines which URL is authoritative, preventing duplicate-content dilution across query-string variants of the same page. Fix: Add inside svelte:head, or set the canonical prop on your meta component. (docs)
- seo/og-image — Open Graph image (warning): og:image is the preview thumbnail shown when the page is shared on social platforms; without it links render bare and get fewer clicks. Fix: Add , or set openGraph.images on your meta component. (docs)
- seo/og-title — Open Graph title (warning): og:title controls the headline shown when the page is shared on social platforms, independent of the document . Fix: Add <meta property="og:title">, or set openGraph.title on your meta component. (docs)
- seo/robots-txt — robots.txt (warning): robots.txt tells crawlers which paths they may fetch and points them to your sitemap; missing it leaves crawl behaviour to defaults. Fix: Create static/robots.txt (or a src/routes/robots.txt/+server endpoint). (docs)
- seo/sitemap-xml — sitemap.xml (warning): A sitemap.xml lists your URLs so search engines can discover and prioritise them, especially pages not well linked internally. Fix: Create static/sitemap.xml (or a src/routes/sitemap.xml/+server endpoint). (docs)
- seo/json-ld — JSON-LD structured data (info): JSON-LD structured data lets search engines render rich results (breadcrumbs, articles, products) for the page. Fix: Add a JSON-LD with literal JSON (Svelte emits the script body as-is). (docs)
- seo/html-lang — (warning): The attribute tells screen readers how to pronounce the page, browsers whether to offer translation, and other assistive tools how to handle the content — Google has said it does not use lang for ranking. Fix: Set the lang attribute on in src/app.html. ()
Performance
- performance/image-dimensions — Image dimensions (warning): An without explicit width and height can trigger layout shift (CLS) as it loads, hurting Core Web Vitals and visual stability — unless the box is reserved another way, e.g. CSS aspect-ratio. Fix: Add explicit width and height attributes to the . (docs)
- performance/image-loading-hint — Image loading hint (info): A loading attribute lets the browser defer offscreen images; without it images load eagerly and can delay more important content. Static analysis cannot tell which image is the LCP, so this is advisory. Fix: Add loading="lazy" to offscreen elements (leave the LCP/hero image eager). (docs)
- performance/preload-missing-as — Preload missing as (warning): A
<link rel="preload"> without an as attribute is ignored by the browser (or fetched a second time), wasting the preload. Fix: Add an as attribute matching the resource type to the preload link. (docs)
- performance/font-preload-crossorigin — Font preload missing crossorigin (warning): A font preload without
crossorigin does not match the actual (CORS) font request, so the preloaded file is never used and the font downloads twice. Fix: Add the crossorigin attribute to the font preload link. (docs)
- performance/lcp-image — LCP image eager loading (warning): Lazy-loading the LCP (first/above-the-fold) image delays the largest paint and hurts Core Web Vitals. The first image is the best static proxy for the LCP candidate. Fix: Remove loading="lazy" from the first/LCP image; consider fetchpriority="high". (docs)
- performance/responsive-image — Responsive image (info): An without srcset ships one fixed-size asset to every device, wasting bytes on small screens. Static analysis cannot measure intended display size, so this is advisory. Fix: Add a srcset (and sizes) to the for responsive delivery. (docs)
- performance/render-blocking-script — Render-blocking script (warning): A synchronous )
- performance/preconnect — Preconnect third-party origin (info): Connecting to a third-party origin (DNS + TCP + TLS) is costly; a preconnect/dns-prefetch hint starts it early so the resource arrives sooner. Fix: Add a preconnect hint for the third-party origin. ()
Correctness
- correctness/each-key — Keyed each block (warning): An unkeyed {#each} adds/removes nodes at the end and rewrites the data of the DOM nodes in between when the list reorders, so element state/focus sticks to positions instead of items; a key lets Svelte insert, move, and delete the right nodes instead. (docs)
- correctness/each-index-key — Index used as each key (warning): Svelte's guidance is explicit: the key must uniquely identify the object — do not use the index. An index key gives items position-based identity, so element state (focus, inputs, transitions) sticks to positions when the list reorders or items are inserted or removed, exactly like an unkeyed block — but the visible key masks the problem. (docs)
- correctness/effect-as-derived — Effect used to derive state (warning): An $effect whose body only assigns to $state is the "useEffect → $effect" anti-pattern: it reruns after render and can cause extra passes or loops. $derived expresses the same dependency declaratively. (docs)
- correctness/effect-as-onmount — Effect used as onMount (warning): An $effect whose body reads no reactive value visible to this analysis runs once after mount and never re-runs on the paths it can see — usually a sign the code belongs in an event handler, {@attach}, or onMount instead of $effect. This can't see a reactive value reached only through a plain function's return value, so a genuinely reactive effect built that way can still be flagged. (docs)
- correctness/unmutated-state — Unmutated $state (info): A $state that is never mutated pays for reactivity (deep proxying, tracking) it never uses; const (or $state.raw) is clearer and cheaper. (docs)
- correctness/prop-mutation — Mutated non-bindable prop (warning): Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. In legacy mode, mutating methods like .push()/.splice() never trigger an update on their own — Svelte's reactivity there is based on assignments, not mutations. Neither case is caught by the compiler, so this rule catches both statically. (docs)
- correctness/stale-prop-derivation — Stale prop derivation (warning): Svelte's guidance is to treat props as though they will change: a plain freezes the first render's value, so the UI silently stops tracking the parent when the prop changes. In runes mode, $derived keeps the computation live at no cost; in legacy mode (export let props), a $: reactive statement does the same job. Fix: Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes mode, or prefix the assignment with $: in legacy mode, keeping the same expression. ()
Security
- security/raw-html — Raw HTML render (warning): {@html} renders its value as unescaped HTML; if the value can contain user input and is not sanitized, it is a cross-site-scripting (XSS) vector. (docs)
- security/javascript-url — javascript: URL (warning): A javascript: URL in href/src/action/formaction breaks under a strict Content-Security-Policy and turns what should be a real navigation into inline script execution on activation — use an event handler on a instead (the same shape is also a classic XSS vector, though detection here is literal-only, so every flagged URL is author-written, not injected). (docs)
- security/handler-state-write — Handler writes imported state (critical): SvelteKit's docs mark this NEVER-DO-THIS: the server is one long-lived process shared by every user, so module state written during a request is visible to ALL later requests. (docs)
- security/server-module-state — Server module-scope state (warning): Module scope on the server is one shared, long-lived instance (SvelteKit docs: "Avoid shared state on the server"): a value reassigned during one user's request is served to every other user, and it silently resets on every deploy or restart. (docs)
- security/shared-state-import — Shared runes-state import on the server (warning): A .svelte.ts module with module-scope $state is one shared instance on the server: mutated, it leaks data between users; read-only, every request sees the same boot-time value instead of per-user data. (docs)
Architecture
- architecture/component-size — Component size (info): A very large component is hard to read, test, and reuse, and is a common sign that several responsibilities should be split out. (docs)
- architecture/prop-count — Prop count (info): A component taking many props is usually doing too much; grouping or splitting keeps its API understandable. (docs)
- architecture/private-scope-import — Private-scope import (info): A unit placed inside a private directory is written for one owner; importing it from elsewhere couples two parts of the tree that were meant to move independently, and the unit belongs higher up instead. Fix: Move this unit out of its private scope, to the directory shared by all of its importers, and update this import. (inert until configured) (docs)
- architecture/unit-entry-file — Unit entry file (info): A directory named after a unit but missing that unit's entry file is either an incomplete unit or a grouping wearing the wrong name; either way the tree no longer says what it means, and tooling that resolves by convention starts guessing. Fix: Make the directory and its entry file agree — add the entry file, or stop declaring this directory a unit. (inert until configured) (docs)
- architecture/directory-naming — Directory naming (info): A directory whose name breaks the convention its location declares stops carrying the meaning the convention gave it, and every reader — human or agent — has to open the directory to learn what it is. Fix: Rename the directory to the declared casing, or narrow the declaration that governs it. (inert until configured) (docs)
- architecture/reserved-directory-names — Reserved directory names (info): A closed set of directory names is only worth writing down if it stays closed: one directory outside it and the table stops describing the tree, so every reader has to open a directory to learn what it holds. Fix: Rename the directory to a declared name, move it under one of them, or add its name to the declaration. (inert until configured) (docs)
- architecture/reserved-name-placement — Reserved name placement (info): A name reserved for one kind of place stops carrying that meaning the moment it appears somewhere else: a reader who has met one exception has to open the directory to learn what it holds. Fix: Move the directory to one of the places declared for its name, rename it, or declare this place for the name. (inert until configured) ()
Accessibility
- a11y/invalid-role — Invalid ARIA role (warning): A role that does not exist in WAI-ARIA (or is abstract, reserved for the spec itself) is ignored or misread by assistive technology, silently breaking the element’s announced semantics. (docs)
- a11y/unknown-aria-attribute — Unknown ARIA attribute (warning): An
aria-* name that does not exist in WAI-ARIA is not recognized by assistive technology, so the attribute is silently ignored instead of doing what the author intended. (docs)
- a11y/required-aria-props — Missing required ARIA props (warning): Some WAI-ARIA roles are unusable to assistive technology without their required state/property attributes — a role="checkbox" with no way to know checked/unchecked announces a control with no discoverable state. (docs)
- a11y/invalid-aria-value — Invalid ARIA attribute value (warning): An
aria-* attribute whose value does not match its spec-defined type (e.g. a boolean given a non-true/false literal) is misread or ignored by assistive technology. (docs)
- a11y/interactive-nesting — Interactive element nested in an interactive element (warning): A control nested inside another interactive element is announced and operated inconsistently across browsers and assistive technology, and inside an
<a href> or a <button> it also violates the HTML content model, which forbids interactive descendants. (docs)
- a11y/accessible-name — Interactive element has no accessible name (warning): A button, link, or image button with no accessible name is announced by assistive technology as its bare role ("button", "link") with nothing to distinguish it from any other control on the page. (docs)
- a11y/label-has-control — has no associated control (warning): A
<label> with no associated control is announced by assistive technology as plain text — clicking or tapping it does not focus the field, and a screen reader gives no relationship between the label and its control. (docs)
- a11y/use-list — Bullet text should be a list (info): A screen reader announces a real / as a list — item count, position, and boundaries. A bullet character typed into plain text carries none of that, so the visual structure is lost on assistive technology. ()