@svelte-vitals/core 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -100,7 +100,7 @@ interface Runtime {
100
100
  * these, so rules never need to know which mode produced them.
101
101
  */
102
102
  interface HeadTag {
103
- kind: 'title' | 'meta' | 'link' | 'jsonld';
103
+ kind: 'title' | 'meta' | 'link' | 'jsonld' | 'script';
104
104
  /** <meta name="...">. */
105
105
  name?: string;
106
106
  /** <meta property="..."> (e.g. og:image). */
@@ -121,6 +121,10 @@ interface HeadTag {
121
121
  text?: string;
122
122
  /** Literal `hreflang` of a `<link rel="alternate">` (e.g. 'en', 'en-US', 'x-default'). Undefined when dynamic/absent. */
123
123
  hreflang?: string;
124
+ /** Literal href (link) / src (script) URL when static — used for third-party origin analysis (PERF008). */
125
+ href?: string;
126
+ /** True for a render-blocking `<script src>` in <head> (no defer/async/module) (PERF007). */
127
+ blocking?: boolean;
124
128
  /** Where this tag was set relative to the route. Never 'none' (absence = no tag). */
125
129
  presence: Exclude<Presence, 'none'>;
126
130
  /** Whether the tag's value is static/dynamic/absent (design §4). */
@@ -160,6 +164,10 @@ interface ImageInfo {
160
164
  hasLoading: boolean;
161
165
  /** True when the <img> has an `alt` attribute at all (incl. empty `alt=""` decorative; SEO025). */
162
166
  hasAlt: boolean;
167
+ /** True when the <img> has a literal `loading="lazy"` (PERF005). Dynamic/spread → false. */
168
+ lazy: boolean;
169
+ /** True when the <img> has a `srcset` attribute (PERF006). */
170
+ hasSrcset: boolean;
163
171
  /** 1-based source line, or 0 if unknown. */
164
172
  line: number;
165
173
  /** Source file the <img> came from. */
@@ -267,10 +275,36 @@ declare const seo009HtmlLang: Rule;
267
275
 
268
276
  declare const perf001ImageDimensions: Rule;
269
277
  declare const perf002ImageLoading: Rule;
278
+ declare const perf006ResponsiveImage: Rule;
270
279
 
271
280
  declare const perf003PreloadAs: Rule;
272
281
  declare const perf004FontPreloadCrossorigin: Rule;
273
282
 
283
+ /**
284
+ * PERF005 — LCP image not lazy-loaded. Lazy-loading the largest contentful paint
285
+ * image delays it. Static analysis approximates the LCP as the first <img> in
286
+ * document order for the route; if that image is loading="lazy", flag it.
287
+ * CLI/static only (the rendered provider does not collect <img>).
288
+ */
289
+ declare const perf005LcpImage: Rule;
290
+
291
+ /**
292
+ * PERF007 — Render-blocking <script> in <head>. A <script src> without
293
+ * defer/async/type=module blocks the parser. SvelteKit's own scripts are
294
+ * module/deferred, so this catches hand-added blocking scripts — in app.html
295
+ * (rendered mode) or in <svelte:head> (static mode). A head with no <script>
296
+ * emits nothing (no signal), like the image rules.
297
+ */
298
+ declare const perf007RenderBlockingScript: Rule;
299
+
300
+ /**
301
+ * PERF008 — Preconnect for third-party origins. A resource from a well-known
302
+ * third-party origin (e.g. Google Fonts) without a preconnect/dns-prefetch pays a
303
+ * connection-setup round-trip. Opt-in by construction: only origins in the
304
+ * allowlist are checked; routes referencing none emit nothing.
305
+ */
306
+ declare const perf008Preconnect: Rule;
307
+
274
308
  declare const seo010Indexability: Rule;
275
309
  declare const seo011TwitterCard: Rule;
276
310
  declare const seo012OgDescription: Rule;
@@ -527,4 +561,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
527
561
  /** Apply per-rule severity overrides to results (design §6). */
528
562
  declare function applyRuleSeverities(results: Result[], config: Config): Result[];
529
563
 
530
- export { BAND_COLOR, type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildHtmlDocument, buildJsonReport, classify, computeHealth, computeScore, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, linkRule, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, runRules, safeHref, scoreBand, scoresByCategory, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, summarize };
564
+ export { BAND_COLOR, type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildHtmlDocument, buildJsonReport, classify, computeHealth, computeScore, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, escapeHtml, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, linkRule, perf001ImageDimensions, perf002ImageLoading, perf003PreloadAs, perf004FontPreloadCrossorigin, perf005LcpImage, perf006ResponsiveImage, perf007RenderBlockingScript, perf008Preconnect, runRules, safeHref, scoreBand, scoresByCategory, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, seo010Indexability, seo011TwitterCard, seo012OgDescription, seo013OgUrl, seo014Viewport, seo015SitemapInRobots, seo016JsonLdValidity, seo017DeprecatedType, seo018RelativeUrl, seo019DateFormat, seo020Placeholder, seo021RequiredProps, seo022TitleLength, seo023DescriptionLength, seo024Charset, seo025ImageAlt, seo026Hreflang, seo027Heading, summarize };
package/dist/index.js CHANGED
@@ -94,7 +94,7 @@ function detect(head, match) {
94
94
  return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
95
95
  }
96
96
  function headTagRule(opts) {
97
- const docsUrl3 = docsUrlFor(opts.id);
97
+ const docsUrl6 = docsUrlFor(opts.id);
98
98
  return {
99
99
  id: opts.id,
100
100
  title: opts.title,
@@ -117,7 +117,7 @@ function headTagRule(opts) {
117
117
  location: head.file,
118
118
  message,
119
119
  recommendation: opts.recommendation,
120
- docsUrl: docsUrl3,
120
+ docsUrl: docsUrl6,
121
121
  // Copy per finding: opts.fix is a rule-level template shared across all
122
122
  // results this rule emits; a fresh object keeps findings independent.
123
123
  ...opts.fix ? { fix: { ...opts.fix } } : {}
@@ -296,7 +296,7 @@ var seo009HtmlLang = {
296
296
 
297
297
  // src/rules/perf/image-rule.ts
298
298
  function imageRule(opts) {
299
- const docsUrl3 = docsUrlFor(opts.id);
299
+ const docsUrl6 = docsUrlFor(opts.id);
300
300
  const category = opts.category ?? "performance";
301
301
  return {
302
302
  id: opts.id,
@@ -320,7 +320,7 @@ function imageRule(opts) {
320
320
  route: route.route,
321
321
  message: opts.label,
322
322
  recommendation: opts.recommendation,
323
- docsUrl: docsUrl3
323
+ docsUrl: docsUrl6
324
324
  });
325
325
  continue;
326
326
  }
@@ -335,7 +335,7 @@ function imageRule(opts) {
335
335
  ...img.line > 0 ? { line: img.line } : {},
336
336
  message: `Missing ${opts.label}`,
337
337
  recommendation: opts.recommendation,
338
- docsUrl: docsUrl3,
338
+ docsUrl: docsUrl6,
339
339
  ...opts.fix ? { fix: { ...opts.fix } } : {}
340
340
  });
341
341
  }
@@ -374,10 +374,24 @@ var perf002ImageLoading = imageRule({
374
374
  },
375
375
  ok: (img) => img.hasLoading
376
376
  });
377
+ var perf006ResponsiveImage = imageRule({
378
+ id: "PERF006",
379
+ title: "Responsive image",
380
+ severity: "info",
381
+ label: "<img> srcset",
382
+ recommendation: "Provide a srcset (and sizes) so the browser can pick a right-sized image per viewport.",
383
+ rationale: "An <img> 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.",
384
+ fix: {
385
+ description: "Add a srcset (and sizes) to the <img> for responsive delivery.",
386
+ snippet: '<img src="/hero.jpg" srcset="/hero-800.jpg 800w, /hero-1600.jpg 1600w" sizes="100vw" width="1600" height="900" alt="\u2026" />',
387
+ lang: "svelte"
388
+ },
389
+ ok: (img) => img.hasSrcset
390
+ });
377
391
 
378
392
  // src/rules/perf/link-rule.ts
379
393
  function linkRule(opts) {
380
- const docsUrl3 = docsUrlFor(opts.id);
394
+ const docsUrl6 = docsUrlFor(opts.id);
381
395
  return {
382
396
  id: opts.id,
383
397
  title: opts.title,
@@ -401,7 +415,7 @@ function linkRule(opts) {
401
415
  route: head.route,
402
416
  message: opts.label,
403
417
  recommendation: opts.recommendation,
404
- docsUrl: docsUrl3
418
+ docsUrl: docsUrl6
405
419
  });
406
420
  continue;
407
421
  }
@@ -418,7 +432,7 @@ function linkRule(opts) {
418
432
  location: tag.file ?? head.file,
419
433
  message: `Missing ${opts.label}`,
420
434
  recommendation: opts.recommendation,
421
- docsUrl: docsUrl3,
435
+ docsUrl: docsUrl6,
422
436
  ...opts.fix ? { fix: { ...opts.fix } } : {}
423
437
  });
424
438
  }
@@ -460,6 +474,175 @@ var perf004FontPreloadCrossorigin = linkRule({
460
474
  ok: (t) => t.hasCrossorigin === true
461
475
  });
462
476
 
477
+ // src/rules/perf/perf005-lcp-image.ts
478
+ var docsUrl = docsUrlFor("PERF005");
479
+ var recommendation = 'Remove loading="lazy" from the LCP/first image and consider fetchpriority="high" so it loads as early as possible.';
480
+ var perf005LcpImage = {
481
+ id: "PERF005",
482
+ title: "LCP image eager loading",
483
+ category: "performance",
484
+ severity: "warning",
485
+ scope: "route",
486
+ rationale: "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.",
487
+ fix: {
488
+ description: 'Remove loading="lazy" from the first/LCP image; consider fetchpriority="high".',
489
+ snippet: '<img src="/hero.jpg" width="1200" height="630" fetchpriority="high" alt="\u2026" />',
490
+ lang: "svelte"
491
+ },
492
+ async check(ctx) {
493
+ const out = [];
494
+ for (const route of ctx.images ?? []) {
495
+ const first = route.images[0];
496
+ if (!first) continue;
497
+ out.push(
498
+ first.lazy ? {
499
+ id: "PERF005",
500
+ category: "performance",
501
+ severity: "warning",
502
+ detection: { presence: "none", value: "absent" },
503
+ route: route.route,
504
+ location: first.file,
505
+ ...first.line > 0 ? { line: first.line } : {},
506
+ message: 'First image (likely LCP) is loading="lazy"',
507
+ recommendation,
508
+ docsUrl,
509
+ fix: { ...perf005LcpImage.fix }
510
+ } : {
511
+ id: "PERF005",
512
+ category: "performance",
513
+ severity: "warning",
514
+ detection: { presence: "own", value: "static" },
515
+ route: route.route,
516
+ message: "LCP image eager loading",
517
+ recommendation,
518
+ docsUrl
519
+ }
520
+ );
521
+ }
522
+ return out;
523
+ }
524
+ };
525
+
526
+ // src/rules/perf/perf007-render-blocking.ts
527
+ var docsUrl2 = docsUrlFor("PERF007");
528
+ var recommendation2 = 'Add defer (or type="module"), or async, to the <script> so it does not block HTML parsing.';
529
+ var perf007RenderBlockingScript = {
530
+ id: "PERF007",
531
+ title: "Render-blocking script",
532
+ category: "performance",
533
+ severity: "warning",
534
+ scope: "route",
535
+ rationale: 'A synchronous <script src> in <head> blocks HTML parsing until it downloads and runs, delaying first paint. defer, async, or type="module" avoids the block.',
536
+ fix: {
537
+ description: 'Add defer (or type="module") / async to the head <script>.',
538
+ snippet: '<script src="/analytics.js" defer></script>',
539
+ lang: "html"
540
+ },
541
+ async check(ctx) {
542
+ const out = [];
543
+ for (const head of ctx.heads) {
544
+ const scripts = head.tags.filter((t) => t.kind === "script");
545
+ if (scripts.length === 0) continue;
546
+ const blocking = scripts.filter((t) => t.blocking);
547
+ if (blocking.length > 0) {
548
+ for (const tag of blocking) {
549
+ out.push({
550
+ id: "PERF007",
551
+ category: "performance",
552
+ severity: "warning",
553
+ detection: { presence: "none", value: "absent" },
554
+ route: head.route,
555
+ // location is a source path (the URL stays in the message), per the rule-engine convention.
556
+ location: tag.file ?? head.file,
557
+ message: `Render-blocking <script>${tag.href ? ` (${tag.href})` : ""} in <head>`,
558
+ recommendation: recommendation2,
559
+ docsUrl: docsUrl2,
560
+ fix: { ...perf007RenderBlockingScript.fix }
561
+ });
562
+ }
563
+ } else {
564
+ out.push({
565
+ id: "PERF007",
566
+ category: "performance",
567
+ severity: "warning",
568
+ detection: { presence: "own", value: "static" },
569
+ route: head.route,
570
+ message: "No render-blocking scripts",
571
+ recommendation: recommendation2,
572
+ docsUrl: docsUrl2
573
+ });
574
+ }
575
+ }
576
+ return out;
577
+ }
578
+ };
579
+
580
+ // src/rules/perf/perf008-preconnect.ts
581
+ var docsUrl3 = docsUrlFor("PERF008");
582
+ var recommendation3 = 'Add <link rel="preconnect"> (or dns-prefetch) for the third-party origin so the connection is set up early.';
583
+ var THIRD_PARTY_ORIGINS = /* @__PURE__ */ new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
584
+ function hostOf(href) {
585
+ const m = /^(?:https?:)?\/\/([^/?#]+)/i.exec(href);
586
+ return m ? m[1].toLowerCase() : void 0;
587
+ }
588
+ var perf008Preconnect = {
589
+ id: "PERF008",
590
+ title: "Preconnect third-party origin",
591
+ category: "performance",
592
+ severity: "info",
593
+ scope: "route",
594
+ rationale: "Connecting to a third-party origin (DNS + TCP + TLS) is costly; a preconnect/dns-prefetch hint starts it early so the resource arrives sooner.",
595
+ fix: {
596
+ description: "Add a preconnect hint for the third-party origin.",
597
+ snippet: '<link rel="preconnect" href="https://fonts.googleapis.com" />',
598
+ lang: "html"
599
+ },
600
+ async check(ctx) {
601
+ const out = [];
602
+ for (const head of ctx.heads) {
603
+ const referenced = /* @__PURE__ */ new Map();
604
+ const covered = /* @__PURE__ */ new Set();
605
+ for (const tag of head.tags) {
606
+ if (tag.kind !== "link" && tag.kind !== "script" || typeof tag.href !== "string") continue;
607
+ const host = hostOf(tag.href);
608
+ if (!host || !THIRD_PARTY_ORIGINS.has(host)) continue;
609
+ if (tag.kind === "link" && (tag.rel === "preconnect" || tag.rel === "dns-prefetch")) covered.add(host);
610
+ else if (!referenced.has(host)) referenced.set(host, tag.file);
611
+ }
612
+ if (referenced.size === 0) continue;
613
+ const missing = [...referenced].filter(([host]) => !covered.has(host));
614
+ if (missing.length === 0) {
615
+ out.push({
616
+ id: "PERF008",
617
+ category: "performance",
618
+ severity: "info",
619
+ detection: { presence: "own", value: "static" },
620
+ route: head.route,
621
+ message: "Third-party origins are preconnected",
622
+ recommendation: recommendation3,
623
+ docsUrl: docsUrl3
624
+ });
625
+ continue;
626
+ }
627
+ for (const [host, file] of missing) {
628
+ out.push({
629
+ id: "PERF008",
630
+ category: "performance",
631
+ severity: "info",
632
+ detection: { presence: "none", value: "absent" },
633
+ route: head.route,
634
+ location: file ?? head.file,
635
+ message: `Third-party origin ${host} used without a preconnect`,
636
+ recommendation: recommendation3,
637
+ docsUrl: docsUrl3,
638
+ fix: { ...perf008Preconnect.fix }
639
+ });
640
+ }
641
+ }
642
+ return out;
643
+ }
644
+ };
645
+
463
646
  // src/rules/seo/seo010-015.ts
464
647
  var SEO010_FIX = {
465
648
  description: 'If this route should be indexed, drop noindex from its <meta name="robots">.',
@@ -475,7 +658,7 @@ var seo010Indexability = {
475
658
  rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
476
659
  fix: SEO010_FIX,
477
660
  async check(ctx) {
478
- const docsUrl3 = docsUrlFor("SEO010");
661
+ const docsUrl6 = docsUrlFor("SEO010");
479
662
  const out = [];
480
663
  for (const head of ctx.heads) {
481
664
  const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
@@ -490,7 +673,7 @@ var seo010Indexability = {
490
673
  location: head.file,
491
674
  message: "Route is noindex \u2014 verify this is intentional",
492
675
  recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
493
- docsUrl: docsUrl3,
676
+ docsUrl: docsUrl6,
494
677
  fix: { ...SEO010_FIX }
495
678
  });
496
679
  }
@@ -745,7 +928,7 @@ var seo016JsonLdValidity = {
745
928
  lang: "svelte"
746
929
  },
747
930
  async check(ctx) {
748
- const docsUrl3 = docsUrlFor("SEO016");
931
+ const docsUrl6 = docsUrlFor("SEO016");
749
932
  const out = [];
750
933
  for (const head of ctx.heads) {
751
934
  for (const tag of jsonldTags(head)) {
@@ -764,7 +947,7 @@ var seo016JsonLdValidity = {
764
947
  location: head.file,
765
948
  message: problem,
766
949
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
767
- docsUrl: docsUrl3,
950
+ docsUrl: docsUrl6,
768
951
  fix: { ...seo016JsonLdValidity.fix }
769
952
  } : {
770
953
  id: "SEO016",
@@ -774,7 +957,7 @@ var seo016JsonLdValidity = {
774
957
  route: head.route,
775
958
  message: "JSON-LD validity",
776
959
  recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
777
- docsUrl: docsUrl3
960
+ docsUrl: docsUrl6
778
961
  }
779
962
  );
780
963
  }
@@ -783,7 +966,7 @@ var seo016JsonLdValidity = {
783
966
  }
784
967
  };
785
968
  function jsonldRule(opts) {
786
- const docsUrl3 = docsUrlFor(opts.id);
969
+ const docsUrl6 = docsUrlFor(opts.id);
787
970
  return {
788
971
  id: opts.id,
789
972
  title: opts.title,
@@ -811,7 +994,7 @@ function jsonldRule(opts) {
811
994
  location: head.file,
812
995
  message: problem,
813
996
  recommendation: opts.recommendation,
814
- docsUrl: docsUrl3,
997
+ docsUrl: docsUrl6,
815
998
  ...opts.fix ? { fix: { ...opts.fix } } : {}
816
999
  } : {
817
1000
  id: opts.id,
@@ -821,7 +1004,7 @@ function jsonldRule(opts) {
821
1004
  route: head.route,
822
1005
  message: opts.label,
823
1006
  recommendation: opts.recommendation,
824
- docsUrl: docsUrl3
1007
+ docsUrl: docsUrl6
825
1008
  }
826
1009
  );
827
1010
  }
@@ -920,7 +1103,7 @@ function visibleLength(s) {
920
1103
 
921
1104
  // src/rules/seo/seo022-023.ts
922
1105
  function lengthRule(opts) {
923
- const docsUrl3 = docsUrlFor(opts.id);
1106
+ const docsUrl6 = docsUrlFor(opts.id);
924
1107
  return {
925
1108
  id: opts.id,
926
1109
  title: opts.title,
@@ -947,7 +1130,7 @@ function lengthRule(opts) {
947
1130
  location: tag.file ?? head.file,
948
1131
  message: problem,
949
1132
  recommendation: opts.recommendation,
950
- docsUrl: docsUrl3
1133
+ docsUrl: docsUrl6
951
1134
  } : {
952
1135
  id: opts.id,
953
1136
  category: "seo",
@@ -956,7 +1139,7 @@ function lengthRule(opts) {
956
1139
  route: head.route,
957
1140
  message: opts.label,
958
1141
  recommendation: opts.recommendation,
959
- docsUrl: docsUrl3
1142
+ docsUrl: docsUrl6
960
1143
  }
961
1144
  );
962
1145
  }
@@ -1022,8 +1205,8 @@ var seo025ImageAlt = imageRule({
1022
1205
  });
1023
1206
 
1024
1207
  // src/rules/seo/seo026-hreflang.ts
1025
- var docsUrl = docsUrlFor("SEO026");
1026
- var recommendation = 'Use valid hreflang codes (e.g. "en", "en-US", "x-default") and include an x-default when you have multiple language alternates.';
1208
+ var docsUrl4 = docsUrlFor("SEO026");
1209
+ var recommendation4 = 'Use valid hreflang codes (e.g. "en", "en-US", "x-default") and include an x-default when you have multiple language alternates.';
1027
1210
  var HREFLANG_RE = /^[a-z]{2,3}(-[a-z]{4})?(-([a-z]{2}|\d{3}))?$/i;
1028
1211
  function isValidHreflang(v) {
1029
1212
  return v.toLowerCase() === "x-default" || HREFLANG_RE.test(v);
@@ -1061,8 +1244,8 @@ var seo026Hreflang = {
1061
1244
  route: head.route,
1062
1245
  location,
1063
1246
  message: problem,
1064
- recommendation,
1065
- docsUrl
1247
+ recommendation: recommendation4,
1248
+ docsUrl: docsUrl4
1066
1249
  } : {
1067
1250
  id: "SEO026",
1068
1251
  category: "seo",
@@ -1070,8 +1253,8 @@ var seo026Hreflang = {
1070
1253
  detection: PASS,
1071
1254
  route: head.route,
1072
1255
  message: "hreflang",
1073
- recommendation,
1074
- docsUrl
1256
+ recommendation: recommendation4,
1257
+ docsUrl: docsUrl4
1075
1258
  }
1076
1259
  );
1077
1260
  }
@@ -1080,8 +1263,8 @@ var seo026Hreflang = {
1080
1263
  };
1081
1264
 
1082
1265
  // src/rules/seo/seo027-heading.ts
1083
- var docsUrl2 = docsUrlFor("SEO027");
1084
- var recommendation2 = "Use exactly one <h1> per page for its main topic; demote extra top-level headings to <h2>+.";
1266
+ var docsUrl5 = docsUrlFor("SEO027");
1267
+ var recommendation5 = "Use exactly one <h1> per page for its main topic; demote extra top-level headings to <h2>+.";
1085
1268
  var seo027Heading = {
1086
1269
  id: "SEO027",
1087
1270
  title: "Heading hierarchy",
@@ -1113,8 +1296,8 @@ var seo027Heading = {
1113
1296
  route: route.route,
1114
1297
  ...where,
1115
1298
  message: problem,
1116
- recommendation: recommendation2,
1117
- docsUrl: docsUrl2
1299
+ recommendation: recommendation5,
1300
+ docsUrl: docsUrl5
1118
1301
  } : {
1119
1302
  id: "SEO027",
1120
1303
  category: "seo",
@@ -1122,8 +1305,8 @@ var seo027Heading = {
1122
1305
  detection: PASS,
1123
1306
  route: route.route,
1124
1307
  message: "Heading hierarchy",
1125
- recommendation: recommendation2,
1126
- docsUrl: docsUrl2
1308
+ recommendation: recommendation5,
1309
+ docsUrl: docsUrl5
1127
1310
  }
1128
1311
  );
1129
1312
  }
@@ -1163,7 +1346,11 @@ var allRules = [
1163
1346
  seo024Charset,
1164
1347
  seo025ImageAlt,
1165
1348
  seo026Hreflang,
1166
- seo027Heading
1349
+ seo027Heading,
1350
+ perf005LcpImage,
1351
+ perf006ResponsiveImage,
1352
+ perf007RenderBlockingScript,
1353
+ perf008Preconnect
1167
1354
  ];
1168
1355
  function explainRule(id) {
1169
1356
  const target = id.toUpperCase();
@@ -1776,6 +1963,10 @@ export {
1776
1963
  perf002ImageLoading,
1777
1964
  perf003PreloadAs,
1778
1965
  perf004FontPreloadCrossorigin,
1966
+ perf005LcpImage,
1967
+ perf006ResponsiveImage,
1968
+ perf007RenderBlockingScript,
1969
+ perf008Preconnect,
1779
1970
  runRules,
1780
1971
  safeHref,
1781
1972
  scoreBand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",