svelte-vitals 0.12.0 → 0.14.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/bin.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  knownRuleIds,
7
7
  readPackageVersion,
8
8
  run
9
- } from "./chunk-NBNJRN52.js";
9
+ } from "./chunk-YMRN4VPN.js";
10
10
 
11
11
  // src/bin.ts
12
12
  import mri from "mri";
@@ -164,6 +164,11 @@ function valueFromNodes(nodes) {
164
164
  const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
165
165
  return text.trim().length > 0 ? "static" : "absent";
166
166
  }
167
+ function textFromNodes(nodes) {
168
+ if (!Array.isArray(nodes) || nodes.some((n) => n?.type === "ExpressionTag")) return void 0;
169
+ const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
170
+ return text.trim().length > 0 ? text : void 0;
171
+ }
167
172
  function attrText(attributes, name) {
168
173
  const attr = findAttr(attributes, name);
169
174
  if (!attr) return void 0;
@@ -210,37 +215,63 @@ function tagsFromHead(head) {
210
215
  const children = head?.fragment?.nodes ?? [];
211
216
  for (const node of children) {
212
217
  if (node?.type === "TitleElement") {
213
- tags.push({ kind: "title", value: valueFromNodes(node.fragment?.nodes ?? []) });
218
+ const titleNodes = node.fragment?.nodes ?? [];
219
+ const text = textFromNodes(titleNodes);
220
+ tags.push({ kind: "title", value: valueFromNodes(titleNodes), ...text !== void 0 ? { text } : {} });
214
221
  continue;
215
222
  }
216
223
  if (node?.type !== "RegularElement") continue;
217
224
  if (node.name === "meta") {
225
+ const charset = attrValue(node.attributes, "charset");
226
+ if (charset !== "absent") {
227
+ tags.push({ kind: "meta", name: "charset", value: charset });
228
+ continue;
229
+ }
218
230
  const name = attrText(node.attributes, "name");
219
231
  const property = attrText(node.attributes, "property");
220
232
  const content = name === "robots" ? attrText(node.attributes, "content") : void 0;
221
233
  const noindex = content !== void 0 && /(^|[\s,])(noindex|none)([\s,]|$)/i.test(content);
234
+ const contentValue = attrValue(node.attributes, "content");
235
+ const descText = name === "description" && contentValue === "static" ? attrText(node.attributes, "content") : void 0;
222
236
  tags.push({
223
237
  kind: "meta",
224
238
  ...name ? { name } : {},
225
239
  ...property ? { property } : {},
226
- value: attrValue(node.attributes, "content"),
227
- ...noindex ? { noindex: true } : {}
240
+ value: contentValue,
241
+ ...noindex ? { noindex: true } : {},
242
+ ...descText !== void 0 ? { text: descText } : {}
228
243
  });
229
244
  } else if (node.name === "link") {
230
245
  const rel = attrText(node.attributes, "rel");
231
246
  const hasAs = findAttr(node.attributes, "as") !== void 0;
232
247
  const asLiteral = attrText(node.attributes, "as");
233
248
  const hasCrossorigin = findAttr(node.attributes, "crossorigin") !== void 0;
249
+ const hreflang = attrText(node.attributes, "hreflang");
250
+ const href = attrText(node.attributes, "href");
234
251
  tags.push({
235
252
  kind: "link",
236
253
  ...rel ? { rel } : {},
237
254
  value: attrValue(node.attributes, "href"),
238
255
  ...hasAs ? { hasAs: true } : {},
239
256
  ...asLiteral ? { as: asLiteral } : {},
240
- ...hasCrossorigin ? { hasCrossorigin: true } : {}
257
+ ...hasCrossorigin ? { hasCrossorigin: true } : {},
258
+ // Keep a literal empty hreflang="" (present-but-invalid) so SEO026 can flag it.
259
+ ...hreflang !== void 0 ? { hreflang } : {},
260
+ ...href ? { href } : {}
241
261
  });
242
- } else if (node.name === "script" && attrText(node.attributes, "type") === "application/ld+json") {
243
- tags.push({ kind: "jsonld", value: valueFromNodes(node.fragment?.nodes ?? []) });
262
+ } else if (node.name === "script") {
263
+ const type = attrText(node.attributes, "type");
264
+ if (type === "application/ld+json") {
265
+ const nodes = node.fragment?.nodes ?? [];
266
+ const raw = textFromNodes(nodes);
267
+ tags.push({ kind: "jsonld", value: valueFromNodes(nodes), ...raw !== void 0 ? { jsonld: raw } : {} });
268
+ } else {
269
+ const src = attrText(node.attributes, "src");
270
+ if (src) {
271
+ const blocking = findAttr(node.attributes, "defer") === void 0 && findAttr(node.attributes, "async") === void 0 && type !== "module";
272
+ tags.push({ kind: "script", value: "static", href: src, ...blocking ? { blocking: true } : {} });
273
+ }
274
+ }
244
275
  }
245
276
  }
246
277
  return tags;
@@ -252,6 +283,12 @@ function attrValueOf(attr) {
252
283
  if (v && v.type === "ExpressionTag") return "dynamic";
253
284
  return "absent";
254
285
  }
286
+ function attrTextOf(attr) {
287
+ const v = attr?.value;
288
+ if (!Array.isArray(v) || v.some((n) => n?.type === "ExpressionTag")) return void 0;
289
+ const text = v.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
290
+ return text.trim().length > 0 ? text : void 0;
291
+ }
255
292
  function collectComponents(node, acc) {
256
293
  if (Array.isArray(node)) {
257
294
  for (const child of node) collectComponents(child, acc);
@@ -283,6 +320,10 @@ function collectImages(node, source, acc) {
283
320
  hasWidth: hasSpread || Boolean(findAttr(attrs, "width")),
284
321
  hasHeight: hasSpread || Boolean(findAttr(attrs, "height")),
285
322
  hasLoading: hasSpread || Boolean(findAttr(attrs, "loading")),
323
+ hasAlt: hasSpread || Boolean(findAttr(attrs, "alt")),
324
+ // A literal loading="lazy" only — a spread or dynamic loading={…} must not be flagged.
325
+ lazy: attrText(attrs, "loading") === "lazy",
326
+ hasSrcset: hasSpread || Boolean(findAttr(attrs, "srcset")),
286
327
  line: lineOf(source, node.start)
287
328
  });
288
329
  }
@@ -290,6 +331,20 @@ function collectImages(node, source, acc) {
290
331
  if (key in node) collectImages(node[key], source, acc);
291
332
  }
292
333
  }
334
+ function collectHeadings(node, source, acc) {
335
+ if (Array.isArray(node)) {
336
+ for (const child of node) collectHeadings(child, source, acc);
337
+ return;
338
+ }
339
+ if (!node || typeof node !== "object") return;
340
+ if (node.type === "SvelteHead") return;
341
+ if (node.type === "RegularElement" && typeof node.name === "string" && /^h[1-6]$/.test(node.name)) {
342
+ acc.push({ level: Number(node.name[1]), line: lineOf(source, node.start) });
343
+ }
344
+ for (const key of CHILD_NODE_KEYS) {
345
+ if (key in node) collectHeadings(node[key], source, acc);
346
+ }
347
+ }
293
348
  function parseFile(source, filename) {
294
349
  const ast = parse(source, { modern: true, filename });
295
350
  const heads = [];
@@ -298,11 +353,14 @@ function parseFile(source, filename) {
298
353
  collectComponents(ast.fragment ?? ast, components);
299
354
  const images = [];
300
355
  collectImages(ast.fragment ?? ast, source, images);
356
+ const headings = [];
357
+ collectHeadings(ast.fragment ?? ast, source, headings);
301
358
  return {
302
359
  headTags: heads.flatMap(tagsFromHead),
303
360
  components,
304
361
  imports: collectImports(ast),
305
- images
362
+ images,
363
+ headings
306
364
  };
307
365
  }
308
366
 
@@ -319,10 +377,20 @@ var svelteMetaTagsAdapter = {
319
377
  resolve(use) {
320
378
  const tags = [];
321
379
  const attrs = use.attributes;
322
- const title = findAttr2(attrs, "title") ?? findAttr2(attrs, "titleTemplate");
323
- if (title) tags.push({ kind: "title", value: attrValueOf(title) });
380
+ const titleAttr = findAttr2(attrs, "title");
381
+ const templateAttr = findAttr2(attrs, "titleTemplate");
382
+ const title = titleAttr ?? templateAttr;
383
+ if (title) {
384
+ const value = attrValueOf(title);
385
+ const text = titleAttr && !templateAttr && value === "static" ? attrTextOf(titleAttr) : void 0;
386
+ tags.push({ kind: "title", value, ...text !== void 0 ? { text } : {} });
387
+ }
324
388
  const description = findAttr2(attrs, "description");
325
- if (description) tags.push({ kind: "meta", name: "description", value: attrValueOf(description) });
389
+ if (description) {
390
+ const value = attrValueOf(description);
391
+ const text = value === "static" ? attrTextOf(description) : void 0;
392
+ tags.push({ kind: "meta", name: "description", value, ...text !== void 0 ? { text } : {} });
393
+ }
326
394
  const canonical = findAttr2(attrs, "canonical");
327
395
  if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
328
396
  const robots = findAttr2(attrs, "robots");
@@ -345,9 +413,17 @@ var svelteSeoAdapter = {
345
413
  const tags = [];
346
414
  const attrs = use.attributes;
347
415
  const title = findAttr3(attrs, "title");
348
- if (title) tags.push({ kind: "title", value: attrValueOf(title) });
416
+ if (title) {
417
+ const value = attrValueOf(title);
418
+ const text = value === "static" ? attrTextOf(title) : void 0;
419
+ tags.push({ kind: "title", value, ...text !== void 0 ? { text } : {} });
420
+ }
349
421
  const description = findAttr3(attrs, "description");
350
- if (description) tags.push({ kind: "meta", name: "description", value: attrValueOf(description) });
422
+ if (description) {
423
+ const value = attrValueOf(description);
424
+ const text = value === "static" ? attrTextOf(description) : void 0;
425
+ tags.push({ kind: "meta", name: "description", value, ...text !== void 0 ? { text } : {} });
426
+ }
351
427
  const canonical = findAttr3(attrs, "canonical");
352
428
  if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
353
429
  const openGraph = findAttr3(attrs, "openGraph");
@@ -381,6 +457,8 @@ function tagKey(tag) {
381
457
  return `link:${tag.rel ?? "?"}`;
382
458
  case "jsonld":
383
459
  return "jsonld";
460
+ case "script":
461
+ return `script:${tag.href ?? "?"}`;
384
462
  }
385
463
  }
386
464
  function resolveComponentPath(source, fromFileRel) {
@@ -466,12 +544,16 @@ async function resolveRoute(rt, cwd, pageRel, config) {
466
544
  let broadOwn = false;
467
545
  let broadInherited = false;
468
546
  const images = [];
547
+ const headings = [];
469
548
  for (const { rel, isPage } of files) {
470
549
  const source = await rt.readFile(rt.join(cwd, rel));
471
550
  const parsed = parseFile(source, rel);
472
551
  for (const img of parsed.images) {
473
552
  images.push({ ...img, file: rel });
474
553
  }
554
+ for (const heading of parsed.headings) {
555
+ headings.push({ ...heading, file: rel });
556
+ }
475
557
  const resolved = await resolveFileTags(rt, cwd, rel, parsed, config, MAX_DEPTH, /* @__PURE__ */ new Set([rel]));
476
558
  for (const tag of resolved.tags) {
477
559
  composed.set(tagKey(tag), { ...tag, presence: isPage ? "own" : "inherited", file: rel });
@@ -491,7 +573,8 @@ async function resolveRoute(rt, cwd, pageRel, config) {
491
573
  const route = deriveRoute(pageRel);
492
574
  return {
493
575
  head: { route, source: "static", tags: [...composed.values()], file: pageRel },
494
- images: { route, images }
576
+ images: { route, images },
577
+ headings: { route, headings }
495
578
  };
496
579
  }
497
580
  async function collectRoutes(rt, cwd, config = defaultConfig) {
@@ -499,7 +582,8 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
499
582
  const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config)));
500
583
  return {
501
584
  heads: facts.map((f) => f.head),
502
- images: facts.map((f) => f.images)
585
+ images: facts.map((f) => f.images),
586
+ headings: facts.map((f) => f.headings)
503
587
  };
504
588
  }
505
589
 
@@ -582,9 +666,10 @@ async function analyzeProject(opts = {}) {
582
666
  const collected = await collectRoutes(rt, cwd, config);
583
667
  const heads = collected.heads.filter((h) => matches(h.route));
584
668
  const images = collected.images.filter((i) => matches(i.route));
669
+ const headings = collected.headings.filter((h) => matches(h.route));
585
670
  const project = await collectProjectFacts(rt, cwd);
586
671
  const rules = selectRules(allRules2, config);
587
- const results = applyRuleSeverities(await runRules(rules, { heads, images, project, config }), config);
672
+ const results = applyRuleSeverities(await runRules(rules, { heads, images, headings, project, config }), config);
588
673
  return { results, config, version: readPackageVersion() };
589
674
  }
590
675
  async function run(opts = {}) {
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  knownRuleIds,
7
7
  routeMatcher,
8
8
  run
9
- } from "./chunk-NBNJRN52.js";
9
+ } from "./chunk-YMRN4VPN.js";
10
10
  export {
11
11
  ProjectError,
12
12
  analyzeProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "A SvelteKit SEO checker — not a runtime Web Vitals reporter. Static analysis of your routes' head metadata.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  "mri": "^1.2.0",
43
43
  "svelte": "^5.56.3",
44
44
  "tinyglobby": "^0.2.17",
45
- "@svelte-vitals/core": "0.13.0"
45
+ "@svelte-vitals/core": "0.15.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^24.7.0"